diff --git a/internal/site/src/components/alerts/alerts-sheet.tsx b/internal/site/src/components/alerts/alerts-sheet.tsx
index 94e4e3b30..845aa0746 100644
--- a/internal/site/src/components/alerts/alerts-sheet.tsx
+++ b/internal/site/src/components/alerts/alerts-sheet.tsx
@@ -10,6 +10,7 @@ import { Checkbox } from "@/components/ui/checkbox"
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { toast } from "@/components/ui/use-toast"
@@ -17,7 +18,7 @@ import { alertInfo } from "@/lib/alerts"
import { pb } from "@/lib/api"
import { $alerts, $systems } from "@/lib/stores"
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"))
@@ -27,6 +28,24 @@ const alertDebounce = 400
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) => {
console.error(error)
toast({
@@ -245,7 +264,17 @@ export function AlertContent({
const [checked, setChecked] = useState(global ? false : !!alert)
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
@@ -267,12 +296,12 @@ export function AlertContent({
return systemIds
}
- function sendUpsert(min: number, value: number) {
+ function sendUpsert(min: number, value: number, valueFactor = factor) {
const systems = getSystemIds()
systems.length &&
upsertAlerts({
name: alertKey,
- value,
+ value: Number((value * valueFactor).toFixed(6)),
min,
systems,
})
@@ -326,7 +355,7 @@ export function AlertContent({
Average drops below{" "}
{value}
- {alertData.unit}
+ {unitLabel}
) : (
@@ -334,7 +363,7 @@ export function AlertContent({
Average exceeds{" "}
{value}
- {alertData.unit}
+ {unitLabel}
)}
@@ -345,9 +374,9 @@ export function AlertContent({
value={[value]}
onValueCommit={(val) => sendUpsert(min, val[0])}
onValueChange={(val) => setValue(val[0])}
- step={alertData.step ?? 1}
- min={alertData.min ?? 1}
- max={alertData.max ?? 99}
+ step={step}
+ min={valueMin ?? 1}
+ max={valueMax ?? 99}
/>
{
let val = parseFloat(e.target.value)
if (!Number.isNaN(val)) {
- if (alertData.max != null) val = Math.min(val, alertData.max)
- if (alertData.min != null) val = Math.max(val, alertData.min)
+ // keep values on the step grid so the unit can be inferred on reload
+ if (unit) val = roundToStep(val, inputStep)
+ if (valueMax != null) val = Math.min(val, valueMax)
+ if (inputMin != null) val = Math.max(val, inputMin)
setValue(val)
sendUpsert(min, val)
}
}}
- step={alertData.step ?? 1}
- min={alertData.min ?? 1}
- max={alertData.max ?? 99}
+ step={inputStep}
+ min={inputMin ?? 1}
+ max={valueMax ?? 99}
className="w-16 h-8 text-center px-1"
/>
+ {units && (
+
+ )}
)}
diff --git a/internal/site/src/lib/alerts.ts b/internal/site/src/lib/alerts.ts
index b5f52a7dc..39c6a0f32 100644
--- a/internal/site/src/lib/alerts.ts
+++ b/internal/site/src/lib/alerts.ts
@@ -53,6 +53,11 @@ export const alertInfo: Record = {
icon: EthernetIcon,
desc: () => t`Triggers when combined up/down exceeds a threshold`,
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: {
name: () => t`Network Monitor Loss`,
diff --git a/internal/site/src/types.d.ts b/internal/site/src/types.d.ts
index 942b9bc2b..45e3d0fab 100644
--- a/internal/site/src/types.d.ts
+++ b/internal/site/src/types.d.ts
@@ -459,6 +459,20 @@ export interface AlertInfo {
/** Additional information that remains visible while the alert is enabled */
note?: () => string
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>