mirror of
https://github.com/henrygd/beszel.git
synced 2026-10-01 13:57:47 +02:00
feat(ui): add MB/s and GB/s unit select to bandwidth alert
Raises the bandwidth threshold max to 5 GB/s without making the MB/s slider less precise. Values are still stored in MB/s, and the unit is inferred from the stored value when the alert sheet opens.
This commit is contained in:
@@ -10,6 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"
|
|||||||
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { toast } from "@/components/ui/use-toast"
|
import { toast } from "@/components/ui/use-toast"
|
||||||
@@ -17,7 +18,7 @@ import { alertInfo } from "@/lib/alerts"
|
|||||||
import { pb } from "@/lib/api"
|
import { pb } from "@/lib/api"
|
||||||
import { $alerts, $systems } from "@/lib/stores"
|
import { $alerts, $systems } from "@/lib/stores"
|
||||||
import { cn, debounce } from "@/lib/utils"
|
import { cn, debounce } from "@/lib/utils"
|
||||||
import type { AlertInfo, AlertRecord, SystemRecord } from "@/types"
|
import type { AlertInfo, AlertRecord, AlertUnit, SystemRecord } from "@/types"
|
||||||
|
|
||||||
const Slider = lazy(() => import("@/components/ui/slider"))
|
const Slider = lazy(() => import("@/components/ui/slider"))
|
||||||
|
|
||||||
@@ -27,6 +28,24 @@ const alertDebounce = 400
|
|||||||
|
|
||||||
const alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]
|
const alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]
|
||||||
|
|
||||||
|
/** Round to a multiple of step, trimming float noise like 0.30000000000000004 */
|
||||||
|
const roundToStep = (val: number, step: number) => Number((Math.round(val / step) * step).toFixed(6))
|
||||||
|
|
||||||
|
/** Number input step and minimum for a unit, which may be finer than the slider */
|
||||||
|
const getInputRange = ({ min, step, inputStep = step }: AlertUnit) => ({ step: inputStep, min: Math.min(min, inputStep) })
|
||||||
|
|
||||||
|
/** Index of the unit to display a stored value in: the largest unit whose input range and step fit it */
|
||||||
|
function getUnitIndex(units: AlertUnit[], value: number) {
|
||||||
|
for (let i = units.length - 1; i > 0; i--) {
|
||||||
|
const { min, step } = getInputRange(units[i])
|
||||||
|
const val = value / units[i].factor
|
||||||
|
if (val >= min && val <= units[i].max && Math.abs(roundToStep(val, step) - val) < 1e-6) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
const failedUpdateToast = (error: unknown) => {
|
const failedUpdateToast = (error: unknown) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
toast({
|
toast({
|
||||||
@@ -245,7 +264,17 @@ export function AlertContent({
|
|||||||
|
|
||||||
const [checked, setChecked] = useState(global ? false : !!alert)
|
const [checked, setChecked] = useState(global ? false : !!alert)
|
||||||
const [min, setMin] = useState(alert?.min || (noDuration ? 0 : 10))
|
const [min, setMin] = useState(alert?.min || (noDuration ? 0 : 10))
|
||||||
const [value, setValue] = useState(alert?.value ?? (noThreshold ? 0 : (alertData.start ?? 80)))
|
const { units } = alertData
|
||||||
|
const storedValue = alert?.value ?? (noThreshold ? 0 : (alertData.start ?? 80))
|
||||||
|
const [unitIndex, setUnitIndex] = useState(() => (units ? getUnitIndex(units, storedValue) : 0))
|
||||||
|
/** Selected threshold unit, if the alert has selectable units */
|
||||||
|
const unit = units?.[unitIndex]
|
||||||
|
const factor = unit?.factor ?? 1
|
||||||
|
const unitLabel = unit?.unit ?? alertData.unit
|
||||||
|
const { min: valueMin, max: valueMax, step = 1 } = unit ?? alertData
|
||||||
|
const { min: inputMin, step: inputStep } = unit ? getInputRange(unit) : { min: valueMin, step }
|
||||||
|
/** Threshold value in the selected unit */
|
||||||
|
const [value, setValue] = useState(() => Number((storedValue / factor).toFixed(6)))
|
||||||
|
|
||||||
const Icon = alertData.icon
|
const Icon = alertData.icon
|
||||||
|
|
||||||
@@ -267,12 +296,12 @@ export function AlertContent({
|
|||||||
return systemIds
|
return systemIds
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendUpsert(min: number, value: number) {
|
function sendUpsert(min: number, value: number, valueFactor = factor) {
|
||||||
const systems = getSystemIds()
|
const systems = getSystemIds()
|
||||||
systems.length &&
|
systems.length &&
|
||||||
upsertAlerts({
|
upsertAlerts({
|
||||||
name: alertKey,
|
name: alertKey,
|
||||||
value,
|
value: Number((value * valueFactor).toFixed(6)),
|
||||||
min,
|
min,
|
||||||
systems,
|
systems,
|
||||||
})
|
})
|
||||||
@@ -326,7 +355,7 @@ export function AlertContent({
|
|||||||
Average drops below{" "}
|
Average drops below{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{alertData.unit}
|
{unitLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
) : (
|
) : (
|
||||||
@@ -334,7 +363,7 @@ export function AlertContent({
|
|||||||
Average exceeds{" "}
|
Average exceeds{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
{value}
|
{value}
|
||||||
{alertData.unit}
|
{unitLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
@@ -345,9 +374,9 @@ export function AlertContent({
|
|||||||
value={[value]}
|
value={[value]}
|
||||||
onValueCommit={(val) => sendUpsert(min, val[0])}
|
onValueCommit={(val) => sendUpsert(min, val[0])}
|
||||||
onValueChange={(val) => setValue(val[0])}
|
onValueChange={(val) => setValue(val[0])}
|
||||||
step={alertData.step ?? 1}
|
step={step}
|
||||||
min={alertData.min ?? 1}
|
min={valueMin ?? 1}
|
||||||
max={alertData.max ?? 99}
|
max={valueMax ?? 99}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -355,17 +384,45 @@ export function AlertContent({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
let val = parseFloat(e.target.value)
|
let val = parseFloat(e.target.value)
|
||||||
if (!Number.isNaN(val)) {
|
if (!Number.isNaN(val)) {
|
||||||
if (alertData.max != null) val = Math.min(val, alertData.max)
|
// keep values on the step grid so the unit can be inferred on reload
|
||||||
if (alertData.min != null) val = Math.max(val, alertData.min)
|
if (unit) val = roundToStep(val, inputStep)
|
||||||
|
if (valueMax != null) val = Math.min(val, valueMax)
|
||||||
|
if (inputMin != null) val = Math.max(val, inputMin)
|
||||||
setValue(val)
|
setValue(val)
|
||||||
sendUpsert(min, val)
|
sendUpsert(min, val)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
step={alertData.step ?? 1}
|
step={inputStep}
|
||||||
min={alertData.min ?? 1}
|
min={inputMin ?? 1}
|
||||||
max={alertData.max ?? 99}
|
max={valueMax ?? 99}
|
||||||
className="w-16 h-8 text-center px-1"
|
className="w-16 h-8 text-center px-1"
|
||||||
/>
|
/>
|
||||||
|
{units && (
|
||||||
|
<Select
|
||||||
|
value={String(unitIndex)}
|
||||||
|
onValueChange={(index) => {
|
||||||
|
const next = units[Number(index)]
|
||||||
|
const nextInput = getInputRange(next)
|
||||||
|
// convert the current threshold to the new unit, clamped to its range
|
||||||
|
const val = roundToStep((value * factor) / next.factor, nextInput.step)
|
||||||
|
const clamped = Math.min(Math.max(val, nextInput.min), next.max)
|
||||||
|
setUnitIndex(Number(index))
|
||||||
|
setValue(clamped)
|
||||||
|
sendUpsert(min, clamped, next.factor)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-auto shrink-0 h-8 gap-1.5 px-2.5">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{units.map(({ unit }, i) => (
|
||||||
|
<SelectItem key={unit} value={String(i)}>
|
||||||
|
{unit.trim()}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export const alertInfo: Record<string, AlertInfo> = {
|
|||||||
icon: EthernetIcon,
|
icon: EthernetIcon,
|
||||||
desc: () => t`Triggers when combined up/down exceeds a threshold`,
|
desc: () => t`Triggers when combined up/down exceeds a threshold`,
|
||||||
max: 250,
|
max: 250,
|
||||||
|
// 1024 to match the hub, which converts bandwidth with 1024 * 1024
|
||||||
|
units: [
|
||||||
|
{ unit: " MB/s", factor: 1, min: 1, max: 250, step: 1, inputStep: 0.1 },
|
||||||
|
{ unit: " GB/s", factor: 1024, min: 0.1, max: 5, step: 0.1 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
NetworkMonitorLoss: {
|
NetworkMonitorLoss: {
|
||||||
name: () => t`Network Monitor Loss`,
|
name: () => t`Network Monitor Loss`,
|
||||||
|
|||||||
14
internal/site/src/types.d.ts
vendored
14
internal/site/src/types.d.ts
vendored
@@ -459,6 +459,20 @@ export interface AlertInfo {
|
|||||||
/** Additional information that remains visible while the alert is enabled */
|
/** Additional information that remains visible while the alert is enabled */
|
||||||
note?: () => string
|
note?: () => string
|
||||||
invert?: boolean
|
invert?: boolean
|
||||||
|
/** Selectable threshold units. Values are stored in the first unit (factor 1) */
|
||||||
|
units?: AlertUnit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertUnit {
|
||||||
|
/** Unit suffix shown after the value */
|
||||||
|
unit: string
|
||||||
|
/** Multiplier converting a value in this unit to the stored value */
|
||||||
|
factor: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
step: number
|
||||||
|
/** Finer step for the number input, which also accepts values down to this step */
|
||||||
|
inputStep?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
||||||
|
|||||||
Reference in New Issue
Block a user