Files
aiops/frontend/src/pages/terminal-audit/HistorySessionsPanel.tsx
T
Logan.Li f45e203e21 refactor(frontend): P1-P2 架构统一 — utils/hooks/api 模块化
P1:
- utils/storage.ts、theme.ts、validate.ts + index 统一出口
- request/store/useTheme 接入 storage;清零 confirm/alert
- PipelineManager、TerminalAuditTab 拆分子模块(<700行红线)

P2:
- api/monitor、api/deploy 子目录化
- hooks/api 扩展:servers/pipelines/alerts/audit/sessionCommands
- PipelineManager、AuditOperationsTab 接入 TanStack Query
2026-06-26 00:18:14 +08:00

170 lines
6.9 KiB
TypeScript

// 终端审计 — 历史会话子面板 // Logan.Li
import { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search, RefreshCw, Play, Download, Monitor } from 'lucide-react';
import { terminalSessionApi } from '@/api';
import { ReplayDialog } from '@/components/system/ReplayDialog';
import { useTerminalSessions } from '@/hooks/api';
import { formatDuration, getStatusBadge } from './helpers';
export function HistorySessionsPanel() {
const { data: sessions = [], isLoading, refetch } = useTerminalSessions({
page: 1,
page_size: 100,
status: 'closed',
});
const [keyword, setKeyword] = useState('');
const [replayId, setReplayId] = useState<number | null>(null);
const [replayOpen, setReplayOpen] = useState(false);
const filtered = sessions.filter((s) => {
const kw = keyword.toLowerCase();
return (
!kw ||
(s.username || '').toLowerCase().includes(kw) ||
(s.server_name || '').toLowerCase().includes(kw) ||
(s.server_ip || '').toLowerCase().includes(kw)
);
});
const openReplay = (id: number) => {
setReplayId(id);
setReplayOpen(true);
};
const handleDownload = async (id: number) => {
try {
await terminalSessionApi.download(id);
} catch {
// download error is handled in http.download
}
};
return (
<>
<div className="flex items-center gap-3 flex-wrap">
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索用户/服务器/IP..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
</div>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
onClick={() => refetch()}
>
<RefreshCw className="h-4 w-4 mr-2" />
刷新
</Button>
</div>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="border-t border-slate-800">
{isLoading ? (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<Monitor className="h-8 w-8 mb-3 opacity-40" />
<p className="text-sm">暂无历史会话</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">ID</TableHead>
<TableHead className="text-slate-400">用户</TableHead>
<TableHead className="text-slate-400">目标服务器</TableHead>
<TableHead className="text-slate-400">端口</TableHead>
<TableHead className="text-slate-400">开始时间</TableHead>
<TableHead className="text-slate-400">结束时间</TableHead>
<TableHead className="text-slate-400">时长</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => (
<TableRow key={s.id} className="border-slate-800 hover:bg-slate-800/50">
<TableCell>
<span className="text-sm text-slate-400">{s.id}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-200">{s.username || '--'}</span>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm text-slate-200">{s.server_name || '--'}</span>
<span className="text-xs text-slate-500">{s.server_ip}</span>
</div>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">{s.server_port}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
{s.start_time ? new Date(s.start_time).toLocaleString() : '--'}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
{s.end_time ? new Date(s.end_time).toLocaleString() : '--'}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-300">{formatDuration(s.duration)}</span>
</TableCell>
<TableCell>{getStatusBadge(s.status)}</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800"
onClick={() => openReplay(s.id)}
title="回放"
>
<Play className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-green-400 hover:bg-slate-800"
onClick={() => handleDownload(s.id)}
title="下载录制"
>
<Download className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</CardContent>
</Card>
<ReplayDialog open={replayOpen} onOpenChange={setReplayOpen} sessionId={replayId} />
</>
);
}