ebcd2ceb63
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
349 lines
9.3 KiB
TypeScript
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;
|