"use client" import * as React from "react" import { ColumnDef, ColumnFiltersState, ColumnSizingState, SortingState, VisibilityState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table" import { IconLayoutColumns, IconBug, IconRadar, IconChevronLeft, IconChevronRight, IconChevronsLeft, IconChevronsRight, IconSearch, IconLoader2, IconChevronDown } from "@tabler/icons-react" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" import { useAllVulnerabilities } from "@/hooks/use-vulnerabilities" import { useScans } from "@/hooks/use-scans" import { VulnerabilityDetailDialog } from "@/components/vulnerabilities/vulnerability-detail-dialog" import { createVulnerabilityColumns } from "@/components/vulnerabilities/vulnerabilities-columns" import { createScanHistoryColumns } from "@/components/scan/history/scan-history-columns" import { ScanProgressDialog, buildScanProgressData, type ScanProgressData } from "@/components/scan/scan-progress-dialog" import { getScan } from "@/services/scan.service" import { useRouter } from "next/navigation" import type { Vulnerability } from "@/types/vulnerability.types" import type { ScanRecord } from "@/types/scan.types" function formatTime(dateStr: string) { const date = new Date(dateStr) return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }) } export function DashboardDataTable() { const router = useRouter() const [activeTab, setActiveTab] = React.useState("scans") const [vulnColumnVisibility, setVulnColumnVisibility] = React.useState({}) const [scanColumnVisibility, setScanColumnVisibility] = React.useState({}) const [vulnColumnSizing, setVulnColumnSizing] = React.useState({}) const [scanColumnSizing, setScanColumnSizing] = React.useState({}) // 漏洞详情弹窗 const [selectedVuln, setSelectedVuln] = React.useState(null) const [vulnDialogOpen, setVulnDialogOpen] = React.useState(false) // 扫描进度弹窗 const [progressData, setProgressData] = React.useState(null) const [progressDialogOpen, setProgressDialogOpen] = React.useState(false) // 分页状态 const [vulnPagination, setVulnPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) const [scanPagination, setScanPagination] = React.useState({ pageIndex: 0, pageSize: 10 }) // 服务端搜索状态 const [vulnSearchQuery, setVulnSearchQuery] = React.useState("") const [scanSearchQuery, setScanSearchQuery] = React.useState("") const [localVulnSearch, setLocalVulnSearch] = React.useState("") const [localScanSearch, setLocalScanSearch] = React.useState("") const [isVulnSearching, setIsVulnSearching] = React.useState(false) const [isScanSearching, setIsScanSearching] = React.useState(false) // 获取漏洞数据 const vulnQuery = useAllVulnerabilities({ page: vulnPagination.pageIndex + 1, pageSize: vulnPagination.pageSize, search: vulnSearchQuery || undefined, }) // 获取扫描数据 const scanQuery = useScans({ page: scanPagination.pageIndex + 1, pageSize: scanPagination.pageSize, search: scanSearchQuery || undefined, }) // 当请求完成时重置搜索状态 React.useEffect(() => { if (!vulnQuery.isFetching && isVulnSearching) { setIsVulnSearching(false) } }, [vulnQuery.isFetching, isVulnSearching]) React.useEffect(() => { if (!scanQuery.isFetching && isScanSearching) { setIsScanSearching(false) } }, [scanQuery.isFetching, isScanSearching]) // 搜索处理 const handleVulnSearch = () => { setIsVulnSearching(true) setVulnSearchQuery(localVulnSearch) setVulnPagination(prev => ({ ...prev, pageIndex: 0 })) } const handleScanSearch = () => { setIsScanSearching(true) setScanSearchQuery(localScanSearch) setScanPagination(prev => ({ ...prev, pageIndex: 0 })) } const vulnerabilities = vulnQuery.data?.vulnerabilities ?? [] const scans = scanQuery.data?.results ?? [] // 格式化日期 const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleString("zh-CN", { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }) } // 点击漏洞行 const handleVulnRowClick = React.useCallback((vuln: Vulnerability) => { setSelectedVuln(vuln) setVulnDialogOpen(true) }, []) // 漏洞列定义 - 复用 vulnerabilities 页面的列 const vulnColumns = React.useMemo( () => createVulnerabilityColumns({ formatDate, handleViewDetail: handleVulnRowClick, }), [handleVulnRowClick] ) // 扫描进度查看 const handleViewProgress = React.useCallback(async (scan: ScanRecord) => { try { const fullScan = await getScan(scan.id) const data = buildScanProgressData(fullScan) setProgressData(data) setProgressDialogOpen(true) } catch (error) { console.error("获取扫描详情失败:", error) } }, []) // 扫描列定义 - 复用 scan-history 页面的列 const scanColumns = React.useMemo( () => createScanHistoryColumns({ formatDate, navigate: (path: string) => router.push(path), handleDelete: () => {}, // Dashboard 不需要删除功能 handleStop: () => {}, // Dashboard 不需要停止功能 handleViewProgress, }), [router, handleViewProgress] ) // 漏洞表格 const vulnTable = useReactTable({ data: vulnerabilities, columns: vulnColumns, getCoreRowModel: getCoreRowModel(), onColumnVisibilityChange: setVulnColumnVisibility, onColumnSizingChange: setVulnColumnSizing, enableColumnResizing: true, columnResizeMode: 'onChange', state: { columnVisibility: vulnColumnVisibility, columnSizing: vulnColumnSizing, }, manualPagination: true, pageCount: vulnQuery.data?.pagination?.totalPages ?? -1, }) // 扫描表格 const scanTable = useReactTable({ data: scans, columns: scanColumns, getCoreRowModel: getCoreRowModel(), onColumnVisibilityChange: setScanColumnVisibility, onColumnSizingChange: setScanColumnSizing, enableColumnResizing: true, columnResizeMode: 'onChange', state: { columnVisibility: scanColumnVisibility, columnSizing: scanColumnSizing, }, manualPagination: true, pageCount: scanQuery.data?.totalPages ?? -1, }) const currentTable = activeTab === "vulnerabilities" ? vulnTable : scanTable const currentLocalSearch = activeTab === "vulnerabilities" ? localVulnSearch : localScanSearch const setCurrentLocalSearch = activeTab === "vulnerabilities" ? setLocalVulnSearch : setLocalScanSearch const handleCurrentSearch = activeTab === "vulnerabilities" ? handleVulnSearch : handleScanSearch const isCurrentSearching = activeTab === "vulnerabilities" ? isVulnSearching : isScanSearching const isLoading = activeTab === "vulnerabilities" ? vulnQuery.isLoading : scanQuery.isLoading const pagination = activeTab === "vulnerabilities" ? vulnPagination : scanPagination const setPagination = activeTab === "vulnerabilities" ? setVulnPagination : setScanPagination const totalPages = activeTab === "vulnerabilities" ? (vulnQuery.data?.pagination?.totalPages ?? 1) : (scanQuery.data?.totalPages ?? 1) return ( <> {progressData && ( )} {/* Tab + 搜索框 + Columns 在同一行 */}
扫描历史 漏洞
setCurrentLocalSearch(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCurrentSearch()} className="h-8 w-[200px]" /> {currentTable .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => ( column.toggleVisibility(!!value)} > {column.id} ))}
{/* 表格内容 */} {isLoading ? (
{[...Array(5)].map((_, i) => )}
) : (
{vulnTable.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanResize() && (
header.column.resetSize()} className="absolute -right-2.5 top-0 h-full w-8 cursor-col-resize select-none touch-none bg-transparent flex justify-center" >
)} ))} ))} {vulnTable.getRowModel().rows?.length ? ( vulnTable.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( 暂无漏洞数据 )}
)}
{scanQuery.isLoading ? (
{[...Array(5)].map((_, i) => )}
) : (
{scanTable.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanResize() && (
header.column.resetSize()} className="absolute -right-2.5 top-0 h-full w-8 cursor-col-resize select-none touch-none bg-transparent flex justify-center" >
)} ))} ))} {scanTable.getRowModel().rows?.length ? ( scanTable.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( 暂无扫描记录 )}
)}
{/* 分页控制 */}
{/* 选中行信息 */}
{currentTable.getFilteredSelectedRowModel().rows.length} of{" "} {activeTab === "vulnerabilities" ? (vulnQuery.data?.pagination?.total ?? 0) : (scanQuery.data?.total ?? 0)} row(s) selected
{/* 分页控制器 */}
{/* 每页显示数量选择 */}
{/* 页码信息 */}
Page {pagination.pageIndex + 1} of {totalPages}
{/* 分页按钮 */}
) }