294dea39f3
- Create unified Admin.css with V21 design tokens - Update Dashboard with stat cards and V21 color scheme - Update Analytics with V21 chart colors and metrics - Update UserManagement with V21 table and search styles - Update LogViewer with V21 filter bar and log level tags - Update SystemMonitor with V21 progress and resource cards - Update AdminComingSoon with V21 result styling - Replace all inline styles with V21 CSS classes - Apply Indigo/Green/Amber/Purple V21 color palette
346 lines
9.5 KiB
TypeScript
346 lines
9.5 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';
|
|
import './Admin.css';
|
|
|
|
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: 'ObjectStorage',
|
|
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 getLevelTagClass = (level: string) => {
|
|
const classes: Record<string, string> = {
|
|
INFO: 'xx-tag info',
|
|
WARN: 'xx-tag warning',
|
|
ERROR: 'xx-tag error',
|
|
DEBUG: 'xx-tag debug',
|
|
};
|
|
return classes[level] || 'xx-tag debug';
|
|
};
|
|
|
|
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 className={getLevelTagClass(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: 'ObjectStorage', value: 'ObjectStorage' },
|
|
{ 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 className="log-viewer-page">
|
|
<div className="xx-page-head-simple">
|
|
<h2>日志查看器</h2>
|
|
<p>系统日志查询与监控</p>
|
|
</div>
|
|
|
|
<div className="xx-filter-bar">
|
|
<Search
|
|
placeholder="搜索日志内容..."
|
|
allowClear
|
|
enterButton={<SearchOutlined />}
|
|
className="xx-search-input"
|
|
style={{ width: 280 }}
|
|
/>
|
|
|
|
<Select
|
|
placeholder="日志级别"
|
|
className="xx-select"
|
|
style={{ width: 130 }}
|
|
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="服务"
|
|
className="xx-select"
|
|
style={{ width: 140 }}
|
|
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="ObjectStorage">Object Storage</Option>
|
|
<Option value="Redis">Redis</Option>
|
|
</Select>
|
|
|
|
<RangePicker showTime className="xx-date-picker" />
|
|
|
|
<Button icon={<FilterOutlined />} disabled className="xx-ghost-btn">
|
|
应用过滤暂未开放
|
|
</Button>
|
|
<Button icon={<DownloadOutlined />} disabled className="xx-ghost-btn">
|
|
导出日志暂未开放
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="xx-table-wrapper">
|
|
<Table
|
|
columns={columns}
|
|
dataSource={logs}
|
|
rowKey="id"
|
|
pagination={{
|
|
total: logs.length,
|
|
pageSize: 10,
|
|
showSizeChanger: true,
|
|
showTotal: (total) => `共 ${total} 条日志`,
|
|
}}
|
|
scroll={{ x: 1200 }}
|
|
/>
|
|
</div>
|
|
|
|
{/* 日志详情抽屉 */}
|
|
<Drawer
|
|
title="日志详情"
|
|
placement="right"
|
|
width={600}
|
|
open={drawerVisible}
|
|
onClose={() => setDrawerVisible(false)}
|
|
className="xx-drawer"
|
|
>
|
|
{selectedLog && (
|
|
<div>
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">时间</div>
|
|
<div className="xx-log-detail-value">{selectedLog.timestamp}</div>
|
|
</div>
|
|
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">级别</div>
|
|
<div className="xx-log-detail-value">
|
|
<Tag className={getLevelTagClass(selectedLog.level)}>{selectedLog.level}</Tag>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">服务</div>
|
|
<div className="xx-log-detail-value">{selectedLog.service}</div>
|
|
</div>
|
|
|
|
{selectedLog.user && (
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">用户</div>
|
|
<div className="xx-log-detail-value">{selectedLog.user}</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">操作</div>
|
|
<div className="xx-log-detail-value">{selectedLog.action}</div>
|
|
</div>
|
|
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">详细信息</div>
|
|
<div className="xx-log-detail-code">{selectedLog.details}</div>
|
|
</div>
|
|
|
|
{selectedLog.ip && (
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">IP 地址</div>
|
|
<div className="xx-log-detail-value">{selectedLog.ip}</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedLog.request_id && (
|
|
<div className="xx-log-detail">
|
|
<div className="xx-log-detail-label">Request ID</div>
|
|
<div className="xx-log-detail-code">{selectedLog.request_id}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Drawer>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LogViewer;
|
|
|
|
export const Component = LogViewer;
|