mirror of
https://github.com/henrygd/beszel.git
synced 2026-05-06 10:51:50 +02:00
update
This commit is contained in:
@@ -31,6 +31,12 @@ func bindNetworkProbesEvents(hub *Hub) {
|
|||||||
if !e.Record.GetBool("enabled") {
|
if !e.Record.GetBool("enabled") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// if system connected, run the probe immediately
|
||||||
|
// if not, return and wait for the system to connect and sync probes then
|
||||||
|
system, err := hub.sm.GetSystem(e.Record.GetString("system"))
|
||||||
|
if err != nil || system.Status != "up" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result, err := hub.upsertNetworkProbe(e.Record, true)
|
result, err := hub.upsertNetworkProbe(e.Record, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hub.Logger().Warn("failed to sync probe to agent", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
hub.Logger().Warn("failed to sync probe to agent", "system", e.Record.GetString("system"), "probe", e.Record.Id, "err", err)
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export default function AreaChartDefault({
|
|||||||
})
|
})
|
||||||
}, [areasKey, displayMaxToggled])
|
}, [areasKey, displayMaxToggled])
|
||||||
|
|
||||||
|
const XAxis = xAxis(chartData.chartTime, displayData.at(-1)?.created)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (displayData.length === 0) {
|
if (displayData.length === 0) {
|
||||||
return null
|
return null
|
||||||
@@ -146,7 +148,7 @@ export default function AreaChartDefault({
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{xAxis(chartData.chartTime, displayData.at(-1)?.created as number)}
|
{XAxis}
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
@@ -167,5 +169,5 @@ export default function AreaChartDefault({
|
|||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)
|
)
|
||||||
}, [displayData, yAxisWidth, filter, Areas])
|
}, [displayData, yAxisWidth, filter, Areas, XAxis])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default function LineChartDefault({
|
|||||||
truncate = false,
|
truncate = false,
|
||||||
chartProps,
|
chartProps,
|
||||||
connectNulls,
|
connectNulls,
|
||||||
|
dot = false,
|
||||||
}: {
|
}: {
|
||||||
chartData: ChartData
|
chartData: ChartData
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
// biome-ignore lint/suspicious/noExplicitAny: accepts different data source types (systemStats or containerData)
|
||||||
@@ -64,6 +65,7 @@ export default function LineChartDefault({
|
|||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
|
chartProps?: Omit<React.ComponentProps<typeof LineChart>, "data" | "margin">
|
||||||
connectNulls?: boolean
|
connectNulls?: boolean
|
||||||
|
dot?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
|
||||||
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
const { isIntersecting, ref } = useIntersectionObserver({ freeze: false })
|
||||||
@@ -87,6 +89,8 @@ export default function LineChartDefault({
|
|||||||
// Use a stable key derived from data point identities and visual properties
|
// Use a stable key derived from data point identities and visual properties
|
||||||
const linesKey = dataPoints?.map((d) => `${d.label}:${d.strokeOpacity ?? ""}`).join("\0")
|
const linesKey = dataPoints?.map((d) => `${d.label}:${d.strokeOpacity ?? ""}`).join("\0")
|
||||||
|
|
||||||
|
const XAxis = xAxis(chartData.chartTime, displayData.at(-1)?.created)
|
||||||
|
|
||||||
const Lines = useMemo(() => {
|
const Lines = useMemo(() => {
|
||||||
return dataPoints?.map((dataPoint, i) => {
|
return dataPoints?.map((dataPoint, i) => {
|
||||||
let { color } = dataPoint
|
let { color } = dataPoint
|
||||||
@@ -99,7 +103,7 @@ export default function LineChartDefault({
|
|||||||
dataKey={dataPoint.dataKey}
|
dataKey={dataPoint.dataKey}
|
||||||
name={dataPoint.label}
|
name={dataPoint.label}
|
||||||
type="monotoneX"
|
type="monotoneX"
|
||||||
dot={false}
|
dot={dot}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeOpacity={dataPoint.strokeOpacity}
|
strokeOpacity={dataPoint.strokeOpacity}
|
||||||
@@ -148,7 +152,7 @@ export default function LineChartDefault({
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{xAxis(chartData.chartTime, displayData.at(-1)?.created as number)}
|
{XAxis}
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
animationDuration={150}
|
animationDuration={150}
|
||||||
@@ -169,5 +173,5 @@ export default function LineChartDefault({
|
|||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)
|
)
|
||||||
}, [displayData, yAxisWidth, filter, Lines])
|
}, [displayData, yAxisWidth, filter, Lines, XAxis])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
import { Trans, useLingui } from "@lingui/react/macro"
|
import { Trans, useLingui } from "@lingui/react/macro"
|
||||||
import { useStore } from "@nanostores/react"
|
import { useStore } from "@nanostores/react"
|
||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
@@ -111,13 +111,14 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
const [bulkInput, setBulkInput] = useState("")
|
const [bulkInput, setBulkInput] = useState("")
|
||||||
const [bulkLoading, setBulkLoading] = useState(false)
|
const [bulkLoading, setBulkLoading] = useState(false)
|
||||||
const [bulkSelectedSystemId, setBulkSelectedSystemId] = useState("")
|
const [bulkSelectedSystemId, setBulkSelectedSystemId] = useState("")
|
||||||
|
const bulkFormRef = useRef<HTMLFormElement>(null)
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const { t } = useLingui()
|
const { t } = useLingui()
|
||||||
const systems = useStore($systems)
|
const systems = useStore($systems)
|
||||||
|
|
||||||
const resetBulkForm = () => {
|
const resetBulkForm = () => {
|
||||||
setBulkInput("")
|
setBulkInput("")
|
||||||
setBulkSelectedSystemId("")
|
// setBulkSelectedSystemId("")
|
||||||
}
|
}
|
||||||
|
|
||||||
const openBulkAdd = (selectedSystemId?: string) => {
|
const openBulkAdd = (selectedSystemId?: string) => {
|
||||||
@@ -140,13 +141,15 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const system = systemId ?? bulkSelectedSystemId
|
const system = systemId ?? bulkSelectedSystemId
|
||||||
|
if (!system) {
|
||||||
|
throw new Error("Select a system.")
|
||||||
|
}
|
||||||
const rawLines = bulkInput.split(/\r?\n/).filter((line) => line.trim())
|
const rawLines = bulkInput.split(/\r?\n/).filter((line) => line.trim())
|
||||||
if (!rawLines.length) {
|
if (!rawLines.length) {
|
||||||
throw new Error("Enter at least one probe.")
|
throw new Error("Enter at least one probe.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloads = rawLines.map((line, index) => parseBulkProbeLine(line, index + 1, system))
|
const payloads = rawLines.map((line, index) => parseBulkProbeLine(line, index + 1, system))
|
||||||
setBulkOpen(false)
|
|
||||||
closedForSubmit = true
|
closedForSubmit = true
|
||||||
let batch = pb.createBatch()
|
let batch = pb.createBatch()
|
||||||
let inBatch = 0
|
let inBatch = 0
|
||||||
@@ -221,16 +224,10 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
<Trans>Bulk Add {{ foo: t`Network Probes` }}</Trans>
|
<Trans>Bulk Add {{ foo: t`Network Probes` }}</Trans>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
<Trans>
|
target[,protocol[,port[,interval[,name]]]] - TCP/HTTP default to port 443.
|
||||||
Paste one probe per line. See{" "}
|
|
||||||
<a href={"#bulk-add-probes-docs"} className="underline underline-offset-2">
|
|
||||||
the documentation
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<form onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden">
|
<form ref={bulkFormRef} onSubmit={handleBulkSubmit} className="flex h-full flex-col overflow-hidden">
|
||||||
<div className="flex-1 space-y-4 overflow-auto p-4">
|
<div className="flex-1 space-y-4 overflow-auto p-4">
|
||||||
{!systemId && (
|
{!systemId && (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
@@ -252,8 +249,8 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="bulk-probes">
|
<Label htmlFor="bulk-probes" className="sr-only">
|
||||||
<Trans>Entries</Trans>
|
Entries
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="bulk-probes"
|
id="bulk-probes"
|
||||||
@@ -262,10 +259,10 @@ export function AddProbeDialog({ systemId }: { systemId?: string }) {
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleBulkSubmit(e)
|
bulkFormRef.current?.requestSubmit()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-120 font-mono text-sm bg-muted/40"
|
className="h-200 font-mono text-sm bg-muted/40"
|
||||||
style={{ maxHeight: `calc(100vh - 20rem)` }}
|
style={{ maxHeight: `calc(100vh - 20rem)` }}
|
||||||
placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Homepage"].join("\n")}
|
placeholder={["1.1.1.1", "example.com,tcp", "https://example.com,http,,60,Homepage"].join("\n")}
|
||||||
required
|
required
|
||||||
@@ -338,8 +335,12 @@ function ProbeDialogContent({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const selectedSystem = systemId ?? selectedSystemId
|
||||||
|
if (!selectedSystem) {
|
||||||
|
throw new Error("Select a system.")
|
||||||
|
}
|
||||||
const payload = buildProbePayload({
|
const payload = buildProbePayload({
|
||||||
system: systemId ?? selectedSystemId,
|
system: selectedSystem,
|
||||||
target,
|
target,
|
||||||
protocol,
|
protocol,
|
||||||
port: protocol === "tcp" ? Number(port) : 0,
|
port: protocol === "tcp" ? Number(port) : 0,
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ function ProbeChart({
|
|||||||
contentFormatter={contentFormatter}
|
contentFormatter={contentFormatter}
|
||||||
legend={legend}
|
legend={legend}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
dot={chartData.chartTime === "1m"}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { JSX } from "react"
|
|
||||||
import { useLingui } from "@lingui/react/macro"
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RechartsPrimitive from "recharts"
|
import * as RechartsPrimitive from "recharts"
|
||||||
import { chartTimeData, cn } from "@/lib/utils"
|
import { chartTimeData, cn } from "@/lib/utils"
|
||||||
import type { ChartTimes } from "@/types"
|
import type { ChartTimes } from "@/types"
|
||||||
import { Separator } from "./separator"
|
import { Separator } from "./separator"
|
||||||
import { AxisDomain } from "recharts/types/util/types"
|
import type { AxisDomain } from "recharts/types/util/types"
|
||||||
import { timeTicks } from "d3-time"
|
import { timeTicks } from "d3-time"
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
@@ -102,7 +101,7 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
labelKey?: string
|
labelKey?: string
|
||||||
unit?: string
|
unit?: string
|
||||||
filter?: string
|
filter?: string
|
||||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
contentFormatter?: (item: unknown, key: string) => React.ReactNode | string
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
showTotal?: boolean
|
showTotal?: boolean
|
||||||
totalLabel?: React.ReactNode
|
totalLabel?: React.ReactNode
|
||||||
@@ -176,7 +175,13 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalKey = "__total__"
|
const totalKey = "__total__"
|
||||||
const totalItem: any = {
|
const totalItem: {
|
||||||
|
value: number
|
||||||
|
name: string
|
||||||
|
dataKey: string
|
||||||
|
color: string | undefined
|
||||||
|
payload?: unknown
|
||||||
|
} = {
|
||||||
value: totalValue,
|
value: totalValue,
|
||||||
name: totalName,
|
name: totalName,
|
||||||
dataKey: totalKey,
|
dataKey: totalKey,
|
||||||
@@ -401,21 +406,23 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key:
|
|||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedAxis: {
|
interface XAxisData {
|
||||||
time: number
|
el: React.ReactElement
|
||||||
el: JSX.Element
|
domain: [number, number]
|
||||||
}
|
}
|
||||||
|
|
||||||
const xAxis = (chartTime: ChartTimes, lastCreationTime: number) => {
|
const xAxisCache = new Map<ChartTimes, XAxisData>()
|
||||||
if (Math.abs(lastCreationTime - cachedAxis?.time) < 1000) {
|
|
||||||
return cachedAxis.el
|
function createXAxisData(chartTime: ChartTimes): XAxisData {
|
||||||
}
|
// console.log("Creating XAxis for", chartTime, new Date())
|
||||||
const now = new Date(lastCreationTime + 1000)
|
const axisEndTime = Date.now() + 500
|
||||||
const startTime = chartTimeData[chartTime].getOffset(now)
|
const axisEndDate = new Date(axisEndTime)
|
||||||
const ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
|
const startTime = chartTimeData[chartTime].getOffset(axisEndDate)
|
||||||
const domain = [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()]
|
const ticks = timeTicks(startTime, axisEndDate, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())
|
||||||
cachedAxis = {
|
const domain: [number, number] = [startTime.getTime(), axisEndTime]
|
||||||
time: lastCreationTime,
|
|
||||||
|
return {
|
||||||
|
domain,
|
||||||
el: (
|
el: (
|
||||||
<RechartsPrimitive.XAxis
|
<RechartsPrimitive.XAxis
|
||||||
dataKey="created"
|
dataKey="created"
|
||||||
@@ -431,9 +438,27 @@ const xAxis = (chartTime: ChartTimes, lastCreationTime: number) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function xAxis(chartTime: ChartTimes, lastCreated: number) {
|
||||||
|
if (!lastCreated) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const cachedAxis = xAxisCache.get(chartTime)
|
||||||
|
|
||||||
|
const expectedInterval = chartTimeData[chartTime].expectedInterval
|
||||||
|
const conservativeEndTime = Date.now() - expectedInterval / 2
|
||||||
|
const axisEndTime = Math.max(lastCreated, conservativeEndTime)
|
||||||
|
|
||||||
|
if (cachedAxis && axisEndTime < cachedAxis.domain[1]) {
|
||||||
return cachedAxis.el
|
return cachedAxis.el
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const axisData = createXAxisData(chartTime)
|
||||||
|
xAxisCache.set(chartTime, axisData)
|
||||||
|
return axisData.el
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
|||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:bg-muted/40",
|
"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:bg-muted/40!",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
// if no previous data or the last data point is older than 1min,
|
// if no previous data or the last data point is older than 1min,
|
||||||
// create a new data set starting with a point 1 second ago to seed the chart data
|
// create a new data set starting with a point 1 second ago to seed the chart data
|
||||||
if (!prev || (prev.at(-1)?.created ?? 0) < now - 60_000) {
|
if (!prev || (prev.at(-1)?.created ?? 0) < now - 60_000) {
|
||||||
prev = [{ created: now - 1000, stats: probesToStats(probes) }]
|
prev = [{ created: now - 2000, stats: probesToStats(probes) }]
|
||||||
}
|
}
|
||||||
const stats = { created: now, stats: data.Probes } as NetworkProbeStatsRecord
|
const stats = { created: now, stats: data.Probes } as NetworkProbeStatsRecord
|
||||||
const newStats = appendData(prev, [stats], 1000, 120)
|
const newStats = appendData(prev, [stats], 1000, 120)
|
||||||
@@ -248,6 +248,7 @@ export function useNetworkProbesData(props: UseNetworkProbesProps) {
|
|||||||
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) {
|
||||||
|
// TODO: include only if probe.updated < charttime
|
||||||
stats[probe.id] = [probe.res, probe.resAvg1h, probe.resMin1h, probe.resMax1h, probe.loss1h]
|
stats[probe.id] = [probe.res, probe.resAvg1h, probe.resMin1h, probe.resMax1h, probe.loss1h]
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
Reference in New Issue
Block a user