Files
xiaoxia-saas/apps/web/src/pages/admin/LogViewer.tsx
T
Xiaoxia AI ebcd2ceb63
Deploy / Deploy Staging (push) Failing after 7s
Deploy / Deploy Production (push) Has been skipped
Tests / test (push) Failing after 21s
Tests / lint (push) Failing after 20s
Phase 6: Complete admin pages (Analytics, Monitor, Logs) and Phase 7: Asset management foundation
Phase 6 completion:
- Add Analytics page with charts (user growth, revenue, retention)
- Add SystemMonitor page (CPU, memory, services health)
- Add LogViewer page (log search, filtering, details)
- Update sidebar navigation with Admin submenu
- Add recharts to package.json for data visualization

Phase 7 Day 1-2:
- Create Asset and AssetLibrary domain entities
- Define AssetRepository and AssetLibraryRepository interfaces
- Implement InMemory repositories for testing
- Implement PostgreSQL repository adapter
- Update SQLAlchemy models for assets and asset_libraries
- Add database migration script 004_asset_management.sql

Architecture: Strict Clean Architecture compliance
Testing: InMemory adapters ready for unit tests
Database: Migration script with indexes and foreign keys
2026-06-17 17:41:48 +08:00

349 lines
9.3 KiB
TypeScript

