mirror of
https://github.com/henrygd/beszel.git
synced 2026-05-06 10:51:50 +02:00
update
This commit is contained in:
@@ -62,16 +62,19 @@ func bindNetworkProbesEvents(hub *Hub) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := e.Next()
|
err := e.Next()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.Record.GetBool("enabled") {
|
if e.Record.GetBool("enabled") {
|
||||||
_, err = hub.upsertNetworkProbe(e.Record, false)
|
var result *probe.Result
|
||||||
|
runNow := !e.Record.Original().GetBool("enabled")
|
||||||
|
result, err = hub.upsertNetworkProbe(e.Record, runNow)
|
||||||
|
if result != nil {
|
||||||
|
setProbeResultFields(e.Record, *result)
|
||||||
|
_ = e.App.SaveNoValidate(e.Record)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
err = hub.deleteNetworkProbe(e.Record)
|
err = hub.deleteNetworkProbe(e.Record)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hub.Logger().Warn("failed to sync updated probe to agent", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
hub.Logger().Warn("failed to sync updated probe", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import {
|
|||||||
NetworkIcon,
|
NetworkIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
|
PauseCircleIcon,
|
||||||
|
PlayCircleIcon,
|
||||||
} 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, SystemRecord } from "@/types"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -25,9 +27,12 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu"
|
} 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 "@/components/ui/use-toast"
|
||||||
import { $allSystemsById } from "@/lib/stores"
|
import { $allSystemsById } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
|
import { SystemStatus } from "@/lib/enums"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
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",
|
||||||
@@ -43,12 +48,54 @@ async function deleteProbe(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setProbeEnabled(id: string, enabled: boolean) {
|
||||||
|
try {
|
||||||
|
await pb.collection("network_probes").update(id, { enabled })
|
||||||
|
} catch (err: unknown) {
|
||||||
|
toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_COLORS = {
|
||||||
|
[SystemStatus.Up]: "bg-green-500",
|
||||||
|
[SystemStatus.Down]: "bg-red-500",
|
||||||
|
[SystemStatus.Paused]: "bg-primary/40",
|
||||||
|
[SystemStatus.Pending]: "bg-yellow-500",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A probe is considered muted if it's disabled or if its associated system is not up.
|
||||||
|
*/
|
||||||
|
const isMuted = (record: NetworkProbeRecord, systemRecord: SystemRecord | undefined) =>
|
||||||
|
!record.enabled || systemRecord?.status !== SystemStatus.Up
|
||||||
|
|
||||||
export function getProbeColumns(
|
export function getProbeColumns(
|
||||||
longestName = 0,
|
longestName = 0,
|
||||||
longestTarget = 0,
|
longestTarget = 0,
|
||||||
onEdit?: (probe: NetworkProbeRecord) => void
|
onEdit?: (probe: NetworkProbeRecord) => void
|
||||||
): ColumnDef<NetworkProbeRecord>[] {
|
): ColumnDef<NetworkProbeRecord>[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
className="ms-2"
|
||||||
|
checked={table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && "indeterminate")}
|
||||||
|
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
||||||
|
aria-label={t`Select all`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label={t`Select row`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
size: 44,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "name",
|
id: "name",
|
||||||
sortingFn: (a, b) => (a.original.name || a.original.target).localeCompare(b.original.name || b.original.target),
|
sortingFn: (a, b) => (a.original.name || a.original.target).localeCompare(b.original.name || b.original.target),
|
||||||
@@ -71,8 +118,19 @@ export function getProbeColumns(
|
|||||||
},
|
},
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const allSystems = useStore($allSystemsById)
|
const system = useStore($allSystemsById)[getValue() as string] as SystemRecord | undefined
|
||||||
return <span className="ms-1.5 xl:w-20 block truncate">{allSystems[getValue() as string]?.name ?? ""}</span>
|
const name = system?.name
|
||||||
|
const status = system?.status as SystemStatus // undefined val is fine but makes lsp mad
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => (
|
||||||
|
<span className="ms-1.5 xl:w-20 truncate flex items-center gap-2">
|
||||||
|
<span className={cn("shrink-0 size-2 rounded-full", STATUS_COLORS[status])} />
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
[status, name]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,12 +197,18 @@ export function getProbeColumns(
|
|||||||
invertSorting: true,
|
invertSorting: true,
|
||||||
header: ({ column }) => <HeaderButton column={column} name={t`Loss 1h`} Icon={WifiOffIcon} />,
|
header: ({ column }) => <HeaderButton column={column} name={t`Loss 1h`} Icon={WifiOffIcon} />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { loss1h, res } = row.original
|
const { loss1h, res, system } = row.original
|
||||||
|
const systemRecord = useStore($allSystemsById)[system]
|
||||||
|
|
||||||
if (loss1h === undefined || (!res && !loss1h)) {
|
if (loss1h === undefined || (!res && !loss1h)) {
|
||||||
return <span className="ms-1.5 text-muted-foreground">-</span>
|
return <span className="ms-1.5 text-muted-foreground">-</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const muted = isMuted(row.original, systemRecord)
|
||||||
let color = "bg-green-500"
|
let color = "bg-green-500"
|
||||||
if (loss1h) {
|
if (muted) {
|
||||||
|
color = "bg-muted-foreground/50"
|
||||||
|
} else if (loss1h) {
|
||||||
color = loss1h > 20 ? "bg-red-500" : "bg-yellow-500"
|
color = loss1h > 20 ? "bg-red-500" : "bg-yellow-500"
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -171,64 +235,82 @@ export function getProbeColumns(
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
header: () => null,
|
header: () => null,
|
||||||
size: 40,
|
size: 40,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<DropdownMenu>
|
const { enabled } = row.original
|
||||||
<DropdownMenuTrigger asChild>
|
return (
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="ghost"
|
<DropdownMenuTrigger asChild>
|
||||||
size="icon"
|
<Button
|
||||||
className="size-10"
|
variant="ghost"
|
||||||
onClick={(event) => event.stopPropagation()}
|
size="icon"
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
className="size-10"
|
||||||
>
|
onClick={(event) => event.stopPropagation()}
|
||||||
<span className="sr-only">
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
<Trans>Open menu</Trans>
|
>
|
||||||
</span>
|
<span className="sr-only">
|
||||||
<MoreHorizontalIcon className="w-5" />
|
<Trans>Open menu</Trans>
|
||||||
</Button>
|
</span>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontalIcon className="w-5" />
|
||||||
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
|
</Button>
|
||||||
<DropdownMenuItem
|
</DropdownMenuTrigger>
|
||||||
onClick={(event) => {
|
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
|
||||||
event.stopPropagation()
|
<DropdownMenuItem onClick={() => onEdit?.(row.original)}>
|
||||||
onEdit?.(row.original)
|
<PenBoxIcon className="me-2.5 size-4" />
|
||||||
}}
|
<Trans>Edit</Trans>
|
||||||
>
|
</DropdownMenuItem>
|
||||||
<PenBoxIcon className="me-2.5 size-4" />
|
<DropdownMenuItem onClick={() => setProbeEnabled(row.original.id, !enabled)}>
|
||||||
<Trans>Edit</Trans>
|
{enabled ? (
|
||||||
</DropdownMenuItem>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<PauseCircleIcon className="me-2.5 size-4" />
|
||||||
<DropdownMenuItem
|
<Trans>Pause</Trans>
|
||||||
onClick={(event) => {
|
</>
|
||||||
event.stopPropagation()
|
) : (
|
||||||
deleteProbe(row.original.id)
|
<>
|
||||||
}}
|
<PlayCircleIcon className="me-2.5 size-4" />
|
||||||
>
|
<Trans>Resume</Trans>
|
||||||
<Trash2Icon className="me-2.5 size-4" />
|
</>
|
||||||
<Trans>Delete</Trans>
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenu>
|
<DropdownMenuItem
|
||||||
),
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
deleteProbe(row.original.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon className="me-2.5 size-4" />
|
||||||
|
<Trans>Delete</Trans>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
function responseTimeCell(cell: CellContext<NetworkProbeRecord, unknown>) {
|
function responseTimeCell(cell: CellContext<NetworkProbeRecord, unknown>) {
|
||||||
const val = cell.getValue() as number | undefined
|
const probe = cell.row.original
|
||||||
if (!val) {
|
const systemRecord = useStore($allSystemsById)[probe.system]
|
||||||
|
const responseTime = cell.getValue() as number | undefined
|
||||||
|
|
||||||
|
if (!responseTime) {
|
||||||
return <span className="ms-1.5 text-muted-foreground">-</span>
|
return <span className="ms-1.5 text-muted-foreground">-</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const muted = isMuted(probe, systemRecord)
|
||||||
let color = "bg-green-500"
|
let color = "bg-green-500"
|
||||||
if (val > 200) {
|
if (muted) {
|
||||||
|
color = "bg-muted-foreground/50"
|
||||||
|
} else if (responseTime > 200) {
|
||||||
color = "bg-yellow-500"
|
color = "bg-yellow-500"
|
||||||
}
|
}
|
||||||
if (val > 2000) {
|
if (!muted && responseTime > 2000) {
|
||||||
color = "bg-red-500"
|
color = "bg-red-500"
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="ms-1.5 tabular-nums flex gap-2 items-center">
|
<span className="ms-1.5 tabular-nums flex gap-2 items-center">
|
||||||
<span className={cn("shrink-0 size-2 rounded-full", color)} />
|
<span className={cn("shrink-0 size-2 rounded-full", color)} />
|
||||||
{decimalString(val, val < 100 ? 2 : 1).toLocaleString()}ms
|
{decimalString(responseTime, responseTime < 100 ? 2 : 1).toLocaleString()}ms
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { t } from "@lingui/core/macro"
|
|||||||
import { Trans } from "@lingui/react/macro"
|
import { Trans } from "@lingui/react/macro"
|
||||||
import {
|
import {
|
||||||
type ColumnFiltersState,
|
type ColumnFiltersState,
|
||||||
type ColumnDef,
|
|
||||||
flexRender,
|
flexRender,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
@@ -30,7 +29,6 @@ import { Button, buttonVariants } from "@/components/ui/button"
|
|||||||
import { memo, useMemo, useRef, useState } from "react"
|
import { memo, useMemo, useRef, useState } from "react"
|
||||||
import { getProbeColumns } from "@/components/network-probes-table/network-probes-columns"
|
import { getProbeColumns } from "@/components/network-probes-table/network-probes-columns"
|
||||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
@@ -78,34 +76,8 @@ export default function NetworkProbesTableNew({
|
|||||||
let columns = getProbeColumns(longestName, longestTarget, setEditingProbe)
|
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) {
|
return columns
|
||||||
return columns
|
}, [systemId, longestName, longestTarget])
|
||||||
}
|
|
||||||
|
|
||||||
const selectionColumn: ColumnDef<NetworkProbeRecord> = {
|
|
||||||
id: "select",
|
|
||||||
header: ({ table }) => (
|
|
||||||
<Checkbox
|
|
||||||
className="ms-2"
|
|
||||||
checked={table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && "indeterminate")}
|
|
||||||
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
|
||||||
aria-label={t`Select all`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
||||||
aria-label={t`Select row`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
size: 44,
|
|
||||||
}
|
|
||||||
|
|
||||||
return [selectionColumn, ...columns]
|
|
||||||
}, [systemId, longestName, longestTarget, canManageProbes])
|
|
||||||
|
|
||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async () => {
|
||||||
setDeleteOpen(false)
|
setDeleteOpen(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user