// 终端审计 — 会话命令子面板(筛选 / 虚拟滚动 / 导出) // Logan.Li import { useMemo, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Search, RefreshCw, Play, FileText } from 'lucide-react'; import { useSessionCommands } from '@/hooks/api'; import { terminalSessionApi, sessionCommandApi } from '@/api'; import { ReplayDialog } from '@/components/system/ReplayDialog'; import { formatCommandOutput, presetAuditRange } from './helpers'; import { isHighRiskCommand } from '@/components/system/replay/replay-utils'; import { exportCommandsListCsv } from '@/components/system/replay/export-audit'; import { unwrapList } from '@/hooks/api/unwrap'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import type { SessionCommand } from './types'; const GRID_COLS = 'minmax(150px,1fr) minmax(80px,0.8fr) minmax(200px,2fr) minmax(100px,0.8fr) minmax(80px,0.5fr) 40px'; const PAGE_SIZE = 1000; export function SessionCommandsPanel() { const { t } = useTranslation(); const [keyword, setKeyword] = useState(''); const [username, setUsername] = useState(''); const [sessionIdInput, setSessionIdInput] = useState(''); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [applied, setApplied] = useState({ keyword: '', username: '', sessionId: '', startDate: '', endDate: '', }); const [replayOpen, setReplayOpen] = useState(false); const [replayId, setReplayId] = useState(null); const [replaySeek, setReplaySeek] = useState(undefined); const [exporting, setExporting] = useState(false); const queryParams = useMemo(() => ({ page: 1, page_size: PAGE_SIZE, ...(applied.sessionId ? { session_id: Number(applied.sessionId) } : {}), ...(applied.username ? { username: applied.username } : {}), ...(applied.keyword ? { command: applied.keyword } : {}), ...(applied.startDate ? { start_date: applied.startDate } : {}), ...(applied.endDate ? { end_date: applied.endDate } : {}), }), [applied]); const { data, isLoading, refetch } = useSessionCommands(queryParams); const commands = data?.list ?? []; const total = data?.total ?? 0; const applyFilters = () => { setApplied({ keyword, username, sessionId: sessionIdInput, startDate, endDate }); }; const handleExportCsv = async () => { setExporting(true); try { const resp = await sessionCommandApi.list({ ...queryParams, page: 1, page_size: Math.min(Math.max(total, 1), 10000), }); const list = unwrapList(resp); if (list.length === 0) { toast.error(t('audit.terminal.noCommands')); return; } exportCommandsListCsv(list); } catch { toast.error(t('audit.terminal.exportFailed')); } finally { setExporting(false); } }; // 虚拟滚动 const scrollRef = useRef(null); const virtualizer = useVirtualizer({ count: commands.length, getScrollElement: () => scrollRef.current, estimateSize: () => 48, overscan: 10, }); const virtualRows = virtualizer.getVirtualItems(); const totalHeight = virtualizer.getTotalSize(); // 从命令行打开回放并跳到该命令时刻(无录制则提示) // Logan.Li const openReplayAtCommand = async (cmd: SessionCommand) => { if (!cmd.session_record_size) { toast.error(t('audit.terminal.replayPlayer.noRecord')); return; } try { const sess = await terminalSessionApi.get(cmd.session_id) as { start_time?: string }; const ts = cmd.timestamp || cmd.created_at; let seek: number | undefined; if (sess?.start_time && ts) { seek = (new Date(ts).getTime() - new Date(sess.start_time).getTime()) / 1000; if (seek < 0) seek = 0; } setReplayId(cmd.session_id); setReplaySeek(seek); setReplayOpen(true); } catch { toast.error(t('audit.terminal.replayPlayer.loadFailed')); } }; return ( <>
setKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyFilters()} className="pl-9 h-9 dg-input" />
setUsername(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyFilters()} className="w-28 h-9 dg-input" /> setSessionIdInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyFilters()} className="w-32 h-9 dg-input" /> setStartDate(e.target.value)} className="w-36 h-9 dg-input" /> — setEndDate(e.target.value)} className="w-36 h-9 dg-input" />
{isLoading ? (
) : commands.length === 0 ? (

{t('audit.terminal.noCommands')}

) : ( <>
{t('audit.terminal.col.startTime')} {t('audit.terminal.col.user')} {t('audit.terminal.col.command')} {t('audit.terminal.col.outputSize')} {t('audit.terminal.col.sessionRef')}
{virtualRows.map((vr) => { const c = commands[vr.index]; if (!c) return null; return (
{c.created_at ? new Date(c.created_at).toLocaleString() : '--'} {c.username || '--'} {c.command || '--'} {formatCommandOutput(c.output_size)} #{c.session_id}
); })}
{t('audit.terminal.totalCommands', { count: total })} {commands.length < total && ` · ${t('audit.terminal.showingLoaded', { loaded: commands.length })}`} {commands.length > 200 && ` · ${t('audit.terminal.virtualScroll')}`}
)}
); }