log admin
This commit is contained in:
parent
9793360235
commit
32c071911a
5 changed files with 323 additions and 2 deletions
|
|
@ -37,7 +37,7 @@ import { DashboardPage } from './pages/Dashboard';
|
|||
import { SettingsPage } from './pages/Settings';
|
||||
import { GDPRPage } from './pages/GDPR';
|
||||
import { FeatureViewPage } from './pages/FeatureView';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage } from './pages/admin';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage } from './pages/admin';
|
||||
import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
|
||||
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||
import { BillingDataView, BillingAdmin } from './pages/billing';
|
||||
|
|
@ -187,6 +187,7 @@ function App() {
|
|||
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
|
||||
<Route path="billing" element={<BillingAdmin />} />
|
||||
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
|
||||
<Route path="logs" element={<AdminLogsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.admin.billing': <FaMoneyBillAlt />,
|
||||
'page.admin.automationEvents': <FaClock />,
|
||||
'page.admin.automation-events': <FaClock />,
|
||||
'page.admin.logs': <FaFileAlt />,
|
||||
|
||||
// Feature pages - Trustee
|
||||
'page.feature.trustee.dashboard': <FaChartLine />,
|
||||
|
|
|
|||
106
src/pages/admin/AdminLogsPage.module.css
Normal file
106
src/pages/admin/AdminLogsPage.module.css
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* AdminLogsPage Styles
|
||||
*
|
||||
* Log viewer specific styles: monospace container, color coding, controls.
|
||||
*/
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.loadGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.controlLabel {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.countInput {
|
||||
width: 100px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.countInput:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #f25843);
|
||||
box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1);
|
||||
}
|
||||
|
||||
.refreshGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggleLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggleLabel input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary-color, #f25843);
|
||||
}
|
||||
|
||||
.logContainer {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
background: var(--bg-tertiary, #1e1e1e);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .logContainer {
|
||||
background: #0d1117;
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
:global(:not(.dark-theme)) .logContainer {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.logLine {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: 1px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logLine:hover {
|
||||
background: var(--bg-secondary, rgba(255, 255, 255, 0.04));
|
||||
}
|
||||
|
||||
.logDirHint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary);
|
||||
font-family: monospace;
|
||||
}
|
||||
212
src/pages/admin/AdminLogsPage.tsx
Normal file
212
src/pages/admin/AdminLogsPage.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* AdminLogsPage
|
||||
*
|
||||
* SysAdmin-only page for viewing gateway application logs.
|
||||
* Color-coded log levels (ERROR, WARNING, INFO, DEBUG).
|
||||
* Auto-refresh with configurable entry count.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { FaSync, FaDownload } from 'react-icons/fa';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
import logStyles from './AdminLogsPage.module.css';
|
||||
|
||||
const LOG_LEVEL_COLORS: Record<string, string> = {
|
||||
ERROR: 'var(--log-error, #e53e3e)',
|
||||
WARNING: 'var(--log-warning, #d69e2e)',
|
||||
INFO: 'var(--log-info, #3182ce)',
|
||||
DEBUG: 'var(--log-debug, #718096)',
|
||||
};
|
||||
|
||||
const AUTO_REFRESH_INTERVAL = 5000;
|
||||
|
||||
function _parseLogLevel(line: string): string | null {
|
||||
const match = line.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} - (\w+) -/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const AdminLogsPage: React.FC = () => {
|
||||
const [lines, setLines] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [count, setCount] = useState(200);
|
||||
const [countInput, setCountInput] = useState('200');
|
||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||
const [logDir, setLogDir] = useState<string>('');
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
const autoScrollRef = useRef(true);
|
||||
|
||||
const _fetchLogs = useCallback(async (entryCount?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const n = entryCount ?? count;
|
||||
const response = await api.get(`/api/admin/logs?count=${n}`);
|
||||
setLines(response.data.lines || []);
|
||||
setLogDir(response.data.logDir || '');
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Fehler beim Laden der Logs');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
const _handleLoad = () => {
|
||||
const parsed = parseInt(countInput, 10);
|
||||
if (isNaN(parsed) || parsed < 1) return;
|
||||
setCount(parsed);
|
||||
_fetchLogs(parsed);
|
||||
};
|
||||
|
||||
const _handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') _handleLoad();
|
||||
};
|
||||
|
||||
const _handleDownload = async () => {
|
||||
try {
|
||||
const response = await api.get(`/api/admin/logs/download?count=${count}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const blob = new Blob([response.data], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `gateway_log_${new Date().toISOString().replace(/[:.]/g, '-')}.log`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Fehler beim Download');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
const interval = setInterval(() => _fetchLogs(), AUTO_REFRESH_INTERVAL);
|
||||
return () => clearInterval(interval);
|
||||
}, [autoRefresh, _fetchLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScrollRef.current && logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [lines]);
|
||||
|
||||
const _handleScroll = () => {
|
||||
if (!logContainerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
|
||||
autoScrollRef.current = scrollHeight - scrollTop - clientHeight < 40;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Gateway Logs</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
{lines.length > 0
|
||||
? `${lines.length} Einträge`
|
||||
: 'Keine Logs geladen'}
|
||||
{logDir && <span className={logStyles.logDirHint}> — {logDir}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={_handleDownload}
|
||||
disabled={lines.length === 0}
|
||||
title="Log herunterladen"
|
||||
>
|
||||
<FaDownload /> Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={logStyles.controls}>
|
||||
<div className={logStyles.loadGroup}>
|
||||
<label className={logStyles.controlLabel}>Letzte</label>
|
||||
<input
|
||||
type="number"
|
||||
className={logStyles.countInput}
|
||||
value={countInput}
|
||||
onChange={(e) => setCountInput(e.target.value)}
|
||||
onKeyDown={_handleKeyDown}
|
||||
min={1}
|
||||
max={50000}
|
||||
/>
|
||||
<label className={logStyles.controlLabel}>Einträge</label>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={_handleLoad}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Laden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={logStyles.refreshGroup}>
|
||||
<label className={logStyles.toggleLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
/>
|
||||
Auto-Refresh (5s)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className={styles.infoBox}
|
||||
style={{
|
||||
background: 'var(--warning-bg, #fffbeb)',
|
||||
borderColor: 'var(--warning-color, #d69e2e)',
|
||||
}}
|
||||
>
|
||||
<span style={{ marginRight: 8, color: 'var(--warning-color, #d69e2e)' }}>!</span>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={logContainerRef}
|
||||
className={logStyles.logContainer}
|
||||
onScroll={_handleScroll}
|
||||
>
|
||||
{lines.length === 0 && !loading && (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>📋</div>
|
||||
<p className={styles.emptyTitle}>Keine Logs geladen</p>
|
||||
<p className={styles.emptyDescription}>
|
||||
Gib die gewünschte Anzahl Einträge ein und klicke auf "Laden".
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{loading && lines.length === 0 && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Logs werden geladen...</span>
|
||||
</div>
|
||||
)}
|
||||
{lines.map((line, idx) => {
|
||||
const level = _parseLogLevel(line);
|
||||
const color = level ? LOG_LEVEL_COLORS[level] : undefined;
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={logStyles.logLine}
|
||||
style={color ? { color } : undefined}
|
||||
>
|
||||
{line}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLogsPage;
|
||||
|
|
@ -15,4 +15,5 @@ export { AdminFeatureRolesPage } from './AdminFeatureRolesPage';
|
|||
export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
|
||||
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
|
||||
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
|
||||
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
|
||||
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
|
||||
export { AdminLogsPage } from './AdminLogsPage';
|
||||
Loading…
Reference in a new issue