mirror of
https://github.com/henrygd/beszel.git
synced 2026-05-06 10:51:50 +02:00
updates
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordManager struct {
|
type RecordManager struct {
|
||||||
@@ -40,7 +41,7 @@ type StatsRecord struct {
|
|||||||
|
|
||||||
// Create longer records by averaging shorter records
|
// Create longer records by averaging shorter records
|
||||||
func (rm *RecordManager) CreateLongerRecords() {
|
func (rm *RecordManager) CreateLongerRecords() {
|
||||||
// start := time.Now()
|
now := time.Now().UTC()
|
||||||
longerRecordData := []LongerRecordData{
|
longerRecordData := []LongerRecordData{
|
||||||
{
|
{
|
||||||
shorterType: "1m",
|
shorterType: "1m",
|
||||||
@@ -71,6 +72,7 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
// wrap the operations in a transaction
|
// wrap the operations in a transaction
|
||||||
rm.app.RunInTransaction(func(txApp core.App) error {
|
rm.app.RunInTransaction(func(txApp core.App) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
collections := [3]*core.Collection{}
|
collections := [3]*core.Collection{}
|
||||||
collections[0], err = txApp.FindCachedCollectionByNameOrId("system_stats")
|
collections[0], err = txApp.FindCachedCollectionByNameOrId("system_stats")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,49 +98,64 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
recordData := longerRecordData[i]
|
recordData := longerRecordData[i]
|
||||||
// log.Println("processing longer record type", recordData.longerType)
|
// log.Println("processing longer record type", recordData.longerType)
|
||||||
// add one minute padding for longer records because they are created slightly later than the job start time
|
// add one minute padding for longer records because they are created slightly later than the job start time
|
||||||
longerRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration + time.Minute)
|
longerRecordPeriod := now.Add(recordData.longerTimeDuration + time.Minute)
|
||||||
// shorter records are created independently of longer records, so we shouldn't need to add padding
|
// shorter records are created independently of longer records, so we shouldn't need to add padding
|
||||||
shorterRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration)
|
shorterRecordPeriod := now.Add(recordData.longerTimeDuration)
|
||||||
// loop through both collections
|
// loop through both collections
|
||||||
for _, collection := range collections {
|
for _, collection := range collections {
|
||||||
// check creation time of last longer record if not 10m, since 10m is created every run
|
// check creation time of last longer record if not 10m, since 10m is created every run
|
||||||
if recordData.longerType != "10m" {
|
if recordData.longerType != "10m" {
|
||||||
count, err := txApp.CountRecords(
|
var existingRecord struct {
|
||||||
collection.Id,
|
Id string
|
||||||
dbx.NewExp(
|
}
|
||||||
"system = {:system} AND type = {:type} AND created > {:created}",
|
|
||||||
dbx.Params{"type": recordData.longerType, "system": system.Id, "created": longerRecordPeriod},
|
params := dbx.Params{
|
||||||
),
|
"type": recordData.longerType,
|
||||||
)
|
"system": system.Id,
|
||||||
|
"created": getCreatedTimeField(collection.Name, longerRecordPeriod),
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = db.Select("id").
|
||||||
|
From(collection.Name).
|
||||||
|
Where(dbx.NewExp("system = {:system} AND type = {:type} AND created > {:created}", params)).
|
||||||
|
Limit(1).
|
||||||
|
One(&existingRecord)
|
||||||
|
|
||||||
// continue if longer record exists
|
// continue if longer record exists
|
||||||
if err != nil || count > 0 {
|
if existingRecord.Id != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// get shorter records from the past x minutes
|
// get shorter records from the past x minutes
|
||||||
var recordIds RecordIds
|
var recordIds RecordIds
|
||||||
|
|
||||||
err := txApp.DB().
|
params := dbx.Params{
|
||||||
Select("id").
|
|
||||||
From(collection.Name).
|
|
||||||
AndWhere(dbx.NewExp(
|
|
||||||
"system={:system} AND type={:type} AND created > {:created}",
|
|
||||||
dbx.Params{
|
|
||||||
"type": recordData.shorterType,
|
"type": recordData.shorterType,
|
||||||
"system": system.Id,
|
"system": system.Id,
|
||||||
"created": shorterRecordPeriod,
|
"created": getCreatedTimeField(collection.Name, shorterRecordPeriod),
|
||||||
},
|
}
|
||||||
|
|
||||||
|
_ = txApp.DB().
|
||||||
|
Select("id").
|
||||||
|
From(collection.Name).
|
||||||
|
Where(dbx.NewExp(
|
||||||
|
"system={:system} AND type={:type} AND created > {:created}",
|
||||||
|
params,
|
||||||
)).
|
)).
|
||||||
All(&recordIds)
|
All(&recordIds)
|
||||||
|
|
||||||
// continue if not enough shorter records
|
// continue if not enough shorter records
|
||||||
if err != nil || len(recordIds) < recordData.minShorterRecords {
|
if len(recordIds) < recordData.minShorterRecords {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// average the shorter records and create longer record
|
// average the shorter records and create longer record
|
||||||
longerRecord := core.NewRecord(collection)
|
longerRecord := core.NewRecord(collection)
|
||||||
longerRecord.Set("system", system.Id)
|
longerRecord.Set("system", system.Id)
|
||||||
longerRecord.Set("type", recordData.longerType)
|
longerRecord.Set("type", recordData.longerType)
|
||||||
|
// network_probe_stats uses created as unix timestamp in milliseconds, so we need to set it manually here instead of relying on the default created field
|
||||||
|
if collection.Name == "network_probe_stats" {
|
||||||
|
longerRecord.Set("created", now.UnixMilli())
|
||||||
|
}
|
||||||
switch collection.Name {
|
switch collection.Name {
|
||||||
case "system_stats":
|
case "system_stats":
|
||||||
longerRecord.Set("stats", rm.AverageSystemStats(db, recordIds))
|
longerRecord.Set("stats", rm.AverageSystemStats(db, recordIds))
|
||||||
@@ -160,6 +177,13 @@ func (rm *RecordManager) CreateLongerRecords() {
|
|||||||
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
|
// log.Println("finished creating longer records", "time (ms)", time.Since(start).Milliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCreatedTimeField(collectionName string, period time.Time) any {
|
||||||
|
if collectionName == "network_probe_stats" {
|
||||||
|
return period.UnixMilli()
|
||||||
|
}
|
||||||
|
return period.Format(types.DefaultDateLayout)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the average stats of a list of system_stats records without reflect
|
// Calculate the average stats of a list of system_stats records without reflect
|
||||||
func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *system.Stats {
|
func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *system.Stats {
|
||||||
stats := make([]system.Stats, 0, len(records))
|
stats := make([]system.Stats, 0, len(records))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package records
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
@@ -75,26 +74,19 @@ func deleteOldSystemStats(app core.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
db := app.DB()
|
||||||
|
|
||||||
for _, collection := range collections {
|
for _, collection := range collections {
|
||||||
// Build the WHERE clause
|
query := db.Delete(collection, dbx.NewExp("type={:type} AND created<{:created}"))
|
||||||
var conditionParts []string
|
for _, rd := range recordData {
|
||||||
var params dbx.Params = make(map[string]any)
|
if _, err := query.Bind(dbx.Params{
|
||||||
for i := range recordData {
|
"type": rd.recordType,
|
||||||
rd := recordData[i]
|
"created": getCreatedTimeField(collection, now.Add(-rd.retention)),
|
||||||
// Create parameterized condition for this record type
|
}).Execute(); err != nil {
|
||||||
dateParam := fmt.Sprintf("date%d", i)
|
|
||||||
conditionParts = append(conditionParts, fmt.Sprintf("(type = '%s' AND created < {:%s})", rd.recordType, dateParam))
|
|
||||||
params[dateParam] = now.Add(-rd.retention)
|
|
||||||
}
|
|
||||||
// Combine conditions with OR
|
|
||||||
conditionStr := strings.Join(conditionParts, " OR ")
|
|
||||||
// Construct and execute the full raw query
|
|
||||||
rawQuery := fmt.Sprintf("DELETE FROM %s WHERE %s", collection, conditionStr)
|
|
||||||
if _, err := app.DB().NewQuery(rawQuery).Bind(params).Execute(); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete from %s: %v", collection, err)
|
return fmt.Errorf("failed to delete from %s: %v", collection, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function AreaChartDefault({
|
|||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||||
const sourceData = customData ?? chartData.systemStats
|
const sourceData = customData ?? chartData.systemStats ?? []
|
||||||
const [displayData, setDisplayData] = useState(sourceData)
|
const [displayData, setDisplayData] = useState(sourceData)
|
||||||
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
|
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function LineChartDefault({
|
|||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||||
const sourceData = customData ?? chartData.systemStats
|
const sourceData = customData ?? chartData.systemStats ?? []
|
||||||
const [displayData, setDisplayData] = useState(sourceData)
|
const [displayData, setDisplayData] = useState(sourceData)
|
||||||
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
|
const [displayMaxToggled, setDisplayMaxToggled] = useState(maxToggled)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export function getProbeColumns(
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
className="ms-2"
|
className="ms-2"
|
||||||
checked={table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && "indeterminate")}
|
checked={table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && "indeterminate")}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
|
||||||
aria-label={t`Select all`}
|
aria-label={t`Select all`}
|
||||||
/>
|
/>
|
||||||
@@ -78,6 +79,7 @@ export function getProbeColumns(
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
aria-label={t`Select row`}
|
aria-label={t`Select row`}
|
||||||
/>
|
/>
|
||||||
@@ -264,14 +266,24 @@ export function getProbeColumns(
|
|||||||
<MoreHorizontalIcon className="w-5" />
|
<MoreHorizontalIcon className="w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" onClick={(event) => event.stopPropagation()}>
|
||||||
{!isBulkAction && (
|
{!isBulkAction && (
|
||||||
<DropdownMenuItem onClick={() => onEdit?.(row.original)}>
|
<DropdownMenuItem
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onEdit?.(row.original)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PenBoxIcon className="me-2.5 size-4" />
|
<PenBoxIcon className="me-2.5 size-4" />
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem onClick={() => onSetEnabled?.(actionRows, !shouldPause)}>
|
<DropdownMenuItem
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
onSetEnabled?.(actionRows, !shouldPause)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{shouldPause ? (
|
{shouldPause ? (
|
||||||
<>
|
<>
|
||||||
<PauseCircleIcon className="me-2.5 size-4" />
|
<PauseCircleIcon className="me-2.5 size-4" />
|
||||||
@@ -286,7 +298,8 @@ export function getProbeColumns(
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
onDelete?.(actionRows)
|
onDelete?.(actionRows)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,11 +33,19 @@ import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/compon
|
|||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
import { isReadOnlyUser } from "@/lib/api"
|
import { isReadOnlyUser } from "@/lib/api"
|
||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
import { $allSystemsById } from "@/lib/stores"
|
import { $allSystemsById, $chartTime, $direction } from "@/lib/stores"
|
||||||
import { cn, useBrowserStorage } from "@/lib/utils"
|
import { cn, useBrowserStorage } from "@/lib/utils"
|
||||||
import type { NetworkProbeRecord } from "@/types"
|
import type { NetworkProbeRecord } from "@/types"
|
||||||
import { AddProbeDialog, EditProbeDialog } from "./probe-dialog"
|
import { AddProbeDialog, EditProbeDialog } from "./probe-dialog"
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react"
|
||||||
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||||
|
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||||
|
import { ResponseChart, LossChart } from "@/components/routes/system/charts/probes-charts"
|
||||||
|
import { useNetworkProbeStats } from "@/lib/use-network-probes"
|
||||||
|
import { useStore } from "@nanostores/react"
|
||||||
|
import type { ChartData } from "@/types"
|
||||||
|
import { parseSemVer } from "@/lib/utils"
|
||||||
|
import { Separator } from "../ui/separator"
|
||||||
|
|
||||||
export default function NetworkProbesTableNew({
|
export default function NetworkProbesTableNew({
|
||||||
systemId,
|
systemId,
|
||||||
@@ -325,6 +333,13 @@ const NetworkProbesTable = memo(function NetworkProbeTable({
|
|||||||
}) {
|
}) {
|
||||||
// The virtualizer will need a reference to the scrollable container element
|
// The virtualizer will need a reference to the scrollable container element
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
|
const [activeProbeId, setActiveProbeId] = useState<string | null>(null)
|
||||||
|
const activeProbe = activeProbeId ? table.options.data.find((probe) => probe.id === activeProbeId) : undefined
|
||||||
|
const openSheet = useCallback((probe: NetworkProbeRecord) => {
|
||||||
|
setActiveProbeId(probe.id)
|
||||||
|
setSheetOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
||||||
count: rows.length,
|
count: rows.length,
|
||||||
@@ -360,6 +375,7 @@ const NetworkProbesTable = memo(function NetworkProbeTable({
|
|||||||
row={row}
|
row={row}
|
||||||
virtualRow={virtualRow}
|
virtualRow={virtualRow}
|
||||||
isSelected={row.getIsSelected()}
|
isSelected={row.getIsSelected()}
|
||||||
|
openSheet={openSheet}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -373,6 +389,13 @@ const NetworkProbesTable = memo(function NetworkProbeTable({
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<NetworkProbeSheet
|
||||||
|
open={sheetOpen}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
setSheetOpen(nextOpen)
|
||||||
|
}}
|
||||||
|
probe={activeProbe}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -399,13 +422,19 @@ const NetworkProbeTableRow = memo(function NetworkProbeTableRow({
|
|||||||
row,
|
row,
|
||||||
virtualRow,
|
virtualRow,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
openSheet,
|
||||||
}: {
|
}: {
|
||||||
row: Row<NetworkProbeRecord>
|
row: Row<NetworkProbeRecord>
|
||||||
virtualRow: VirtualItem
|
virtualRow: VirtualItem
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
openSheet: (probe: NetworkProbeRecord) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TableRow data-state={isSelected && "selected"} className="transition-opacity">
|
<TableRow
|
||||||
|
data-state={isSelected && "selected"}
|
||||||
|
className="cursor-pointer transition-opacity"
|
||||||
|
onClick={() => openSheet(row.original)}
|
||||||
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
@@ -421,3 +450,88 @@ const NetworkProbeTableRow = memo(function NetworkProbeTableRow({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function NetworkProbeSheet({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
probe,
|
||||||
|
}: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
probe?: NetworkProbeRecord
|
||||||
|
}) {
|
||||||
|
if (!probe) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NetworkProbeSheetContent key={probe.system} open={open} onOpenChange={onOpenChange} probe={probe} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function NetworkProbeSheetContent({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
probe,
|
||||||
|
}: {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
probe: NetworkProbeRecord
|
||||||
|
}) {
|
||||||
|
const chartTime = useStore($chartTime)
|
||||||
|
const direction = useStore($direction)
|
||||||
|
const system = useStore($allSystemsById)[probe.system]
|
||||||
|
|
||||||
|
const probeStats = useNetworkProbeStats({ systemId: probe.system, chartTime })
|
||||||
|
|
||||||
|
const chartData = useMemo<ChartData>(
|
||||||
|
() => ({
|
||||||
|
agentVersion: parseSemVer(system?.info?.v),
|
||||||
|
orientation: direction === "rtl" ? "right" : "left",
|
||||||
|
chartTime,
|
||||||
|
}),
|
||||||
|
[chartTime]
|
||||||
|
)
|
||||||
|
const hasProbeStats = probeStats.some((record) => record.stats?.[probe.id] != null)
|
||||||
|
const probeLabel = probe.name || probe.target
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SheetContent className="w-full sm:max-w-220 overflow-auto p-4 sm:p-6">
|
||||||
|
<SheetHeader className="mb-0 border-b p-0 pb-4">
|
||||||
|
<SheetTitle>{probeLabel}</SheetTitle>
|
||||||
|
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
|
{system?.name ?? ""}
|
||||||
|
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||||
|
{probe.protocol.toUpperCase()}
|
||||||
|
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||||
|
{probe.target}
|
||||||
|
{probe.port > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
|
||||||
|
<span>{probe.port}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<ChartTimeSelect className="bg-card" agentVersion={chartData.agentVersion} />
|
||||||
|
<ResponseChart
|
||||||
|
probeStats={probeStats}
|
||||||
|
grid={false}
|
||||||
|
probes={[probe]}
|
||||||
|
chartData={chartData}
|
||||||
|
empty={!hasProbeStats}
|
||||||
|
showFilter={false}
|
||||||
|
/>
|
||||||
|
<LossChart
|
||||||
|
probeStats={probeStats}
|
||||||
|
grid={false}
|
||||||
|
probes={[probe]}
|
||||||
|
chartData={chartData}
|
||||||
|
empty={!hasProbeStats}
|
||||||
|
showFilter={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { memo, useEffect } from "react"
|
|||||||
import NetworkProbesTableNew from "@/components/network-probes-table/network-probes-table"
|
import NetworkProbesTableNew from "@/components/network-probes-table/network-probes-table"
|
||||||
import { ActiveAlerts } from "@/components/active-alerts"
|
import { ActiveAlerts } from "@/components/active-alerts"
|
||||||
import { FooterRepoLink } from "@/components/footer-repo-link"
|
import { FooterRepoLink } from "@/components/footer-repo-link"
|
||||||
import { useNetworkProbesData } from "@/lib/use-network-probes"
|
import { useNetworkProbes } from "@/lib/use-network-probes"
|
||||||
|
|
||||||
export default memo(() => {
|
export default memo(() => {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const { probes } = useNetworkProbesData({})
|
const probes = useNetworkProbes({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${t`Network Probes`} / Beszel`
|
document.title = `${t`Network Probes`} / Beszel`
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { getPbTimestamp, pb } from "@/lib/api"
|
import { getPbTimestamp, pb } from "@/lib/api"
|
||||||
import { chartTimeData } from "@/lib/utils"
|
import { chartTimeData } from "@/lib/utils"
|
||||||
import type { ChartData, ChartTimes, ContainerStatsRecord, NetworkProbeStatsRecord, SystemStatsRecord } from "@/types"
|
import type {
|
||||||
|
ChartData,
|
||||||
|
ChartDataContainer,
|
||||||
|
ChartTimes,
|
||||||
|
ContainerStatsRecord,
|
||||||
|
NetworkProbeStatsRecord,
|
||||||
|
SystemStatsRecord,
|
||||||
|
} from "@/types"
|
||||||
|
|
||||||
type ChartTimeData = {
|
type ChartTimeData = {
|
||||||
time: number
|
time: number
|
||||||
@@ -19,7 +26,7 @@ export const cache = new Map<
|
|||||||
/** Append new records onto prev with gap detection. Converts string `created` values to ms timestamps in place.
|
/** Append new records onto prev with gap detection. Converts string `created` values to ms timestamps in place.
|
||||||
* Pass `maxLen` to cap the result length in one copy instead of slicing again after the call. */
|
* Pass `maxLen` to cap the result length in one copy instead of slicing again after the call. */
|
||||||
export function appendData<T extends { created: string | number | null }>(
|
export function appendData<T extends { created: string | number | null }>(
|
||||||
prev: T[],
|
prev: T[] = [],
|
||||||
newRecords: T[],
|
newRecords: T[],
|
||||||
expectedInterval: number,
|
expectedInterval: number,
|
||||||
maxLen?: number
|
maxLen?: number
|
||||||
@@ -63,11 +70,11 @@ export async function getStats<T extends SystemStatsRecord | ContainerStatsRecor
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeContainerData(containers: ContainerStatsRecord[]): ChartData["containerData"] {
|
export function makeContainerData(containers: ContainerStatsRecord[]): ChartDataContainer[] {
|
||||||
const result = [] as ChartData["containerData"]
|
const result = [] as ChartDataContainer[]
|
||||||
for (const { created, stats } of containers) {
|
for (const { created, stats } of containers) {
|
||||||
if (!created) {
|
if (!created) {
|
||||||
result.push({ created: null } as ChartData["containerData"][0])
|
result.push({ created: null } as ChartDataContainer)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result.push(makeContainerPoint(new Date(created).getTime(), stats))
|
result.push(makeContainerPoint(new Date(created).getTime(), stats))
|
||||||
@@ -76,11 +83,8 @@ export function makeContainerData(containers: ContainerStatsRecord[]): ChartData
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Transform a single realtime container stats message into a ChartDataContainer point. */
|
/** Transform a single realtime container stats message into a ChartDataContainer point. */
|
||||||
export function makeContainerPoint(
|
export function makeContainerPoint(created: number, stats: ContainerStatsRecord["stats"]): ChartDataContainer {
|
||||||
created: number,
|
const point: ChartDataContainer = { created } as ChartDataContainer
|
||||||
stats: ContainerStatsRecord["stats"]
|
|
||||||
): ChartData["containerData"][0] {
|
|
||||||
const point: ChartData["containerData"][0] = { created } as ChartData["containerData"][0]
|
|
||||||
for (const container of stats) {
|
for (const container of stats) {
|
||||||
;(point as Record<string, unknown>)[container.n] = container
|
;(point as Record<string, unknown>)[container.n] = container
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ProbeChartProps = {
|
|||||||
probes: NetworkProbeRecord[]
|
probes: NetworkProbeRecord[]
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
empty: boolean
|
empty: boolean
|
||||||
|
showFilter?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProbeChartBaseProps = ProbeChartProps & {
|
type ProbeChartBaseProps = ProbeChartProps & {
|
||||||
@@ -39,8 +40,10 @@ function ProbeChart({
|
|||||||
tickFormatter,
|
tickFormatter,
|
||||||
contentFormatter,
|
contentFormatter,
|
||||||
domain,
|
domain,
|
||||||
|
showFilter = probes.length > 1,
|
||||||
}: ProbeChartBaseProps) {
|
}: ProbeChartBaseProps) {
|
||||||
const filter = useStore($filter)
|
const storedFilter = useStore($filter)
|
||||||
|
const filter = showFilter ? storedFilter : ""
|
||||||
|
|
||||||
const { dataPoints, visibleKeys } = useMemo(() => {
|
const { dataPoints, visibleKeys } = useMemo(() => {
|
||||||
const sortedProbes = [...probes].sort((a, b) => b.resAvg1h - a.resAvg1h)
|
const sortedProbes = [...probes].sort((a, b) => b.resAvg1h - a.resAvg1h)
|
||||||
@@ -78,12 +81,12 @@ function ProbeChart({
|
|||||||
return probeStats.filter((record) => visibleKeys.some((id) => record.stats?.[id] != null))
|
return probeStats.filter((record) => visibleKeys.some((id) => record.stats?.[id] != null))
|
||||||
}, [probeStats, visibleKeys])
|
}, [probeStats, visibleKeys])
|
||||||
|
|
||||||
const legend = dataPoints.length < 10
|
const legend = dataPoints.length < 10 && dataPoints.length > 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
legend={legend}
|
legend={legend || !showFilter}
|
||||||
cornerEl={<FilterBar store={$filter} />}
|
cornerEl={showFilter ? <FilterBar store={$filter} /> : undefined}
|
||||||
empty={empty}
|
empty={empty}
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
@@ -129,6 +132,30 @@ export function ResponseChart({ probeStats, grid, probes, chartData, empty }: Pr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MaxResponseChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) {
|
||||||
|
const { t } = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProbeChart
|
||||||
|
probeStats={probeStats}
|
||||||
|
grid={grid}
|
||||||
|
probes={probes}
|
||||||
|
chartData={chartData}
|
||||||
|
empty={empty}
|
||||||
|
valueIndex={0}
|
||||||
|
title={t`Response`}
|
||||||
|
description={t`Average response time`}
|
||||||
|
tickFormatter={(value) => formatMicroseconds(value, false)}
|
||||||
|
contentFormatter={({ value }) => {
|
||||||
|
if (typeof value !== "number") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return formatMicroseconds(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function LossChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) {
|
export function LossChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) {
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ResponseChart, LossChart } from "./charts/probes-charts"
|
|||||||
import type { SystemData } from "./use-system-data"
|
import type { SystemData } from "./use-system-data"
|
||||||
import { $chartTime } from "@/lib/stores"
|
import { $chartTime } from "@/lib/stores"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { useNetworkProbesData } from "@/lib/use-network-probes"
|
import { useNetworkProbes, useNetworkProbeStats } from "@/lib/use-network-probes"
|
||||||
|
|
||||||
const ContainersTable = lazy(() => import("../../containers-table/containers-table"))
|
const ContainersTable = lazy(() => import("../../containers-table/containers-table"))
|
||||||
|
|
||||||
@@ -56,7 +56,8 @@ function ProbesTable({ systemId, systemData }: { systemId: string; systemData: S
|
|||||||
const { grid, chartData } = systemData ?? {}
|
const { grid, chartData } = systemData ?? {}
|
||||||
const chartTime = useStore($chartTime)
|
const chartTime = useStore($chartTime)
|
||||||
|
|
||||||
const { probes, probeStats } = useNetworkProbesData({ systemId, loadStats: !!chartData, chartTime })
|
const probes = useNetworkProbes({ systemId })
|
||||||
|
const probeStats = useNetworkProbeStats({ systemId, chartTime })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export function useSystemData(id: string) {
|
|||||||
// derived values
|
// derived values
|
||||||
const isLongerChart = !["1m", "1h"].includes(chartTime)
|
const isLongerChart = !["1m", "1h"].includes(chartTime)
|
||||||
const showMax = maxValues && isLongerChart
|
const showMax = maxValues && isLongerChart
|
||||||
const dataEmpty = !chartLoading && chartData.systemStats.length === 0
|
const dataEmpty = !chartLoading && chartData.systemStats?.length === 0
|
||||||
const lastGpus = systemStats.at(-1)?.stats?.g
|
const lastGpus = systemStats.at(-1)?.stats?.g
|
||||||
const isPodman = details?.podman ?? system.info?.p ?? false
|
const isPodman = details?.podman ?? system.info?.p ?? false
|
||||||
|
|
||||||
|
|||||||
@@ -36,22 +36,15 @@ const NETWORK_PROBE_FIELDS =
|
|||||||
|
|
||||||
interface UseNetworkProbesProps {
|
interface UseNetworkProbesProps {
|
||||||
systemId?: string
|
systemId?: string
|
||||||
loadStats?: boolean
|
|
||||||
chartTime?: ChartTimes
|
|
||||||
existingProbes?: NetworkProbeRecord[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
export function useNetworkProbes(props: UseNetworkProbesProps) {
|
||||||
const { systemId, loadStats, chartTime, existingProbes } = props
|
const { systemId } = props
|
||||||
|
|
||||||
const [p, setProbes] = useState<NetworkProbeRecord[]>([])
|
const [probes, setProbes] = useState<NetworkProbeRecord[]>([])
|
||||||
const [probeStats, setProbeStats] = useState<NetworkProbeStatsRecord[]>([])
|
|
||||||
const statsRequestId = useRef(0)
|
|
||||||
const pendingProbeEvents = useRef(new Map<string, RecordSubscription<NetworkProbeRecord>>())
|
const pendingProbeEvents = useRef(new Map<string, RecordSubscription<NetworkProbeRecord>>())
|
||||||
const probeBatchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const probeBatchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const probes = existingProbes ?? p
|
|
||||||
|
|
||||||
// clear old data when systemId changes
|
// clear old data when systemId changes
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// return setProbes([])
|
// return setProbes([])
|
||||||
@@ -59,16 +52,11 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
|
|
||||||
// initial load - fetch probes if not provided by caller
|
// initial load - fetch probes if not provided by caller
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!existingProbes) {
|
|
||||||
fetchProbes(systemId).then((probes) => setProbes(probes))
|
fetchProbes(systemId).then((probes) => setProbes(probes))
|
||||||
}
|
|
||||||
}, [systemId])
|
}, [systemId])
|
||||||
|
|
||||||
// Subscribe to updates if probes not provided by caller
|
// Subscribe to updates if probes not provided by caller
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (existingProbes) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let unsubscribe: (() => void) | undefined
|
let unsubscribe: (() => void) | undefined
|
||||||
|
|
||||||
function flushPendingProbeEvents() {
|
function flushPendingProbeEvents() {
|
||||||
@@ -115,9 +103,22 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
}
|
}
|
||||||
}, [systemId])
|
}, [systemId])
|
||||||
|
|
||||||
|
return probes
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseNetworkProbeStatsProps {
|
||||||
|
systemId?: string
|
||||||
|
chartTime: ChartTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNetworkProbeStats(props: UseNetworkProbeStatsProps) {
|
||||||
|
const { systemId, chartTime } = props
|
||||||
|
const [probeStats, setProbeStats] = useState<NetworkProbeStatsRecord[]>([])
|
||||||
|
const requestID = useRef(0)
|
||||||
|
|
||||||
// Subscribe to new probe stats
|
// Subscribe to new probe stats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadStats || !systemId) {
|
if (!systemId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let unsubscribe: (() => void) | undefined
|
let unsubscribe: (() => void) | undefined
|
||||||
@@ -175,12 +176,12 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
|
|
||||||
// fetch missing probe stats on load and when chart time changes
|
// fetch missing probe stats on load and when chart time changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadStats || !systemId || !chartTime || chartTime === "1m") {
|
if (!systemId || !chartTime || chartTime === "1m") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { expectedInterval } = chartTimeData[chartTime]
|
const { expectedInterval } = chartTimeData[chartTime]
|
||||||
const requestId = ++statsRequestId.current
|
const requestId = ++requestID.current
|
||||||
|
|
||||||
const cachedProbeStats = getCacheValue(systemId, chartTime)
|
const cachedProbeStats = getCacheValue(systemId, chartTime)
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
getStats<NetworkProbeStatsRecord>("network_probe_stats", systemId, chartTime, cachedProbeStats, true).then(
|
getStats<NetworkProbeStatsRecord>("network_probe_stats", systemId, chartTime, cachedProbeStats, true).then(
|
||||||
(probeStats) => {
|
(probeStats) => {
|
||||||
// If another request has been made since this one, ignore the results
|
// If another request has been made since this one, ignore the results
|
||||||
if (requestId !== statsRequestId.current) {
|
if (requestId !== requestID.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newStats = appendCacheValue(systemId, chartTime, probeStats)
|
const newStats = appendCacheValue(systemId, chartTime, probeStats)
|
||||||
@@ -209,7 +210,7 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
|
|
||||||
// subscribe to realtime metrics if chart time is 1m
|
// subscribe to realtime metrics if chart time is 1m
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadStats || !systemId || chartTime !== "1m") {
|
if (!systemId || chartTime !== "1m") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let unsubscribe: (() => void) | undefined
|
let unsubscribe: (() => void) | undefined
|
||||||
@@ -238,12 +239,8 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
return () => unsubscribe?.()
|
return () => unsubscribe?.()
|
||||||
}, [chartTime, systemId])
|
}, [chartTime, systemId])
|
||||||
|
|
||||||
return {
|
return probeStats
|
||||||
probes,
|
|
||||||
probeStats,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// function probesToStats(probes: NetworkProbeRecord[]): NetworkProbeStatsRecord["stats"] {
|
// function probesToStats(probes: NetworkProbeRecord[]): NetworkProbeStatsRecord["stats"] {
|
||||||
// const stats: NetworkProbeStatsRecord["stats"] = {}
|
// const stats: NetworkProbeStatsRecord["stats"] = {}
|
||||||
// for (const probe of probes) {
|
// for (const probe of probes) {
|
||||||
|
|||||||
4
internal/site/src/types.d.ts
vendored
4
internal/site/src/types.d.ts
vendored
@@ -313,8 +313,8 @@ export interface SemVer {
|
|||||||
|
|
||||||
export interface ChartData {
|
export interface ChartData {
|
||||||
agentVersion: SemVer
|
agentVersion: SemVer
|
||||||
systemStats: SystemStatsRecord[]
|
systemStats?: SystemStatsRecord[]
|
||||||
containerData: ChartDataContainer[]
|
containerData?: ChartDataContainer[]
|
||||||
orientation: "right" | "left"
|
orientation: "right" | "left"
|
||||||
chartTime: ChartTimes
|
chartTime: ChartTimes
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user