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 { SettingsPage } from './pages/Settings';
|
||||||
import { GDPRPage } from './pages/GDPR';
|
import { GDPRPage } from './pages/GDPR';
|
||||||
import { FeatureViewPage } from './pages/FeatureView';
|
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 { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
|
||||||
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||||
import { BillingDataView, BillingAdmin } from './pages/billing';
|
import { BillingDataView, BillingAdmin } from './pages/billing';
|
||||||
|
|
@ -187,6 +187,7 @@ function App() {
|
||||||
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
|
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
|
||||||
<Route path="billing" element={<BillingAdmin />} />
|
<Route path="billing" element={<BillingAdmin />} />
|
||||||
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
|
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
|
||||||
|
<Route path="logs" element={<AdminLogsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
||||||
'page.admin.billing': <FaMoneyBillAlt />,
|
'page.admin.billing': <FaMoneyBillAlt />,
|
||||||
'page.admin.automationEvents': <FaClock />,
|
'page.admin.automationEvents': <FaClock />,
|
||||||
'page.admin.automation-events': <FaClock />,
|
'page.admin.automation-events': <FaClock />,
|
||||||
|
'page.admin.logs': <FaFileAlt />,
|
||||||
|
|
||||||
// Feature pages - Trustee
|
// Feature pages - Trustee
|
||||||
'page.feature.trustee.dashboard': <FaChartLine />,
|
'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 { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
|
||||||
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
|
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
|
||||||
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
|
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
|
||||||
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
|
export { AdminAutomationEventsPage } from './AdminAutomationEventsPage';
|
||||||
|
export { AdminLogsPage } from './AdminLogsPage';
|
||||||
Loading…
Reference in a new issue