This commit is contained in:
henrygd
2026-04-28 18:29:41 -04:00
parent 891b03426f
commit 04e2b8b974
2 changed files with 42 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
import type { CellContext, Column, ColumnDef } from "@tanstack/react-table" import type { CellContext, Column, ColumnDef } from "@tanstack/react-table"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { cn, formatMicroseconds, hourWithSeconds } from "@/lib/utils" import { cn, copyToClipboard, formatMicroseconds, hourWithSeconds } from "@/lib/utils"
import { import {
GlobeIcon, GlobeIcon,
TimerIcon, TimerIcon,
@@ -15,6 +15,7 @@ import {
PenBoxIcon, PenBoxIcon,
PauseCircleIcon, PauseCircleIcon,
PlayCircleIcon, PlayCircleIcon,
CopyIcon,
} from "lucide-react" } from "lucide-react"
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
import type { NetworkProbeRecord, SystemRecord } from "@/types" import type { NetworkProbeRecord, SystemRecord } from "@/types"
@@ -31,6 +32,7 @@ import { useStore } from "@nanostores/react"
import { SystemStatus } from "@/lib/enums" import { SystemStatus } from "@/lib/enums"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { useMemo } from "react" import { useMemo } from "react"
import { formatBulkProbeLine } from "@/components/network-probes-table/probe-dialog"
const protocolColors: Record<string, string> = { const protocolColors: Record<string, string> = {
icmp: "bg-blue-500/15 text-blue-400", icmp: "bg-blue-500/15 text-blue-400",
@@ -256,6 +258,7 @@ export function getProbeColumns(
: [row.original] : [row.original]
const isBulkAction = actionRows.length > 1 const isBulkAction = actionRows.length > 1
const shouldPause = actionRows.some((probe) => probe.enabled) const shouldPause = actionRows.some((probe) => probe.enabled)
const bulkCopyContent = actionRows.map((probe) => formatBulkProbeLine(probe)).join("\n")
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -269,8 +272,7 @@ export function getProbeColumns(
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}> <DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
{!isBulkAction && ( {!isBulkAction && (
<DropdownMenuItem <DropdownMenuItem
onClick={(event) => { onClick={() => {
event.stopPropagation()
onEdit?.(row.original) onEdit?.(row.original)
}} }}
> >
@@ -279,8 +281,7 @@ export function getProbeColumns(
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem <DropdownMenuItem
onClick={(event) => { onClick={() => {
event.stopPropagation()
onSetEnabled?.(actionRows, !shouldPause) onSetEnabled?.(actionRows, !shouldPause)
}} }}
> >
@@ -296,10 +297,17 @@ export function getProbeColumns(
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
copyToClipboard(bulkCopyContent)
}}
>
<CopyIcon className="me-2.5 size-4" />
<Trans>Bulk copy</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={(event) => { onClick={() => {
event.stopPropagation()
onDelete?.(actionRows) onDelete?.(actionRows)
}} }}
> >

View File

@@ -17,7 +17,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { ChevronDownIcon, ListIcon } from "lucide-react" import { ChevronDownIcon, ListIcon, ServerIcon } from "lucide-react"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { $systems } from "@/lib/stores" import { $systems } from "@/lib/stores"
import type { NetworkProbeRecord } from "@/types" import type { NetworkProbeRecord } from "@/types"
@@ -38,6 +38,8 @@ type NormalizedProbeValues = Omit<ProbeValues, "system" | "interval"> & {
interval: number interval: number
} }
type BulkProbeLineSource = Pick<NetworkProbeRecord, "target" | "protocol" | "port" | "interval" | "name">
const defaultInterval = 20 const defaultInterval = 20
const ProbeProtocolSchema = v.picklist(["icmp", "tcp", "http"]) const ProbeProtocolSchema = v.picklist(["icmp", "tcp", "http"])
@@ -101,6 +103,14 @@ function normalizeHttpTarget(target: string, port: number) {
return `${port === 443 ? "https" : "http"}://${target}` return `${port === 443 ? "https" : "http"}://${target}`
} }
function trimTrailingEmptyFields(fields: string[]) {
let lastValueIndex = fields.length - 1
while (lastValueIndex > 0 && !fields[lastValueIndex]) {
lastValueIndex--
}
return fields.slice(0, lastValueIndex + 1)
}
function buildProbePayload(values: ProbeValues, enabled = true) { function buildProbePayload(values: ProbeValues, enabled = true) {
const normalizedValues = v.safeParse(NormalizedProbeValuesSchema, values) const normalizedValues = v.safeParse(NormalizedProbeValuesSchema, values)
if (!normalizedValues.success) { if (!normalizedValues.success) {
@@ -154,6 +164,12 @@ function parseBulkProbeLine(line: string, lineNumber: number, system: string) {
}) })
} }
export function formatBulkProbeLine(probe: BulkProbeLineSource) {
const port = probe.protocol === "icmp" || probe.port === 443 ? "" : `${probe.port}`
const interval = probe.interval === defaultInterval ? "" : `${probe.interval}`
return trimTrailingEmptyFields([probe.target, probe.protocol, port, interval, probe.name?.trim() || ""]).join(",")
}
export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes: NetworkProbeRecord[] }) { export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes: NetworkProbeRecord[] }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [bulkOpen, setBulkOpen] = useState(false) const [bulkOpen, setBulkOpen] = useState(false)
@@ -215,7 +231,7 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes
} }
if (!newPayloads.length) { if (!newPayloads.length) {
throw new Error("No new probes to add. All entries already exist.") throw new Error("No new probes. All entries exist.")
} }
closedForSubmit = true closedForSubmit = true
@@ -291,19 +307,18 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes
<SheetTitle> <SheetTitle>
<Trans>Bulk Add {{ foo: t`Network Probes` }}</Trans> <Trans>Bulk Add {{ foo: t`Network Probes` }}</Trans>
</SheetTitle> </SheetTitle>
<SheetDescription> <SheetDescription>target[,protocol[,port[,interval[,name]]]]</SheetDescription>
target[,protocol[,port[,interval[,name]]]] - TCP/HTTP default to port 443.
</SheetDescription>
</SheetHeader> </SheetHeader>
<form ref={bulkFormRef} onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden"> <form ref={bulkFormRef} onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden">
<div className="flex-1 space-y-4 overflow-auto p-4"> <div className="flex-1 flex flex-col space-y-4 overflow-auto p-4">
{!systemId && ( {!systemId && (
<div className="grid gap-2"> <div className="grid gap-2">
<Label> <Label className="sr-only">
<Trans>System</Trans> <Trans>System</Trans>
</Label> </Label>
<Select value={bulkSelectedSystemId} onValueChange={setBulkSelectedSystemId} required> <Select value={bulkSelectedSystemId} onValueChange={setBulkSelectedSystemId} required>
<SelectTrigger> <SelectTrigger className="relative ps-10 pe-5 bg-card">
<ServerIcon className="size-3.5 absolute start-4 top-1/2 -translate-y-1/2 opacity-85" />
<SelectValue placeholder={t`Select a system`} /> <SelectValue placeholder={t`Select a system`} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -316,7 +331,7 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes
</Select> </Select>
</div> </div>
)} )}
<div className="grid gap-2"> <div className="grow flex flex-col gap-2">
<Label htmlFor="bulk-probes" className="sr-only"> <Label htmlFor="bulk-probes" className="sr-only">
Entries Entries
</Label> </Label>
@@ -330,14 +345,11 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes
bulkFormRef.current?.requestSubmit() bulkFormRef.current?.requestSubmit()
} }
}} }}
className="h-200 font-mono text-sm bg-muted/40" className="font-mono grow text-sm bg-card"
style={{ maxHeight: `calc(100vh - 20rem)` }} placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Example"].join("\n")}
placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Homepage"].join("\n")}
required required
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">target[,protocol[,port[,interval[,name]]]]</p>
target[,protocol[,port[,interval[,name]]]] TCP and HTTP default to port 443.
</p>
</div> </div>
</div> </div>
<SheetFooter className="border-t"> <SheetFooter className="border-t">