Files
xiaoxia-saas/apps/web/src/pages/admin/LogViewer.tsx
T
CI Test 294dea39f3 feat: V21 design system alignment for admin pages
- 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
2026-06-26 21:40:50 +08:00

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;