// 终端审计 — 历史会话子面板(筛选 / 分页 / 回放 / 导出,只读无删除) // Logan.Li import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { cn } from '@/lib/utils'; import { useTranslation } from 'react-i18next'; 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, FileText, ChevronLeft, ChevronRight } from 'lucide-react'; import { terminalSessionApi, sessionCommandApi } from '@/api'; import { rdpSessionApi } from '@/api'; import { ReplayDialog } from '@/components/system/ReplayDialog'; import { RdpAuditViewer } from '@/components/system/RdpAuditViewer'; import { useTerminalSessions } from '@/hooks/api'; import { unwrapList, unwrapTotal } from '@/hooks/api/unwrap'; import { toast } from 'sonner'; import { formatDuration, formatSize, getStatusBadge, presetAuditRange } from './helpers'; import { buildCommandBookmarks } from '@/components/system/replay/replay-utils'; import { exportAuditReportCsv } from '@/components/system/replay/export-audit'; import type { ReplaySessionMeta } from '@/components/system/replay/replay-utils'; import type { TerminalSession } from './types'; const PAGE_SIZE = 20; export function HistorySessionsPanel() { const { t } = useTranslation(); const [page, setPage] = useState(1); const [rdpPage, setRdpPage] = useState(1); const [keyword, setKeyword] = useState(''); const [username, setUsername] = useState(''); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [hasRecordOnly, setHasRecordOnly] = useState(false); const [applied, setApplied] = useState({ keyword: '', username: '', startDate: '', endDate: '', hasRecordOnly: false, }); const queryParams = useMemo(() => ({ page, page_size: PAGE_SIZE, status: 'closed', ...(applied.keyword ? { keyword: applied.keyword } : {}), ...(applied.username ? { username: applied.username } : {}), ...(applied.startDate ? { start_date: applied.startDate } : {}), ...(applied.endDate ? { end_date: applied.endDate } : {}), ...(applied.hasRecordOnly ? { has_record: '1' } : {}), }), [page, applied]); const rdpQueryParams = useMemo(() => ({ ...queryParams, page: rdpPage, }), [queryParams, rdpPage]); const { data, isLoading, isFetching, refetch } = useTerminalSessions(queryParams); const rdpQuery = useQuery({ queryKey: ['rdp-sessions', 'history', rdpQueryParams], queryFn: async () => { const resp = await rdpSessionApi.list(rdpQueryParams); return { list: unwrapList(resp), total: unwrapTotal(resp), }; }, }); const sessions = data?.list ?? []; const total = data?.total ?? 0; const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const rdpSessions = rdpQuery.data?.list ?? []; const rdpTotal = rdpQuery.data?.total ?? 0; const rdpTotalPages = Math.max(1, Math.ceil(rdpTotal / PAGE_SIZE)); const [replayId, setReplayId] = useState(null); const [replayOpen, setReplayOpen] = useState(false); const [rdpReplayId, setRdpReplayId] = useState(null); const [rdpReplayOpen, setRdpReplayOpen] = useState(false); const applyFilters = () => { setApplied({ keyword, username, startDate, endDate, hasRecordOnly }); setPage(1); setRdpPage(1); }; const openReplay = (id: number) => { setReplayId(id); setReplayOpen(true); }; const openRdpReplay = (id: number) => { setRdpReplayId(id); setRdpReplayOpen(true); }; const handleDownload = (id: number) => { terminalSessionApi.download(id).catch(() => toast.error(t('audit.terminal.exportFailed'))); }; // 导出审计报告:命令列表失败时仍导出会话元信息 // Logan.Li const handleExportReport = async (id: number) => { try { const sess = await terminalSessionApi.get(id); const meta = sess as ReplaySessionMeta; let cmdList: { id: number; command: string; timestamp?: string; created_at?: string }[] = []; try { const cmds = await sessionCommandApi.list({ session_id: id, page: 1, page_size: 500 }); cmdList = unwrapList(cmds); } catch { /* 无命令表数据时仅导出会话信息 */ } const duration = meta.duration || 0; const bookmarks = buildCommandBookmarks(cmdList, meta.start_time, duration); exportAuditReportCsv({ ...meta, id }, bookmarks); toast.success(t('audit.terminal.exportReport')); } catch { toast.error(t('audit.terminal.exportFailed')); } }; 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-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 ? (
) : sessions.length === 0 ? (

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

) : ( <> {t('audit.terminal.col.id')} {t('audit.terminal.col.user')} {t('audit.terminal.col.targetServer')} {t('audit.terminal.col.startTime')} {t('audit.terminal.col.duration')} {t('audit.terminal.col.recordSize')} {t('audit.terminal.col.status')} {t('audit.terminal.col.actions')} {sessions.map((s) => ( {s.id} {s.username || '--'}
{s.server_name || '--'} {s.server_ip}
{s.start_time ? new Date(s.start_time).toLocaleString() : '--'} {formatDuration(s.duration)} {s.record_size > 0 ? formatSize(s.record_size) : '--'} {getStatusBadge(s.status)}
))}
{t('audit.terminal.totalSessions', { count: total })}
{page} / {totalPages}
)}
RDP 历史会话
{rdpQuery.isLoading ? (
) : rdpSessions.length === 0 ? (

{t("common.noRdpHistory")}

) : ( <> {t('audit.terminal.col.id')} {t('audit.terminal.col.user')} {t('audit.terminal.col.targetServer')} {t('audit.terminal.col.startTime')} {t('audit.terminal.col.duration')} {t('audit.terminal.col.recordSize')} {t('audit.terminal.col.status')} {t('audit.terminal.col.actions')} {rdpSessions.map((s) => ( {s.id} {s.username || '--'}
{s.server_name || '--'} {s.server_ip}
{s.start_time ? new Date(s.start_time).toLocaleString() : '--'} {formatDuration(s.duration)} {s.record_size > 0 ? formatSize(s.record_size) : '--'} {getStatusBadge(s.status)}
))}
RDP 会话共 {rdpTotal} 条
{rdpPage} / {rdpTotalPages}
)} ); }