fiixed feature instance role access
This commit is contained in:
parent
2cbee8fe57
commit
8f29bdb270
1 changed files with 220 additions and 1 deletions
|
|
@ -10,12 +10,15 @@
|
||||||
* - Mandate-specific roles (mandateId=xyz) - editable permissions
|
* - Mandate-specific roles (mandateId=xyz) - editable permissions
|
||||||
*
|
*
|
||||||
* Each role can be expanded to show/edit its AccessRules via AccessRulesEditor.
|
* Each role can be expanded to show/edit its AccessRules via AccessRulesEditor.
|
||||||
|
*
|
||||||
|
* Includes a "Cleanup Duplicates" tool to find and remove duplicate AccessRules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { useMandateRoles, type Role } from '../../hooks/useMandateRoles';
|
import { useMandateRoles, type Role } from '../../hooks/useMandateRoles';
|
||||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||||
import { AccessRulesEditor } from '../../components/AccessRules';
|
import { AccessRulesEditor } from '../../components/AccessRules';
|
||||||
|
import api from '../../api';
|
||||||
import {
|
import {
|
||||||
FaUserShield,
|
FaUserShield,
|
||||||
FaShieldAlt,
|
FaShieldAlt,
|
||||||
|
|
@ -24,10 +27,35 @@ import {
|
||||||
FaChevronRight,
|
FaChevronRight,
|
||||||
FaGlobe,
|
FaGlobe,
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
FaFilter
|
FaFilter,
|
||||||
|
FaBroom,
|
||||||
|
FaTimes,
|
||||||
|
FaExclamationTriangle,
|
||||||
|
FaCheckCircle
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import styles from './Admin.module.css';
|
import styles from './Admin.module.css';
|
||||||
|
|
||||||
|
// Types for cleanup result
|
||||||
|
interface DuplicateGroup {
|
||||||
|
roleId: string;
|
||||||
|
context: string;
|
||||||
|
item: string;
|
||||||
|
totalCount: number;
|
||||||
|
keepId: string;
|
||||||
|
deleteCount: number;
|
||||||
|
deleteIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CleanupResult {
|
||||||
|
dryRun: boolean;
|
||||||
|
totalRules: number;
|
||||||
|
uniqueSignatures: number;
|
||||||
|
duplicateGroups: number;
|
||||||
|
duplicateRulesToDelete: number;
|
||||||
|
deletedCount: number;
|
||||||
|
details: DuplicateGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
export const AdminMandateRolePermissionsPage: React.FC = () => {
|
export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
roles,
|
roles,
|
||||||
|
|
@ -44,6 +72,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||||
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('all');
|
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('all');
|
||||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Cleanup state
|
||||||
|
const [showCleanupModal, setShowCleanupModal] = useState(false);
|
||||||
|
const [cleanupLoading, setCleanupLoading] = useState(false);
|
||||||
|
const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null);
|
||||||
|
const [cleanupError, setCleanupError] = useState<string | null>(null);
|
||||||
|
const [cleanupPhase, setCleanupPhase] = useState<'idle' | 'preview' | 'done'>('idle');
|
||||||
|
|
||||||
// Load mandates on mount
|
// Load mandates on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadMandates = async () => {
|
const loadMandates = async () => {
|
||||||
|
|
@ -105,6 +140,49 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Cleanup functions ---
|
||||||
|
const _openCleanupModal = useCallback(async () => {
|
||||||
|
setShowCleanupModal(true);
|
||||||
|
setCleanupError(null);
|
||||||
|
setCleanupResult(null);
|
||||||
|
setCleanupPhase('idle');
|
||||||
|
setCleanupLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=true');
|
||||||
|
setCleanupResult(response.data);
|
||||||
|
setCleanupPhase('preview');
|
||||||
|
} catch (err: any) {
|
||||||
|
setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Duplikate');
|
||||||
|
} finally {
|
||||||
|
setCleanupLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const _executeCleanup = useCallback(async () => {
|
||||||
|
setCleanupLoading(true);
|
||||||
|
setCleanupError(null);
|
||||||
|
try {
|
||||||
|
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=false');
|
||||||
|
setCleanupResult(response.data);
|
||||||
|
setCleanupPhase('done');
|
||||||
|
// Refresh roles after cleanup
|
||||||
|
if (selectedMandateId) {
|
||||||
|
fetchRoles(selectedMandateId, { scopeFilter });
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setCleanupError(err?.response?.data?.detail || err?.message || 'Fehler beim Bereinigen');
|
||||||
|
} finally {
|
||||||
|
setCleanupLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||||
|
|
||||||
|
const _closeCleanupModal = useCallback(() => {
|
||||||
|
setShowCleanupModal(false);
|
||||||
|
setCleanupResult(null);
|
||||||
|
setCleanupError(null);
|
||||||
|
setCleanupPhase('idle');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Filter options for scope
|
// Filter options for scope
|
||||||
const scopeOptions = useMemo(() => [
|
const scopeOptions = useMemo(() => [
|
||||||
{ value: 'all', label: 'Alle Rollen' },
|
{ value: 'all', label: 'Alle Rollen' },
|
||||||
|
|
@ -140,6 +218,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
|
<button
|
||||||
|
className={styles.secondaryButton}
|
||||||
|
onClick={_openCleanupModal}
|
||||||
|
disabled={loading}
|
||||||
|
title="Doppelte Regeln finden und bereinigen"
|
||||||
|
>
|
||||||
|
<FaBroom /> Duplikate bereinigen
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={styles.secondaryButton}
|
className={styles.secondaryButton}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
|
|
@ -256,6 +342,139 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Cleanup Duplicates Modal */}
|
||||||
|
{showCleanupModal && (
|
||||||
|
<div className={styles.modalOverlay} onClick={_closeCleanupModal}>
|
||||||
|
<div className={styles.modal} style={{ maxWidth: '750px' }} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<h3 className={styles.modalTitle}>
|
||||||
|
<FaBroom style={{ marginRight: '0.5rem' }} />
|
||||||
|
Doppelte Regeln bereinigen
|
||||||
|
</h3>
|
||||||
|
<button className={styles.modalClose} onClick={_closeCleanupModal}>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
{/* Loading */}
|
||||||
|
{cleanupLoading && (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<div className={styles.spinner} />
|
||||||
|
<span>{cleanupPhase === 'idle' ? 'Analysiere Duplikate...' : 'Bereinige Duplikate...'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{cleanupError && (
|
||||||
|
<div style={{ padding: '1rem', background: '#fed7d7', borderRadius: '6px', color: '#c53030', marginBottom: '1rem' }}>
|
||||||
|
<FaExclamationTriangle style={{ marginRight: '0.5rem' }} />
|
||||||
|
{cleanupError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{cleanupResult && !cleanupLoading && (
|
||||||
|
<>
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
|
||||||
|
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Regeln total</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Eindeutige Regeln</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Duplikat-Gruppen</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateRulesToDelete > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateRulesToDelete > 0 ? '#c53030' : '#2f855a' }}>
|
||||||
|
{cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||||||
|
{cleanupPhase === 'done' ? 'Geloescht' : 'Zu loeschen'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Message */}
|
||||||
|
{cleanupPhase === 'done' && (
|
||||||
|
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||||
|
<FaCheckCircle />
|
||||||
|
<span><strong>{cleanupResult.deletedCount}</strong> doppelte Regeln wurden erfolgreich entfernt.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
|
||||||
|
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||||
|
<FaCheckCircle />
|
||||||
|
<span>Keine Duplikate gefunden. Alles sauber!</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Details Table */}
|
||||||
|
{cleanupResult.details.length > 0 && (
|
||||||
|
<div style={{ marginTop: '0.5rem' }}>
|
||||||
|
<h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--text-secondary)' }}>
|
||||||
|
Duplikat-Details {cleanupResult.details.length < cleanupResult.duplicateGroups && `(${cleanupResult.details.length} von ${cleanupResult.duplicateGroups})`}
|
||||||
|
</h4>
|
||||||
|
<div style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
|
||||||
|
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Kontext</th>
|
||||||
|
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Item</th>
|
||||||
|
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Total</th>
|
||||||
|
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)', color: '#c53030' }}>Duplikate</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{cleanupResult.details.map((group, idx) => (
|
||||||
|
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||||
|
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||||
|
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||||
|
{group.context}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||||
|
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||||
|
{group.item}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>{group.totalCount}</td>
|
||||||
|
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center', color: '#c53030', fontWeight: 600 }}>{group.deleteCount}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter}>
|
||||||
|
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
|
||||||
|
{cleanupPhase === 'done' ? 'Schliessen' : 'Abbrechen'}
|
||||||
|
</button>
|
||||||
|
{cleanupPhase === 'preview' && cleanupResult && cleanupResult.duplicateRulesToDelete > 0 && (
|
||||||
|
<button
|
||||||
|
className={styles.dangerButton}
|
||||||
|
onClick={_executeCleanup}
|
||||||
|
disabled={cleanupLoading}
|
||||||
|
>
|
||||||
|
<FaBroom /> {cleanupResult.duplicateRulesToDelete} Duplikate loeschen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue