This commit is contained in:
henrygd
2026-04-23 21:34:56 -04:00
parent 9f7c1b22bb
commit e154123511
6 changed files with 321 additions and 197 deletions

View File

@@ -20,6 +20,7 @@ import { Textarea } from "@/components/ui/textarea"
import { ChevronDownIcon, ListIcon } from "lucide-react"
import { useToast } from "@/components/ui/use-toast"
import { $systems } from "@/lib/stores"
import type { NetworkProbeRecord } from "@/types"
import * as v from "valibot"
type ProbeProtocol = "icmp" | "tcp" | "http"
@@ -59,6 +60,8 @@ function buildProbePayload(values: ProbeValues) {
payload.name = trimmedName
} else if (targetName !== values.target) {
payload.name = targetName
} else {
payload.name = ""
}
return payload
}
@@ -105,36 +108,19 @@ function parseBulkProbeLine(line: string, lineNumber: number, system: string) {
export function AddProbeDialog({ systemId }: { systemId?: string }) {
const [open, setOpen] = useState(false)
const [bulkOpen, setBulkOpen] = useState(false)
const [protocol, setProtocol] = useState<string>("icmp")
const [target, setTarget] = useState("")
const [port, setPort] = useState("")
const [probeInterval, setProbeInterval] = useState("30")
const [name, setName] = useState("")
const [loading, setLoading] = useState(false)
const [bulkInput, setBulkInput] = useState("")
const [bulkLoading, setBulkLoading] = useState(false)
const [selectedSystemId, setSelectedSystemId] = useState("")
const [bulkSelectedSystemId, setBulkSelectedSystemId] = useState("")
const systems = useStore($systems)
const { toast } = useToast()
const { t } = useLingui()
const targetName = target.replace(/^https?:\/\//, "")
const resetForm = () => {
setProtocol("icmp")
setTarget("")
setPort("")
setProbeInterval("30")
setName("")
setSelectedSystemId("")
}
const systems = useStore($systems)
const resetBulkForm = () => {
setBulkInput("")
setBulkSelectedSystemId("")
}
const openBulkAdd = () => {
const openBulkAdd = (selectedSystemId?: string) => {
if (!systemId && selectedSystemId) {
setBulkSelectedSystemId(selectedSystemId)
}
@@ -147,29 +133,6 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
setOpen(true)
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
try {
const payload = buildProbePayload({
system: systemId ?? selectedSystemId,
target,
protocol: protocol as ProbeProtocol,
port: protocol === "tcp" ? Number(port) : 0,
interval: probeInterval,
name,
})
await pb.collection("network_probes").create(payload)
resetForm()
setOpen(false)
} catch (err: unknown) {
toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message })
} finally {
setLoading(false)
}
}
async function handleBulkSubmit(e: React.FormEvent) {
e.preventDefault()
setBulkLoading(true)
@@ -227,119 +190,31 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={openBulkAdd}>
<DropdownMenuItem onClick={() => openBulkAdd(systemId)}>
<ListIcon className="size-4 me-2" />
<Trans>Bulk Add</Trans>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
<Trans>Add {{ foo: t`Network Probe` }}</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Configure response monitoring from this agent.</Trans>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="grid gap-4 tabular-nums">
{!systemId && (
<div className="grid gap-2">
<Label>
<Trans>System</Trans>
</Label>
<Select value={selectedSystemId} onValueChange={setSelectedSystemId} required>
<SelectTrigger>
<SelectValue placeholder={t`Select a system`} />
</SelectTrigger>
<SelectContent>
{systems.map((sys) => (
<SelectItem key={sys.id} value={sys.id}>
{sys.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="grid gap-2">
<Label>
<Trans>Target</Trans>
</Label>
<Input
value={target}
onChange={(e) => setTarget(e.target.value)}
placeholder={protocol === "http" ? "https://example.com" : "1.1.1.1"}
required
/>
</div>
<div className="grid gap-2">
<Label>
<Trans>Protocol</Trans>
</Label>
<Select value={protocol} onValueChange={setProtocol}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="icmp">ICMP</SelectItem>
<SelectItem value="tcp">TCP</SelectItem>
<SelectItem value="http">HTTP</SelectItem>
</SelectContent>
</Select>
</div>
{protocol === "tcp" && (
<div className="grid gap-2">
<Label>
<Trans>Port</Trans>
</Label>
<Input
type="number"
value={port}
onChange={(e) => setPort(e.target.value)}
placeholder="443"
min={1}
max={65535}
required
/>
</div>
)}
<div className="grid gap-2">
<Label>
<Trans>Interval (seconds)</Trans>
</Label>
<Input
type="number"
value={probeInterval}
onChange={(e) => setProbeInterval(e.target.value)}
min={1}
max={3600}
required
/>
</div>
<div className="grid gap-2">
<Label>
<Trans>Name (optional)</Trans>
</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={targetName || t`e.g. Cloudflare DNS`}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={loading || (!systemId && !selectedSystemId)}>
{loading ? <Trans>Creating...</Trans> : <Trans>Add {{ foo: t`Probe` }}</Trans>}
</Button>
</DialogFooter>
</form>
</DialogContent>
<Dialog
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen)
}}
>
{open && <ProbeDialogContent setOpen={setOpen} systemId={systemId} onOpenBulkAdd={openBulkAdd} />}
</Dialog>
<Sheet open={bulkOpen} onOpenChange={setBulkOpen}>
<Sheet
open={bulkOpen}
onOpenChange={(nextOpen) => {
setBulkOpen(nextOpen)
if (!nextOpen) {
resetBulkForm()
}
}}
>
<SheetContent className="w-full sm:max-w-xl gap-0">
<SheetHeader className="border-b">
<SheetTitle>
@@ -411,3 +286,203 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
</>
)
}
export function EditProbeDialog({
open,
setOpen,
systemId,
probe,
}: {
open: boolean
setOpen: (open: boolean) => void
systemId?: string
probe?: NetworkProbeRecord
}) {
if (!probe) {
return null
}
return (
<Dialog open={open} onOpenChange={setOpen}>
{open && <ProbeDialogContent setOpen={setOpen} systemId={systemId} probe={probe} />}
</Dialog>
)
}
function ProbeDialogContent({
setOpen,
systemId,
probe,
onOpenBulkAdd,
}: {
setOpen: (open: boolean) => void
systemId?: string
probe?: NetworkProbeRecord
onOpenBulkAdd?: (selectedSystemId?: string) => void
}) {
const [protocol, setProtocol] = useState<ProbeProtocol>(probe?.protocol ?? "icmp")
const [target, setTarget] = useState(probe?.target ?? "")
const [port, setPort] = useState(probe?.protocol === "tcp" && probe.port ? String(probe.port) : "")
const [probeInterval, setProbeInterval] = useState(String(probe?.interval ?? 30))
const [name, setName] = useState(probe?.name ?? "")
const [loading, setLoading] = useState(false)
const [selectedSystemId, setSelectedSystemId] = useState(probe?.system ?? "")
const systems = useStore($systems)
const { toast } = useToast()
const { t } = useLingui()
const isEditing = !!probe
const targetName = target.replace(/^https?:\/\//, "")
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setLoading(true)
try {
const payload = buildProbePayload({
system: systemId ?? selectedSystemId,
target,
protocol,
port: protocol === "tcp" ? Number(port) : 0,
interval: probeInterval,
name,
})
if (probe) {
await pb.collection("network_probes").update(probe.id, payload)
} else {
await pb.collection("network_probes").create(payload)
}
setOpen(false)
} catch (err: unknown) {
toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message })
} finally {
setLoading(false)
}
}
return (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>
{isEditing ? <Trans>Edit {{ foo: t`Network Probe` }}</Trans> : <Trans>Add {{ foo: t`Network Probe` }}</Trans>}
</DialogTitle>
<DialogDescription>
<Trans>Configure response monitoring from this agent.</Trans>
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="grid gap-4 tabular-nums">
{!systemId && (
<div className="grid gap-2">
<Label>
<Trans>System</Trans>
</Label>
<Select value={selectedSystemId} onValueChange={setSelectedSystemId} required>
<SelectTrigger>
<SelectValue placeholder={t`Select a system`} />
</SelectTrigger>
<SelectContent>
{systems.map((sys) => (
<SelectItem key={sys.id} value={sys.id}>
{sys.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="grid gap-2">
<Label>
<Trans>Target</Trans>
</Label>
<Input
value={target}
onChange={(e) => setTarget(e.target.value)}
placeholder={protocol === "http" ? "https://example.com" : "1.1.1.1"}
required
/>
</div>
<div className="grid gap-2">
<Label>
<Trans>Protocol</Trans>
</Label>
<Select value={protocol} onValueChange={(value) => setProtocol(value as ProbeProtocol)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="icmp">ICMP</SelectItem>
<SelectItem value="tcp">TCP</SelectItem>
<SelectItem value="http">HTTP</SelectItem>
</SelectContent>
</Select>
</div>
{protocol === "tcp" && (
<div className="grid gap-2">
<Label>
<Trans>Port</Trans>
</Label>
<Input
type="number"
value={port}
onChange={(e) => setPort(e.target.value)}
placeholder="443"
min={1}
max={65535}
required
/>
</div>
)}
<div className="grid gap-2">
<Label>
<Trans>Interval (seconds)</Trans>
</Label>
<Input
type="number"
value={probeInterval}
onChange={(e) => setProbeInterval(e.target.value)}
min={1}
max={3600}
required
/>
</div>
<div className="grid gap-2">
<Label>
<Trans>Name (optional)</Trans>
</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={targetName || t`e.g. Cloudflare DNS`}
/>
</div>
<DialogFooter>
{!isEditing && onOpenBulkAdd && (
<Button
type="button"
variant="outline"
onClick={() => onOpenBulkAdd(selectedSystemId)}
disabled={loading}
className="me-auto"
>
<ListIcon className="size-4 me-2" />
<Trans>Bulk Add</Trans>
</Button>
)}
<Button type="submit" disabled={loading || (!systemId && !selectedSystemId)}>
{loading ? (
isEditing ? (
<Trans>Saving...</Trans>
) : (
<Trans>Creating...</Trans>
)
) : isEditing ? (
<Trans>Save {{ foo: t`Probe` }}</Trans>
) : (
<Trans>Add {{ foo: t`Probe` }}</Trans>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
)
}