mirror of
https://github.com/henrygd/beszel.git
synced 2026-05-06 10:51:50 +02:00
updates
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
package hub
|
package hub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/entities/probe"
|
"github.com/henrygd/beszel/internal/entities/probe"
|
||||||
"github.com/henrygd/beszel/internal/hub/systems"
|
"github.com/henrygd/beszel/internal/hub/systems"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -10,9 +8,7 @@ import (
|
|||||||
|
|
||||||
// generateProbeID creates a stable hash ID for a probe based on its configuration and the system it belongs to.
|
// generateProbeID creates a stable hash ID for a probe based on its configuration and the system it belongs to.
|
||||||
func generateProbeID(systemId string, config probe.Config) string {
|
func generateProbeID(systemId string, config probe.Config) string {
|
||||||
intervalStr := strconv.FormatUint(uint64(config.Interval), 10)
|
return systems.MakeStableHashId(systemId, config.Target, config.Protocol)
|
||||||
portStr := strconv.FormatUint(uint64(config.Port), 10)
|
|
||||||
return systems.MakeStableHashId(systemId, config.Protocol, config.Target, portStr, intervalStr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindNetworkProbesEvents keeps probe records and agent probe state in sync.
|
// bindNetworkProbesEvents keeps probe records and agent probe state in sync.
|
||||||
@@ -27,7 +23,7 @@ func bindNetworkProbesEvents(hub *Hub) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// sync probe to agent on creation and persist the first result immediately when available
|
// sync probe to agent on creation and persist the first result immediately when available
|
||||||
hub.OnRecordCreateRequest("network_probes").BindFunc(func(e *core.RecordRequestEvent) error {
|
hub.OnRecordAfterCreateSuccess("network_probes").BindFunc(func(e *core.RecordEvent) error {
|
||||||
err := e.Next()
|
err := e.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -47,10 +43,24 @@ func bindNetworkProbesEvents(hub *Hub) {
|
|||||||
if err := e.App.SaveNoValidate(e.Record); err != nil {
|
if err := e.App.SaveNoValidate(e.Record); err != nil {
|
||||||
hub.Logger().Warn("failed to save initial probe result", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
hub.Logger().Warn("failed to save initial probe result", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
||||||
}
|
}
|
||||||
return nil
|
return e.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// On API update requests, if the probe config changed in a way that requires a new ID, we will create a new
|
||||||
|
// record with the new ID and delete the old one. Otherwise, we will just update the existing probe on the agent.
|
||||||
hub.OnRecordUpdateRequest("network_probes").BindFunc(func(e *core.RecordRequestEvent) error {
|
hub.OnRecordUpdateRequest("network_probes").BindFunc(func(e *core.RecordRequestEvent) error {
|
||||||
|
systemID := e.Record.GetString("system")
|
||||||
|
ID := generateProbeID(systemID, *probeConfigFromRecord(e.Record))
|
||||||
|
if ID != e.Record.Id {
|
||||||
|
newRecord := copyProbeToNewRecord(e.Record, ID)
|
||||||
|
if err := e.App.Save(newRecord); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.App.Delete(e.Record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err := e.Next()
|
err := e.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -67,15 +77,11 @@ func bindNetworkProbesEvents(hub *Hub) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// sync probe to agent on delete
|
// sync probe to agent on delete
|
||||||
hub.OnRecordDeleteRequest("network_probes").BindFunc(func(e *core.RecordRequestEvent) error {
|
hub.OnRecordAfterDeleteSuccess("network_probes").BindFunc(func(e *core.RecordEvent) error {
|
||||||
err := e.Next()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := hub.deleteNetworkProbe(e.Record); err != nil {
|
if err := hub.deleteNetworkProbe(e.Record); err != nil {
|
||||||
hub.Logger().Warn("failed to delete probe on agent", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
hub.Logger().Warn("failed to delete probe on agent", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
||||||
}
|
}
|
||||||
return nil
|
return e.Next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +105,17 @@ func setProbeResultFields(record *core.Record, result probe.Result) {
|
|||||||
record.Set("loss1h", result.Get(4))
|
record.Set("loss1h", result.Get(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyProbeToNewRecord creates a new record with the same field values as the old one.
|
||||||
|
// This is used when the probe config changes in a way that requires a new ID, so we need
|
||||||
|
// to create a new record with the new ID and delete the old one.
|
||||||
|
func copyProbeToNewRecord(oldRecord *core.Record, newID string) *core.Record {
|
||||||
|
collection := oldRecord.Collection()
|
||||||
|
newRecord := core.NewRecord(collection)
|
||||||
|
newRecord.Load(oldRecord.FieldsData())
|
||||||
|
newRecord.Set("id", newID)
|
||||||
|
return newRecord
|
||||||
|
}
|
||||||
|
|
||||||
// upsertNetworkProbe applies the record's probe config to the target system.
|
// upsertNetworkProbe applies the record's probe config to the target system.
|
||||||
func (h *Hub) upsertNetworkProbe(record *core.Record, runNow bool) (*probe.Result, error) {
|
func (h *Hub) upsertNetworkProbe(record *core.Record, runNow bool) (*probe.Result, error) {
|
||||||
systemID := record.GetString("system")
|
systemID := record.GetString("system")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestGenerateProbeID(t *testing.T) {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
Interval: 60,
|
Interval: 60,
|
||||||
},
|
},
|
||||||
expected: "d5f27931",
|
expected: "a20a5827",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HTTP probe on example.com with different system ID",
|
name: "HTTP probe on example.com with different system ID",
|
||||||
@@ -34,7 +34,7 @@ func TestGenerateProbeID(t *testing.T) {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
Interval: 60,
|
Interval: 60,
|
||||||
},
|
},
|
||||||
expected: "6f8b17f1",
|
expected: "ab602ae7",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Same probe, different interval",
|
name: "Same probe, different interval",
|
||||||
@@ -45,7 +45,7 @@ func TestGenerateProbeID(t *testing.T) {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
Interval: 120,
|
Interval: 120,
|
||||||
},
|
},
|
||||||
expected: "6d4baf8",
|
expected: "ab602ae7",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ICMP probe on 1.1.1.1",
|
name: "ICMP probe on 1.1.1.1",
|
||||||
@@ -56,7 +56,7 @@ func TestGenerateProbeID(t *testing.T) {
|
|||||||
Port: 0,
|
Port: 0,
|
||||||
Interval: 10,
|
Interval: 10,
|
||||||
},
|
},
|
||||||
expected: "80b5836b",
|
expected: "6d13a4a4",
|
||||||
}, {
|
}, {
|
||||||
name: "ICMP probe on 1.1.1.1 with different system ID",
|
name: "ICMP probe on 1.1.1.1 with different system ID",
|
||||||
systemID: "sys4567",
|
systemID: "sys4567",
|
||||||
@@ -66,7 +66,7 @@ func TestGenerateProbeID(t *testing.T) {
|
|||||||
Port: 0,
|
Port: 0,
|
||||||
Interval: 10,
|
Interval: 10,
|
||||||
},
|
},
|
||||||
expected: "a6652680",
|
expected: "ddd6c81",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"log/slog"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -325,7 +324,6 @@ func updateNetworkProbesRecords(app core.App, data map[string]probe.Result, syst
|
|||||||
// If realtime updates are active, we save via PocketBase records to trigger realtime events.
|
// If realtime updates are active, we save via PocketBase records to trigger realtime events.
|
||||||
// Otherwise we can do a more efficient direct update via SQL
|
// Otherwise we can do a more efficient direct update via SQL
|
||||||
realtimeActive := utils.RealtimeActiveForCollection(app, collectionName, func(filterQuery string) bool {
|
realtimeActive := utils.RealtimeActiveForCollection(app, collectionName, func(filterQuery string) bool {
|
||||||
slog.Info("Checking realtime subscription filter for network probes", "filterQuery", filterQuery)
|
|
||||||
return !strings.Contains(filterQuery, "system") || strings.Contains(filterQuery, systemId)
|
return !strings.Contains(filterQuery, "system") || strings.Contains(filterQuery, systemId)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -335,35 +333,10 @@ func updateNetworkProbesRecords(app core.App, data map[string]probe.Result, syst
|
|||||||
if !realtimeActive {
|
if !realtimeActive {
|
||||||
db = app.DB()
|
db = app.DB()
|
||||||
nowString = time.Now().UTC().Format(types.DefaultDateLayout)
|
nowString = time.Now().UTC().Format(types.DefaultDateLayout)
|
||||||
sql := fmt.Sprintf("UPDATE %s SET resAvg={:res}, resMin1h={:resMin1h}, resMax1h={:resMax1h}, resAvg1h={:resAvg1h}, loss1h={:loss1h}, updated={:updated} WHERE id={:id}", collectionName)
|
sql := fmt.Sprintf("UPDATE %s SET res={:res}, resMin1h={:resMin1h}, resMax1h={:resMax1h}, resAvg1h={:resAvg1h}, loss1h={:loss1h}, updated={:updated} WHERE id={:id}", collectionName)
|
||||||
updateQuery = db.NewQuery(sql)
|
updateQuery = db.NewQuery(sql)
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert network probe stats records
|
|
||||||
switch realtimeActive {
|
|
||||||
case true:
|
|
||||||
collection, _ := app.FindCachedCollectionByNameOrId("network_probe_stats")
|
|
||||||
record := core.NewRecord(collection)
|
|
||||||
record.Set("system", systemId)
|
|
||||||
record.Set("stats", data)
|
|
||||||
record.Set("type", "1m")
|
|
||||||
err = app.SaveNoValidate(record)
|
|
||||||
default:
|
|
||||||
if dataJSON, marshalErr := json.Marshal(data); marshalErr == nil {
|
|
||||||
sql := "INSERT INTO network_probe_stats (system, stats, type, created) VALUES ({:system}, {:stats}, {:type}, {:created})"
|
|
||||||
insertQuery := db.NewQuery(sql)
|
|
||||||
_, err = insertQuery.Bind(dbx.Params{
|
|
||||||
"system": systemId,
|
|
||||||
"stats": dataJSON,
|
|
||||||
"type": "1m",
|
|
||||||
"created": nowString,
|
|
||||||
}).Execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Failed to update probe stats", "system", systemId, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update network_probes records
|
// update network_probes records
|
||||||
for id, values := range data {
|
for id, values := range data {
|
||||||
switch realtimeActive {
|
switch realtimeActive {
|
||||||
@@ -394,6 +367,31 @@ func updateNetworkProbesRecords(app core.App, data map[string]probe.Result, syst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert network probe stats records
|
||||||
|
switch realtimeActive {
|
||||||
|
case true:
|
||||||
|
collection, _ := app.FindCachedCollectionByNameOrId("network_probe_stats")
|
||||||
|
record := core.NewRecord(collection)
|
||||||
|
record.Set("system", systemId)
|
||||||
|
record.Set("stats", data)
|
||||||
|
record.Set("type", "1m")
|
||||||
|
err = app.SaveNoValidate(record)
|
||||||
|
default:
|
||||||
|
var statsJson types.JSONRaw
|
||||||
|
if err := statsJson.Scan(data); err == nil {
|
||||||
|
insertQuery := db.NewQuery("INSERT INTO network_probe_stats (system, stats, type, created) VALUES ({:system}, {:stats}, {:type}, {:created})")
|
||||||
|
_, err = insertQuery.Bind(dbx.Params{
|
||||||
|
"system": systemId,
|
||||||
|
"stats": statsJson,
|
||||||
|
"type": "1m",
|
||||||
|
"created": nowString,
|
||||||
|
}).Execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.Logger().Error("Failed to update probe stats", "system", systemId, "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
NetworkIcon,
|
NetworkIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
|
PenBoxIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import type { NetworkProbeRecord } from "@/types"
|
import type { NetworkProbeRecord } from "@/types"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
import { toast } from "../ui/use-toast"
|
import { toast } from "../ui/use-toast"
|
||||||
@@ -36,7 +43,11 @@ async function deleteProbe(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProbeColumns(longestName = 0, longestTarget = 0): ColumnDef<NetworkProbeRecord>[] {
|
export function getProbeColumns(
|
||||||
|
longestName = 0,
|
||||||
|
longestTarget = 0,
|
||||||
|
onEdit?: (probe: NetworkProbeRecord) => void
|
||||||
|
): ColumnDef<NetworkProbeRecord>[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "name",
|
id: "name",
|
||||||
@@ -177,6 +188,16 @@ export function getProbeColumns(longestName = 0, longestTarget = 0): ColumnDef<N
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
|
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onEdit?.(row.original)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="me-2.5 size-4" />
|
||||||
|
<Trans>Edit</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import { $allSystemsById } from "@/lib/stores"
|
|||||||
import { cn, getVisualStringWidth, useBrowserStorage } from "@/lib/utils"
|
import { cn, getVisualStringWidth, useBrowserStorage } from "@/lib/utils"
|
||||||
import { Trash2Icon } from "lucide-react"
|
import { Trash2Icon } from "lucide-react"
|
||||||
import type { NetworkProbeRecord } from "@/types"
|
import type { NetworkProbeRecord } from "@/types"
|
||||||
import { AddProbeDialog } from "./probe-dialog"
|
import { AddProbeDialog, EditProbeDialog } from "./probe-dialog"
|
||||||
|
|
||||||
export default function NetworkProbesTableNew({
|
export default function NetworkProbesTableNew({
|
||||||
systemId,
|
systemId,
|
||||||
@@ -59,6 +59,7 @@ export default function NetworkProbesTableNew({
|
|||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
const [globalFilter, setGlobalFilter] = useState("")
|
const [globalFilter, setGlobalFilter] = useState("")
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||||
|
const [editingProbe, setEditingProbe] = useState<NetworkProbeRecord>()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const canManageProbes = !isReadOnlyUser()
|
const canManageProbes = !isReadOnlyUser()
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export default function NetworkProbesTableNew({
|
|||||||
|
|
||||||
// Filter columns based on whether systemId is provided
|
// Filter columns based on whether systemId is provided
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
let columns = getProbeColumns(longestName, longestTarget)
|
let columns = getProbeColumns(longestName, longestTarget, setEditingProbe)
|
||||||
columns = systemId ? columns.filter((col) => col.id !== "system") : columns
|
columns = systemId ? columns.filter((col) => col.id !== "system") : columns
|
||||||
columns = canManageProbes ? columns : columns.filter((col) => col.id !== "actions")
|
columns = canManageProbes ? columns : columns.filter((col) => col.id !== "actions")
|
||||||
if (!canManageProbes) {
|
if (!canManageProbes) {
|
||||||
@@ -233,6 +234,18 @@ export default function NetworkProbesTableNew({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canManageProbes ? <AddProbeDialog systemId={systemId} /> : null}
|
{canManageProbes ? <AddProbeDialog systemId={systemId} /> : null}
|
||||||
|
{canManageProbes ? (
|
||||||
|
<EditProbeDialog
|
||||||
|
systemId={systemId}
|
||||||
|
probe={editingProbe}
|
||||||
|
open={!!editingProbe}
|
||||||
|
setOpen={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setEditingProbe(undefined)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Textarea } from "@/components/ui/textarea"
|
|||||||
import { ChevronDownIcon, ListIcon } from "lucide-react"
|
import { ChevronDownIcon, ListIcon } 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 * as v from "valibot"
|
import * as v from "valibot"
|
||||||
|
|
||||||
type ProbeProtocol = "icmp" | "tcp" | "http"
|
type ProbeProtocol = "icmp" | "tcp" | "http"
|
||||||
@@ -59,6 +60,8 @@ function buildProbePayload(values: ProbeValues) {
|
|||||||
payload.name = trimmedName
|
payload.name = trimmedName
|
||||||
} else if (targetName !== values.target) {
|
} else if (targetName !== values.target) {
|
||||||
payload.name = targetName
|
payload.name = targetName
|
||||||
|
} else {
|
||||||
|
payload.name = ""
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -105,36 +108,19 @@ function parseBulkProbeLine(line: string, lineNumber: number, system: string) {
|
|||||||
export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [bulkOpen, setBulkOpen] = 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 [bulkInput, setBulkInput] = useState("")
|
||||||
const [bulkLoading, setBulkLoading] = useState(false)
|
const [bulkLoading, setBulkLoading] = useState(false)
|
||||||
const [selectedSystemId, setSelectedSystemId] = useState("")
|
|
||||||
const [bulkSelectedSystemId, setBulkSelectedSystemId] = useState("")
|
const [bulkSelectedSystemId, setBulkSelectedSystemId] = useState("")
|
||||||
const systems = useStore($systems)
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const targetName = target.replace(/^https?:\/\//, "")
|
const systems = useStore($systems)
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setProtocol("icmp")
|
|
||||||
setTarget("")
|
|
||||||
setPort("")
|
|
||||||
setProbeInterval("30")
|
|
||||||
setName("")
|
|
||||||
setSelectedSystemId("")
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetBulkForm = () => {
|
const resetBulkForm = () => {
|
||||||
setBulkInput("")
|
setBulkInput("")
|
||||||
setBulkSelectedSystemId("")
|
setBulkSelectedSystemId("")
|
||||||
}
|
}
|
||||||
|
|
||||||
const openBulkAdd = () => {
|
const openBulkAdd = (selectedSystemId?: string) => {
|
||||||
if (!systemId && selectedSystemId) {
|
if (!systemId && selectedSystemId) {
|
||||||
setBulkSelectedSystemId(selectedSystemId)
|
setBulkSelectedSystemId(selectedSystemId)
|
||||||
}
|
}
|
||||||
@@ -147,29 +133,6 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
setOpen(true)
|
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) {
|
async function handleBulkSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setBulkLoading(true)
|
setBulkLoading(true)
|
||||||
@@ -227,119 +190,31 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={openBulkAdd}>
|
<DropdownMenuItem onClick={() => openBulkAdd(systemId)}>
|
||||||
<ListIcon className="size-4 me-2" />
|
<ListIcon className="size-4 me-2" />
|
||||||
<Trans>Bulk Add</Trans>
|
<Trans>Bulk Add</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog
|
||||||
<DialogContent className="max-w-md">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={(nextOpen) => {
|
||||||
<DialogTitle>
|
setOpen(nextOpen)
|
||||||
<Trans>Add {{ foo: t`Network Probe` }}</Trans>
|
}}
|
||||||
</DialogTitle>
|
>
|
||||||
<DialogDescription>
|
{open && <ProbeDialogContent setOpen={setOpen} systemId={systemId} onOpenBulkAdd={openBulkAdd} />}
|
||||||
<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>
|
</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">
|
<SheetContent className="w-full sm:max-w-xl gap-0">
|
||||||
<SheetHeader className="border-b">
|
<SheetHeader className="border-b">
|
||||||
<SheetTitle>
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user