Files
xingrin/frontend/components/endpoints/endpoints-detail-view.tsx
yyhuni 6885fa8715 Remove unused action handlers and imports from endpoint and IP address components
- Remove navigate callback and handleDelete prop from endpoints columns and detail view
- Remove unused EndpointRowActions component and MoreHorizontal, Eye, Trash2 imports from endpoints columns
- Remove unused handleDeleteEndpoint and navigate callbacks from endpoints detail view
- Remove unused DropdownMenu imports and MoreHorizontal, Trash2 imports from IP addresses columns
- Remove unused onViewDetail prop from directories
2025-12-12 22:15:29 +08:00

263 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import React, { useState, useMemo } from "react"
import { AlertTriangle } from "lucide-react"
import { useTargetEndpoints } from "@/hooks/use-targets"
import { useDeleteEndpoint, useScanEndpoints } from "@/hooks/use-endpoints"
import { EndpointsDataTable } from "./endpoints-data-table"
import { createEndpointColumns } from "./endpoints-columns"
import { LoadingSpinner } from "@/components/loading-spinner"
import { DataTableSkeleton } from "@/components/ui/data-table-skeleton"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import type { Endpoint } from "@/types/endpoint.types"
import { EndpointService } from "@/services/endpoint.service"
import { toast } from "sonner"
/**
* 目标端点详情视图组件
* 用于显示和管理目标下的端点列表
*/
export function EndpointsDetailView({
targetId,
scanId,
}: {
targetId?: number
scanId?: number
}) {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [endpointToDelete, setEndpointToDelete] = useState<Endpoint | null>(null)
const [selectedEndpoints, setSelectedEndpoints] = useState<Endpoint[]>([])
// 分页状态管理
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10
})
const [searchQuery, setSearchQuery] = useState("")
const [isSearching, setIsSearching] = useState(false)
const handleSearchChange = (value: string) => {
setIsSearching(true)
setSearchQuery(value)
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}
// 删除相关 hooks
const deleteEndpoint = useDeleteEndpoint()
// 使用 React Query 获取端点数据:优先按 targetId其次按 scanId历史快照
const targetEndpointsQuery = useTargetEndpoints(targetId || 0, {
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
search: searchQuery || undefined,
}, { enabled: !!targetId })
const scanEndpointsQuery = useScanEndpoints(scanId || 0, {
page: pagination.pageIndex + 1,
pageSize: pagination.pageSize,
search: searchQuery || undefined,
}, { enabled: !!scanId })
const {
data,
isLoading,
isFetching,
error,
refetch,
} = targetId ? targetEndpointsQuery : scanEndpointsQuery
React.useEffect(() => {
if (!isFetching && isSearching) {
setIsSearching(false)
}
}, [isFetching, isSearching])
// 辅助函数 - 格式化日期
const formatDate = React.useCallback((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 confirmDelete = async () => {
if (!endpointToDelete) return
setDeleteDialogOpen(false)
setEndpointToDelete(null)
deleteEndpoint.mutate(endpointToDelete.id)
}
// 处理分页变化
const handlePaginationChange = (newPagination: { pageIndex: number; pageSize: number }) => {
setPagination(newPagination)
}
const handleSelectionChange = React.useCallback((selectedRows: Endpoint[]) => {
setSelectedEndpoints(selectedRows)
}, [])
// 创建列定义
const endpointColumns = useMemo(
() =>
createEndpointColumns({
formatDate,
}),
[formatDate]
)
// 下载所有端点 URL
const handleDownloadAll = async () => {
try {
let blob: Blob | null = null
if (scanId) {
const data = await EndpointService.exportEndpointsByScanId(scanId)
blob = data
} else if (targetId) {
const data = await EndpointService.exportEndpointsByTargetId(targetId)
blob = data
} else {
const endpoints: Endpoint[] = (data as any)?.endpoints || []
if (!endpoints || endpoints.length === 0) {
return
}
const content = endpoints.map((item) => item.url).join("\n")
blob = new Blob([content], { type: "text/plain;charset=utf-8" })
}
if (!blob) return
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "endpoints"
a.href = url
a.download = `${prefix}-endpoints-${Date.now()}.txt`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (error) {
console.error("下载端点列表失败", error)
toast.error("下载端点列表失败,请稍后重试")
}
}
// 下载选中的端点 URL
const handleDownloadSelected = () => {
if (selectedEndpoints.length === 0) {
return
}
const content = selectedEndpoints.map((item) => item.url).join("\n")
const blob = new Blob([content], { type: "text/plain;charset=utf-8" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
const prefix = scanId ? `scan-${scanId}` : targetId ? `target-${targetId}` : "endpoints"
a.href = url
a.download = `${prefix}-endpoints-selected-${Date.now()}.txt`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// 错误状态
if (error) {
return (
<div className="flex flex-col items-center justify-center py-12">
<div className="rounded-full bg-destructive/10 p-3 mb-4">
<AlertTriangle className="h-10 w-10 text-destructive" />
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-muted-foreground text-center mb-4">
{error.message || "加载端点数据时出现错误,请重试"}
</p>
<button
onClick={() => refetch()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
</button>
</div>
)
}
// 加载状态(仅首次加载时显示骨架屏)
if (isLoading && !data) {
return (
<DataTableSkeleton
toolbarButtonCount={2}
rows={6}
columns={5}
/>
)
}
return (
<>
<EndpointsDataTable
data={data?.endpoints || []}
columns={endpointColumns}
searchPlaceholder="搜索主机名..."
searchValue={searchQuery}
onSearch={handleSearchChange}
isSearching={isSearching}
pagination={pagination}
onPaginationChange={handlePaginationChange}
totalCount={data?.pagination?.total || 0}
totalPages={data?.pagination?.totalPages || 1}
onSelectionChange={handleSelectionChange}
onDownloadAll={handleDownloadAll}
onDownloadSelected={handleDownloadSelected}
/>
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={deleteEndpoint.isPending}
>
{deleteEndpoint.isPending ? (
<>
<LoadingSpinner />
...
</>
) : (
"删除"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}