660 lines
25 KiB
TypeScript
660 lines
25 KiB
TypeScript
/**
|
||
* AdminUserAccessOverviewPage
|
||
*
|
||
* Admin page for viewing comprehensive user access permissions.
|
||
* Shows what pages a user can see and what data they can access.
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { FaSync, FaUserShield, FaEye, FaDatabase, FaCube, FaChevronDown, FaChevronRight, FaCheckCircle, FaTimesCircle, FaInfoCircle } from 'react-icons/fa';
|
||
import api from '../../api';
|
||
import styles from './Admin.module.css';
|
||
|
||
interface UserOption {
|
||
id: string;
|
||
username: string;
|
||
email: string;
|
||
fullName: string;
|
||
isSysAdmin: boolean;
|
||
enabled: boolean;
|
||
}
|
||
|
||
interface RoleInfo {
|
||
id: string;
|
||
roleLabel: string;
|
||
description: { [key: string]: string };
|
||
scope: 'global' | 'mandate' | 'instance';
|
||
mandateId?: string;
|
||
featureInstanceId?: string;
|
||
source: string;
|
||
sourceMandateId?: string;
|
||
sourceMandateName?: string;
|
||
sourceInstanceId?: string;
|
||
sourceInstanceLabel?: string;
|
||
}
|
||
|
||
interface AccessEntry {
|
||
item: string;
|
||
view?: boolean;
|
||
read?: string;
|
||
create?: string;
|
||
update?: string;
|
||
delete?: string;
|
||
grantedByRoleLabels: string[];
|
||
roleScope: string;
|
||
}
|
||
|
||
interface MandateInfo {
|
||
id: string;
|
||
name: string;
|
||
label?: string;
|
||
roleIds: string[];
|
||
featureInstances: {
|
||
id: string;
|
||
label: string;
|
||
featureCode: string;
|
||
featureLabel: { [key: string]: string };
|
||
roleIds: string[];
|
||
}[];
|
||
}
|
||
|
||
interface UserAccessOverview {
|
||
user: UserOption;
|
||
isSysAdmin: boolean;
|
||
sysAdminNote?: string;
|
||
roles: RoleInfo[];
|
||
mandates: MandateInfo[];
|
||
uiAccess: AccessEntry[];
|
||
dataAccess: AccessEntry[];
|
||
resourceAccess: AccessEntry[];
|
||
}
|
||
|
||
type TabId = 'overview' | 'ui' | 'data' | 'resources';
|
||
|
||
export const AdminUserAccessOverviewPage: React.FC = () => {
|
||
const [users, setUsers] = useState<UserOption[]>([]);
|
||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||
const [overview, setOverview] = useState<UserAccessOverview | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
||
const [expandedRoles, setExpandedRoles] = useState<Set<string>>(new Set());
|
||
const [expandedMandates, setExpandedMandates] = useState<Set<string>>(new Set());
|
||
|
||
// Fetch users list on mount
|
||
useEffect(() => {
|
||
const fetchUsers = async () => {
|
||
try {
|
||
setLoadingUsers(true);
|
||
const response = await api.get('/api/admin/user-access-overview/users');
|
||
setUsers(response.data);
|
||
} catch (err: any) {
|
||
setError(err?.response?.data?.detail || err?.message || 'Failed to fetch users');
|
||
} finally {
|
||
setLoadingUsers(false);
|
||
}
|
||
};
|
||
|
||
fetchUsers();
|
||
}, []);
|
||
|
||
// Fetch access overview when user is selected
|
||
useEffect(() => {
|
||
if (!selectedUserId) {
|
||
setOverview(null);
|
||
return;
|
||
}
|
||
|
||
const fetchOverview = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
const response = await api.get(`/api/admin/user-access-overview/${selectedUserId}`);
|
||
const data = response.data;
|
||
setOverview(data);
|
||
|
||
// Auto-expand all mandates
|
||
setExpandedMandates(new Set(data.mandates?.map((m: MandateInfo) => m.id) || []));
|
||
} catch (err: any) {
|
||
setError(err?.response?.data?.detail || err?.message || 'Failed to fetch overview');
|
||
setOverview(null);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchOverview();
|
||
}, [selectedUserId]);
|
||
|
||
const toggleRole = (roleId: string) => {
|
||
setExpandedRoles(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(roleId)) {
|
||
newSet.delete(roleId);
|
||
} else {
|
||
newSet.add(roleId);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
const toggleMandate = (mandateId: string) => {
|
||
setExpandedMandates(prev => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(mandateId)) {
|
||
newSet.delete(mandateId);
|
||
} else {
|
||
newSet.add(mandateId);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
const getScopeColor = (scope: string): string => {
|
||
switch (scope) {
|
||
case 'instance': return '#10b981';
|
||
case 'mandate': return '#3b82f6';
|
||
case 'global': return '#8b5cf6';
|
||
default: return '#6b7280';
|
||
}
|
||
};
|
||
|
||
const getAccessLevelColor = (level: string): string => {
|
||
switch (level) {
|
||
case 'ALL': return '#10b981';
|
||
case 'GROUP': return '#3b82f6';
|
||
case 'MY': return '#f59e0b';
|
||
case 'NONE': return '#ef4444';
|
||
default: return '#6b7280';
|
||
}
|
||
};
|
||
|
||
const renderOverviewTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
{/* SysAdmin Notice */}
|
||
{overview.isSysAdmin && (
|
||
<div className={styles.infoBox} style={{ background: '#fef3c7', borderColor: '#f59e0b' }}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: '#f59e0b' }} />
|
||
<span>{overview.sysAdminNote || 'Dieser Benutzer ist SysAdmin und hat vollen Systemzugriff.'}</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Mandates & Feature Instances */}
|
||
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>Mandate & Feature-Instanzen</h3>
|
||
{overview.mandates.length === 0 ? (
|
||
<p className={styles.emptyHint}>Keine Mandate-Zuordnungen vorhanden.</p>
|
||
) : (
|
||
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
|
||
{overview.mandates.map(mandate => (
|
||
<div key={mandate.id} className={styles.roleCard}>
|
||
<div
|
||
className={styles.roleHeader}
|
||
onClick={() => toggleMandate(mandate.id)}
|
||
>
|
||
<div className={styles.roleInfo}>
|
||
{expandedMandates.has(mandate.id) ? <FaChevronDown className={styles.expandIcon} /> : <FaChevronRight className={styles.expandIcon} />}
|
||
<span className={styles.roleLabel}>{mandate.label || mandate.name}</span>
|
||
<span className={styles.roleDescription}>
|
||
{mandate.featureInstances.length} Feature-Instanz(en)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{expandedMandates.has(mandate.id) && (
|
||
<div className={styles.roleContent}>
|
||
{mandate.featureInstances.length === 0 ? (
|
||
<p className={styles.emptyHint}>Keine Feature-Instanzen zugewiesen.</p>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||
{mandate.featureInstances.map(instance => (
|
||
<div
|
||
key={instance.id}
|
||
style={{
|
||
padding: '0.75rem',
|
||
background: 'var(--bg-secondary)',
|
||
borderRadius: '6px',
|
||
border: '1px solid var(--border-color)',
|
||
}}
|
||
>
|
||
<div style={{ fontWeight: 500, marginBottom: '0.25rem' }}>
|
||
{instance.label}
|
||
</div>
|
||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||
Feature: {instance.featureLabel?.de || instance.featureCode}
|
||
</div>
|
||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
|
||
Rollen: {instance.roleIds.length > 0
|
||
? overview.roles
|
||
.filter(r => instance.roleIds.includes(r.id))
|
||
.map(r => r.roleLabel)
|
||
.join(', ')
|
||
: 'Keine'
|
||
}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Roles */}
|
||
<h3 style={{ marginTop: '2rem', marginBottom: '1rem', color: 'var(--text-primary)' }}>Zugewiesene Rollen</h3>
|
||
{overview.roles.length === 0 ? (
|
||
<p className={styles.emptyHint}>Keine Rollen zugewiesen.</p>
|
||
) : (
|
||
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
|
||
{overview.roles.map(role => (
|
||
<div key={role.id} className={styles.roleCard}>
|
||
<div
|
||
className={styles.roleHeader}
|
||
onClick={() => toggleRole(role.id)}
|
||
>
|
||
<div className={styles.roleInfo}>
|
||
{expandedRoles.has(role.id) ? <FaChevronDown className={styles.expandIcon} /> : <FaChevronRight className={styles.expandIcon} />}
|
||
<span className={styles.roleLabel}>{role.roleLabel}</span>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getScopeColor(role.scope),
|
||
color: 'white',
|
||
marginLeft: '0.5rem'
|
||
}}
|
||
>
|
||
{role.scope}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{expandedRoles.has(role.id) && (
|
||
<div className={styles.roleContent}>
|
||
<div style={{ fontSize: '0.875rem' }}>
|
||
<p><strong>Beschreibung:</strong> {role.description?.de || role.description?.en || '-'}</p>
|
||
<p><strong>Quelle:</strong> {role.source === 'mandate'
|
||
? `Mandate: ${role.sourceMandateName}`
|
||
: `Feature-Instanz: ${role.sourceInstanceLabel}`
|
||
}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderUiAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>UI-Zugriffsrechte bestimmen, welche Seiten und Views der Benutzer sehen kann.</span>
|
||
</div>
|
||
|
||
{overview.uiAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaEye className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine UI-Berechtigungen</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>UI-Element</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Sichtbar</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.uiAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
{entry.view ? (
|
||
<FaCheckCircle style={{ color: '#10b981' }} />
|
||
) : (
|
||
<FaTimesCircle style={{ color: '#ef4444' }} />
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderDataAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>
|
||
Daten-Zugriffsrechte: <strong>ALL</strong> = Alle Datensätze, <strong>GROUP</strong> = Gruppen-Datensätze,
|
||
<strong> MY</strong> = Eigene Datensätze, <strong>NONE</strong> = Kein Zugriff
|
||
</span>
|
||
</div>
|
||
|
||
{overview.dataAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaDatabase className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine Daten-Berechtigungen</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Tabelle/Feld</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Lesen</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Erstellen</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Update</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Löschen</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.dataAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.read || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.read || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.create || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.create || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.update || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.update || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
<span
|
||
className={styles.badge}
|
||
style={{
|
||
background: getAccessLevelColor(entry.delete || '-'),
|
||
color: 'white',
|
||
fontSize: '0.7rem'
|
||
}}
|
||
>
|
||
{entry.delete || '-'}
|
||
</span>
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderResourceAccessTab = () => {
|
||
if (!overview) return null;
|
||
|
||
return (
|
||
<div className={styles.scrollableContent}>
|
||
<div className={styles.infoBox}>
|
||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||
<span>Ressourcen-Zugriffsrechte bestimmen, welche System-Ressourcen (z.B. AI-Modelle) der Benutzer verwenden kann.</span>
|
||
</div>
|
||
|
||
{overview.resourceAccess.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaCube className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine Ressourcen-Berechtigungen</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Ressource</th>
|
||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Zugriff</th>
|
||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{overview.resourceAccess.map((entry, idx) => (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: '1px solid var(--border-color)',
|
||
background: idx % 2 === 0 ? 'transparent' : 'var(--bg-secondary)'
|
||
}}
|
||
>
|
||
<td style={{ padding: '0.75rem', fontFamily: 'monospace', fontSize: '0.875rem' }}>
|
||
{entry.item}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||
{entry.view ? (
|
||
<FaCheckCircle style={{ color: '#10b981' }} />
|
||
) : (
|
||
<FaTimesCircle style={{ color: '#ef4444' }} />
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', color: 'var(--text-secondary)' }}>
|
||
{entry.grantedByRoleLabels?.join(', ') || '-'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (error && !overview) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => window.location.reload()}
|
||
>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>Benutzer-Zugriffsübersicht</h1>
|
||
<p className={styles.pageSubtitle}>Zeigt alle Berechtigungen eines Benutzers an</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* User Selection */}
|
||
<div className={styles.filterSection}>
|
||
<div className={styles.filterGroup}>
|
||
<label className={styles.filterLabel}>
|
||
<FaUserShield style={{ marginRight: '0.5rem' }} />
|
||
Benutzer auswählen:
|
||
</label>
|
||
<select
|
||
className={styles.filterSelect}
|
||
value={selectedUserId}
|
||
onChange={(e) => setSelectedUserId(e.target.value)}
|
||
disabled={loadingUsers}
|
||
style={{ minWidth: '300px' }}
|
||
>
|
||
<option value="">-- Benutzer wählen --</option>
|
||
{users.map(user => (
|
||
<option key={user.id} value={user.id}>
|
||
{user.fullName || user.username} ({user.email})
|
||
{user.isSysAdmin && ' [SysAdmin]'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{selectedUserId && (
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => setSelectedUserId(selectedUserId)}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{!selectedUserId ? (
|
||
<div className={styles.emptyState}>
|
||
<FaUserShield className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Benutzer auswählen</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen.
|
||
</p>
|
||
</div>
|
||
) : loading ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Zugriffsübersicht...</span>
|
||
</div>
|
||
) : overview ? (
|
||
<>
|
||
{/* User Info */}
|
||
<div className={styles.infoBox} style={{ marginBottom: '1rem', flexShrink: 0 }}>
|
||
<strong>{overview.user.fullName || overview.user.username}</strong>
|
||
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
|
||
<span>{overview.user.email}</span>
|
||
{overview.isSysAdmin && (
|
||
<>
|
||
<span style={{ margin: '0 1rem', color: 'var(--text-secondary)' }}>|</span>
|
||
<span className={styles.badge} style={{ background: '#f59e0b', color: 'white' }}>
|
||
SysAdmin
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div style={{
|
||
display: 'flex',
|
||
gap: '0.5rem',
|
||
marginBottom: '1rem',
|
||
borderBottom: '1px solid var(--border-color)',
|
||
paddingBottom: '0.5rem',
|
||
flexShrink: 0
|
||
}}>
|
||
<button
|
||
className={activeTab === 'overview' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('overview')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaUserShield /> Übersicht
|
||
</button>
|
||
<button
|
||
className={activeTab === 'ui' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('ui')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaEye /> UI-Zugriff ({overview.uiAccess.length})
|
||
</button>
|
||
<button
|
||
className={activeTab === 'data' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('data')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaDatabase /> Daten-Zugriff ({overview.dataAccess.length})
|
||
</button>
|
||
<button
|
||
className={activeTab === 'resources' ? styles.primaryButton : styles.secondaryButton}
|
||
onClick={() => setActiveTab('resources')}
|
||
style={{ padding: '0.5rem 1rem' }}
|
||
>
|
||
<FaCube /> Ressourcen ({overview.resourceAccess.length})
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
<div className={styles.tableContainer}>
|
||
{activeTab === 'overview' && renderOverviewTab()}
|
||
{activeTab === 'ui' && renderUiAccessTab()}
|
||
{activeTab === 'data' && renderDataAccessTab()}
|
||
{activeTab === 'resources' && renderResourceAccessTab()}
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AdminUserAccessOverviewPage;
|