Files
xingrin/frontend/components/endpoints/endpoints-columns.tsx

338 lines
9.7 KiB
TypeScript
Raw Normal View History

2025-12-12 18:04:57 +08:00
"use client"
import React from "react"
import { ColumnDef } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
2025-12-13 19:34:56 +08:00
import { ChevronsUpDown, ChevronUp, ChevronDown } from "lucide-react"
2025-12-12 18:04:57 +08:00
import type { Endpoint } from "@/types/endpoint.types"
2025-12-13 19:34:56 +08:00
import { TruncatedCell, TruncatedUrlCell } from "@/components/ui/truncated-cell"
2025-12-12 18:04:57 +08:00
interface CreateColumnsProps {
formatDate: (dateString: string) => string
}
function DataTableColumnHeader({
column,
title,
}: {
column: { getCanSort: () => boolean; getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void }
title: string
}) {
if (!column.getCanSort()) {
return <div className="-ml-3 font-medium">{title}</div>
}
const isSorted = column.getIsSorted()
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="-ml-3 h-8 data-[state=open]:bg-accent hover:bg-muted"
>
{title}
{isSorted === "asc" ? (
<ChevronUp />
) : isSorted === "desc" ? (
<ChevronDown />
) : (
<ChevronsUpDown />
)}
</Button>
)
}
function HttpStatusBadge({ statusCode }: { statusCode: number | null | undefined }) {
if (statusCode === null || statusCode === undefined) {
return (
<Badge variant="outline" className="text-muted-foreground px-2 py-1 font-mono">
-
</Badge>
)
}
const getStatusVariant = (code: number): "default" | "secondary" | "destructive" | "outline" => {
if (code >= 200 && code < 300) {
return "outline"
} else if (code >= 300 && code < 400) {
return "secondary"
} else if (code >= 400 && code < 500) {
return "default"
} else if (code >= 500) {
return "destructive"
} else {
return "secondary"
}
}
const variant = getStatusVariant(statusCode)
return (
<Badge variant={variant} className="px-2 py-1 font-mono tabular-nums">
{statusCode}
</Badge>
)
}
2025-12-22 11:14:46 +08:00
/**
* Body Preview - 3
*/
function BodyPreviewCell({ value }: { value: string | null | undefined }) {
const [expanded, setExpanded] = React.useState(false)
if (!value) {
return <span className="text-muted-foreground text-sm">-</span>
}
return (
<div className="flex flex-col gap-1">
<div
className={`text-sm text-muted-foreground break-all leading-relaxed whitespace-normal cursor-pointer hover:text-foreground transition-colors ${!expanded ? 'line-clamp-3' : ''}`}
onClick={() => setExpanded(!expanded)}
title={expanded ? "点击收起" : "点击展开"}
>
{value}
</div>
{value.length > 100 && (
<button
onClick={() => setExpanded(!expanded)}
className="text-xs text-primary hover:underline self-start"
>
{expanded ? "收起" : "展开"}
</button>
)}
</div>
)
}
2025-12-12 18:04:57 +08:00
export function createEndpointColumns({
formatDate,
}: CreateColumnsProps): ColumnDef<Endpoint>[] {
return [
{
id: "select",
2025-12-22 10:04:27 +08:00
size: 40,
minSize: 40,
maxSize: 40,
enableResizing: false,
2025-12-12 18:04:57 +08:00
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "url",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="URL" />
),
2025-12-22 10:04:27 +08:00
size: 400,
2025-12-12 18:04:57 +08:00
minSize: 200,
2025-12-22 10:04:27 +08:00
maxSize: 700,
cell: ({ row }) => {
const url = row.getValue("url") as string
return <TruncatedUrlCell value={url} />
},
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "title",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Title" />
),
2025-12-22 10:04:27 +08:00
size: 150,
minSize: 100,
maxSize: 300,
2025-12-13 19:34:56 +08:00
cell: ({ row }) => (
<TruncatedCell value={row.getValue("title")} maxLength="title" />
),
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "statusCode",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
2025-12-22 10:04:27 +08:00
size: 80,
minSize: 60,
maxSize: 100,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const status = row.getValue("statusCode") as number | null | undefined
return <HttpStatusBadge statusCode={status} />
},
},
{
accessorKey: "contentLength",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Content Length" />
),
2025-12-22 10:04:27 +08:00
size: 100,
minSize: 80,
maxSize: 150,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const len = row.getValue("contentLength") as number | null | undefined
if (len === null || len === undefined) {
return <span className="text-muted-foreground text-sm">-</span>
}
return <span className="font-mono tabular-nums">{new Intl.NumberFormat().format(len)}</span>
},
},
{
accessorKey: "location",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Location" />
),
2025-12-22 10:04:27 +08:00
size: 150,
minSize: 100,
maxSize: 300,
2025-12-13 19:34:56 +08:00
cell: ({ row }) => (
<TruncatedCell value={row.getValue("location")} maxLength="location" />
),
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "webserver",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Web Server" />
),
2025-12-22 10:04:27 +08:00
size: 120,
minSize: 80,
maxSize: 200,
2025-12-13 19:34:56 +08:00
cell: ({ row }) => (
<TruncatedCell value={row.getValue("webserver")} maxLength="webServer" />
),
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "contentType",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Content Type" />
),
2025-12-22 10:04:27 +08:00
size: 120,
minSize: 80,
maxSize: 200,
2025-12-13 19:34:56 +08:00
cell: ({ row }) => (
<TruncatedCell value={row.getValue("contentType")} maxLength="contentType" />
),
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "tech",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Technologies" />
),
2025-12-22 10:04:27 +08:00
size: 200,
minSize: 150,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const tech = (row.getValue("tech") as string[] | null | undefined) || []
if (!tech.length) return <span className="text-sm text-muted-foreground">-</span>
return (
2025-12-22 11:14:46 +08:00
<div className="flex flex-wrap items-center gap-1.5">
{tech.map((t, index) => (
2025-12-12 18:04:57 +08:00
<Badge key={index} variant="outline" className="text-xs">
{t}
</Badge>
))}
</div>
)
},
},
{
accessorKey: "bodyPreview",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Body Preview" />
),
2025-12-22 11:14:46 +08:00
size: 350,
minSize: 250,
2025-12-13 19:34:56 +08:00
cell: ({ row }) => (
2025-12-22 11:14:46 +08:00
<BodyPreviewCell value={row.getValue("bodyPreview")} />
2025-12-13 19:34:56 +08:00
),
2025-12-12 18:04:57 +08:00
},
{
accessorKey: "vhost",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="VHost" />
),
2025-12-22 10:04:27 +08:00
size: 80,
minSize: 60,
maxSize: 100,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const vhost = row.getValue("vhost") as boolean | null | undefined
if (vhost === null || vhost === undefined) return <span className="text-sm text-muted-foreground">-</span>
return <span className="text-sm font-mono">{vhost ? "true" : "false"}</span>
},
},
{
accessorKey: "tags",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Tags" />
),
2025-12-22 10:04:27 +08:00
size: 150,
minSize: 100,
maxSize: 250,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const tags = (row.getValue("tags") as string[] | null | undefined) || []
if (!tags.length) {
return <span className="text-muted-foreground text-sm">-</span>
}
return (
<div className="flex flex-wrap gap-1">
{tags.map((tag, idx) => (
<Badge
key={idx}
variant={/xss|sqli|idor|rce|ssrf|lfi|rfi|xxe|csrf|open.?redirect|interesting/i.test(tag) ? "destructive" : "secondary"}
className="text-xs"
>
{tag}
</Badge>
))}
</div>
)
},
enableSorting: false,
},
{
accessorKey: "responseTime",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Response Time" />
),
2025-12-22 10:04:27 +08:00
size: 100,
minSize: 80,
maxSize: 150,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const rt = row.getValue("responseTime") as number | null | undefined
if (rt === null || rt === undefined) {
return <span className="text-muted-foreground text-sm">-</span>
}
const formatted = `${rt.toFixed(4)}s`
return <span className="font-mono text-emerald-600 dark:text-emerald-400">{formatted}</span>
},
},
{
accessorKey: "discoveredAt",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Discovered At" />
),
2025-12-22 10:04:27 +08:00
size: 150,
minSize: 120,
maxSize: 200,
2025-12-12 18:04:57 +08:00
cell: ({ row }) => {
const discoveredAt = row.getValue("discoveredAt") as string | undefined
return <div className="text-sm">{discoveredAt ? formatDate(discoveredAt) : "-"}</div>
},
},
]
}