diff --git a/internal/site/src/components/network-probes-table/network-probes-columns.tsx b/internal/site/src/components/network-probes-table/network-probes-columns.tsx index c1aad175..b3dc6ae5 100644 --- a/internal/site/src/components/network-probes-table/network-probes-columns.tsx +++ b/internal/site/src/components/network-probes-table/network-probes-columns.tsx @@ -1,6 +1,6 @@ import type { CellContext, Column, ColumnDef } from "@tanstack/react-table" import { Button } from "@/components/ui/button" -import { cn, formatMicroseconds, hourWithSeconds } from "@/lib/utils" +import { cn, copyToClipboard, formatMicroseconds, hourWithSeconds } from "@/lib/utils" import { GlobeIcon, TimerIcon, @@ -15,6 +15,7 @@ import { PenBoxIcon, PauseCircleIcon, PlayCircleIcon, + CopyIcon, } from "lucide-react" import { t } from "@lingui/core/macro" import type { NetworkProbeRecord, SystemRecord } from "@/types" @@ -31,6 +32,7 @@ import { useStore } from "@nanostores/react" import { SystemStatus } from "@/lib/enums" import { Checkbox } from "@/components/ui/checkbox" import { useMemo } from "react" +import { formatBulkProbeLine } from "@/components/network-probes-table/probe-dialog" const protocolColors: Record = { icmp: "bg-blue-500/15 text-blue-400", @@ -256,6 +258,7 @@ export function getProbeColumns( : [row.original] const isBulkAction = actionRows.length > 1 const shouldPause = actionRows.some((probe) => probe.enabled) + const bulkCopyContent = actionRows.map((probe) => formatBulkProbeLine(probe)).join("\n") return ( @@ -269,8 +272,7 @@ export function getProbeColumns( event.stopPropagation()}> {!isBulkAction && ( { - event.stopPropagation() + onClick={() => { onEdit?.(row.original) }} > @@ -279,8 +281,7 @@ export function getProbeColumns( )} { - event.stopPropagation() + onClick={() => { onSetEnabled?.(actionRows, !shouldPause) }} > @@ -296,10 +297,17 @@ export function getProbeColumns( )} + { + copyToClipboard(bulkCopyContent) + }} + > + + Bulk copy + { - event.stopPropagation() + onClick={() => { onDelete?.(actionRows) }} > diff --git a/internal/site/src/components/network-probes-table/probe-dialog.tsx b/internal/site/src/components/network-probes-table/probe-dialog.tsx index bcddb120..c8df42ce 100644 --- a/internal/site/src/components/network-probes-table/probe-dialog.tsx +++ b/internal/site/src/components/network-probes-table/probe-dialog.tsx @@ -17,7 +17,7 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 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 { $systems } from "@/lib/stores" import type { NetworkProbeRecord } from "@/types" @@ -38,6 +38,8 @@ type NormalizedProbeValues = Omit & { interval: number } +type BulkProbeLineSource = Pick + const defaultInterval = 20 const ProbeProtocolSchema = v.picklist(["icmp", "tcp", "http"]) @@ -101,6 +103,14 @@ function normalizeHttpTarget(target: string, port: number) { 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) { const normalizedValues = v.safeParse(NormalizedProbeValuesSchema, values) 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[] }) { const [open, setOpen] = useState(false) const [bulkOpen, setBulkOpen] = useState(false) @@ -215,7 +231,7 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes } 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 @@ -291,19 +307,18 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes Bulk Add {{ foo: t`Network Probes` }} - - target[,protocol[,port[,interval[,name]]]] - TCP/HTTP default to port 443. - + target[,protocol[,port[,interval[,name]]]]
-
+
{!systemId && (
-
)} -
+
@@ -330,14 +345,11 @@ export function AddProbeDialog({ systemId, probes }: { systemId?: string; probes bulkFormRef.current?.requestSubmit() } }} - className="h-200 font-mono text-sm bg-muted/40" - style={{ maxHeight: `calc(100vh - 20rem)` }} - placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Homepage"].join("\n")} + className="font-mono grow text-sm bg-card" + placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Example"].join("\n")} required /> -

- target[,protocol[,port[,interval[,name]]]] • TCP and HTTP default to port 443. -

+

target[,protocol[,port[,interval[,name]]]]