/**
* 日志查看器
*/
import React, { useState } from 'react';
import { Card, Table, Tag, Input, Select, DatePicker, Space, Button, Drawer } from 'antd';
import {
SearchOutlined,
FilterOutlined,
DownloadOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Search } = Input;
const { RangePicker } = DatePicker;
const { Option } = Select;
interface LogEntry {
id: string;
timestamp: string;
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
service: string;
user?: string;
action: string;
details: string;
ip?: string;
request_id?: string;
}
const LogViewer: React.FC = () => {
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
const [drawerVisible, setDrawerVisible] = useState(false);
const [filterLevel, setFilterLevel] = useState<string>('all');
const [filterService, setFilterService] = useState<string>('all');
// 模拟日志数据
const logs: LogEntry[] = [
{
id: '1',
timestamp: '2024-06-17 17:15:32',
level: 'INFO',
service: 'API',
user: 'user@example.com',
action: 'User Login',
details: 'User successfully logged in',
ip: '192.168.1.100',
request_id: 'req_abc123',
},
{
id: '2',
timestamp: '2024-06-17 17:14:28',
level: 'WARN',
service: 'API',
user: 'admin@example.com',
action: 'Failed Login Attempt',
details: 'Invalid password provided (attempt 2/5)',
ip: '192.168.1.101',
request_id: 'req_def456',
},
{
id: '3',
timestamp: '2024-06-17 17:12:45',
level: 'ERROR',
service: 'Database',
action: 'Connection Timeout',
details: 'PostgreSQL connection pool exhausted, timeout after 5s',
request_id: 'req_ghi789',
},
{
id: '4',
timestamp: '2024-06-17 17:10:15',
level: 'INFO',
service: 'Celery',
action: 'Task Completed',
details: 'Video processing task completed successfully',
request_id: 'task_jkl012',
},
{
id: '5',
timestamp: '2024-06-17 17:08:52',
level: 'WARN',
service: 'MinIO',
action: 'Slow Upload',
details: 'File upload took 3.2s (>1s threshold)',
ip: '192.168.1.102',
request_id: 'req_mno345',
},
{
id: '6',
timestamp: '2024-06-17 17:05:33',
level: 'ERROR',
service: 'API',
user: 'user2@example.com',
action: 'Permission Denied',
details: 'User attempted to access admin endpoint without permission',
ip: '192.168.1.103',
request_id: 'req_pqr678',
},
{
id: '7',
timestamp: '2024-06-17 17:03:21',
level: 'INFO',
service: 'API',
user: 'user3@example.com',
action: 'Workspace Created',
details: 'New workspace "My Project" created',
ip: '192.168.1.104',
request_id: 'req_stu901',
},
{
id: '8',
timestamp: '2024-06-17 17:01:08',
level: 'DEBUG',
service: 'Redis',
action: 'Cache Miss',
details: 'Cache key "workspace:123" not found, fetching from DB',
request_id: 'req_vwx234',
},
];
const getLevelColor = (level: string) => {
const colors: Record<string, string> = {
INFO: 'blue',
WARN: 'orange',
ERROR: 'red',
DEBUG: 'default',
};
return colors[level] || 'default';
};
const handleViewDetails = (log: LogEntry) => {
setSelectedLog(log);
setDrawerVisible(true);
};
const columns: ColumnsType<LogEntry> = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
width: 180,
sorter: (a, b) => a.timestamp.localeCompare(b.timestamp),
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
width: 100,
render: (level: string) => <Tag color={getLevelColor(level)}>{level}</Tag>,
filters: [
{ text: 'INFO', value: 'INFO' },
{ text: 'WARN', value: 'WARN' },
{ text: 'ERROR', value: 'ERROR' },
{ text: 'DEBUG', value: 'DEBUG' },
],
onFilter: (value, record) => record.level === value,
},
{
title: '服务',
dataIndex: 'service',
key: 'service',
width: 120,
filters: [
{ text: 'API', value: 'API' },
{ text: 'Database', value: 'Database' },
{ text: 'Celery', value: 'Celery' },
{ text: 'MinIO', value: 'MinIO' },
{ text: 'Redis', value: 'Redis' },
],
onFilter: (value, record) => record.service === value,
},
{
title: '用户',
dataIndex: 'user',
key: 'user',
width: 180,
render: (user?: string) => user || '-',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 200,
},
{
title: '详情',
dataIndex: 'details',
key: 'details',
ellipsis: true,
},
{
title: '操作',
key: 'actions',
width: 100,
render: (_, record) => (
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => handleViewDetails(record)}
>
查看
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<h1>日志查看器</h1>
<Card style={{ marginBottom: 16 }}>
<Space size="middle" wrap>
<Search
placeholder="搜索日志内容..."
allowClear
enterButton={<SearchOutlined />}
style={{ width: 300 }}
/>
<Select
placeholder="日志级别"
style={{ width: 120 }}
value={filterLevel}
onChange={setFilterLevel}
>
<Option value="all">全部级别</Option>
<Option value="INFO">INFO</Option>
<Option value="WARN">WARN</Option>
<Option value="ERROR">ERROR</Option>
<Option value="DEBUG">DEBUG</Option>
</Select>
<Select
placeholder="服务"
style={{ width: 120 }}
value={filterService}
onChange={setFilterService}
>
<Option value="all">全部服务</Option>
<Option value="API">API</Option>
<Option value="Database">Database</Option>
<Option value="Celery">Celery</Option>
<Option value="MinIO">MinIO</Option>
<Option value="Redis">Redis</Option>
</Select>
<RangePicker showTime />
<Button icon={<FilterOutlined />}>应用过滤</Button>
<Button icon={<DownloadOutlined />}>导出日志</Button>
</Space>
</Card>
<Card>
<Table
columns={columns}
dataSource={logs}
rowKey="id"
pagination={{
total: logs.length,
pageSize: 10,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条日志`,
}}
scroll={{ x: 1200 }}
/>
</Card>
{/* 日志详情抽屉 */}
<Drawer
title="日志详情"
placement="right"
width={600}
open={drawerVisible}
onClose={() => setDrawerVisible(false)}
>
{selectedLog && (
<div>
<div style={{ marginBottom: 16 }}>
<strong>时间:</strong>
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.timestamp}</div>
</div>
<div style={{ marginBottom: 16 }}>
<strong>级别:</strong>
<div style={{ marginTop: 4 }}>
<Tag color={getLevelColor(selectedLog.level)}>{selectedLog.level}</Tag>
</div>
</div>
<div style={{ marginBottom: 16 }}>
<strong>服务:</strong>
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.service}</div>
</div>
{selectedLog.user && (
<div style={{ marginBottom: 16 }}>
<strong>用户:</strong>
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.user}</div>
</div>
)}
<div style={{ marginBottom: 16 }}>
<strong>操作:</strong>
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.action}</div>
</div>
<div style={{ marginBottom: 16 }}>
<strong>详细信息:</strong>
<div
style={{
marginTop: 4,
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
color: '#595959',
whiteSpace: 'pre-wrap',
}}
>
{selectedLog.details}
</div>
</div>
{selectedLog.ip && (
<div style={{ marginBottom: 16 }}>
<strong>IP 地址:</strong>
<div style={{ marginTop: 4, color: '#595959' }}>{selectedLog.ip}</div>
</div>
)}
{selectedLog.request_id && (
<div style={{ marginBottom: 16 }}>
<strong>Request ID:</strong>
<div style={{ marginTop: 4, fontFamily: 'monospace', color: '#595959' }}>
{selectedLog.request_id}
</div>
</div>
)}
</div>
)}
</Drawer>
</div>
);
};
export default LogViewer;
export const Component = LogViewer;