"use client" import React, { useCallback, useEffect, useRef, useState } from "react" import { AlertTriangle, ChevronDown, ChevronRight, Plus, RotateCcw, Trash2, } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { AlarmThreshold, ThresholdUpdate, ThresholdCreate, fetchThresholds, updateThreshold, createThreshold, deleteThreshold, resetThresholds, } from "@/lib/api" // ── Labels ──────────────────────────────────────────────────────────────────── const SENSOR_TYPE_LABELS: Record = { temperature: "Temperature (°C)", humidity: "Humidity (%)", power_kw: "Rack Power (kW)", pdu_imbalance: "Phase Imbalance (%)", ups_charge: "UPS Battery Charge (%)", ups_load: "UPS Load (%)", ups_runtime: "UPS Runtime (min)", gen_fuel_pct: "Generator Fuel (%)", gen_load_pct: "Generator Load (%)", gen_coolant_c: "Generator Coolant (°C)", gen_oil_press: "Generator Oil Pressure (bar)", cooling_cap_pct: "CRAC Capacity (%)", cooling_cop: "CRAC COP", cooling_comp_load: "Compressor Load (%)", cooling_high_press: "High-Side Pressure (bar)", cooling_low_press: "Low-Side Pressure (bar)", cooling_superheat: "Discharge Superheat (°C)", cooling_filter_dp: "Filter Delta-P (Pa)", cooling_return: "Return Air Temp (°C)", net_pkt_loss_pct: "Packet Loss (%)", net_temp_c: "Switch Temperature (°C)", ats_ua_v: "Utility A Voltage (V)", chiller_cop: "Chiller COP", } // ── Groups ──────────────────────────────────────────────────────────────────── type GroupIcon = "Thermometer" | "Zap" | "Battery" | "Fuel" | "Wind" | "Network" interface Group { label: string icon: GroupIcon types: string[] } const GROUPS: Group[] = [ { label: "Temperature & Humidity", icon: "Thermometer", types: ["temperature", "humidity"] }, { label: "Rack Power", icon: "Zap", types: ["power_kw", "pdu_imbalance"] }, { label: "UPS", icon: "Battery", types: ["ups_charge", "ups_load", "ups_runtime"] }, { label: "Generator", icon: "Fuel", types: ["gen_fuel_pct", "gen_load_pct", "gen_coolant_c", "gen_oil_press"] }, { label: "Cooling / CRAC", icon: "Wind", types: ["cooling_cap_pct", "cooling_cop", "cooling_comp_load", "cooling_high_press", "cooling_low_press", "cooling_superheat", "cooling_filter_dp", "cooling_return"] }, { label: "Network", icon: "Network", types: ["net_pkt_loss_pct", "net_temp_c", "ats_ua_v", "chiller_cop"] }, ] // ── Group icon renderer ─────────────────────────────────────────────────────── function GroupIconEl({ icon }: { icon: GroupIcon }) { const cls = "size-4 shrink-0" switch (icon) { case "Thermometer": return ( ) case "Zap": return ( ) case "Battery": return ( ) case "Fuel": return ( ) case "Wind": return ( ) case "Network": return ( ) } } // ── Status toast ────────────────────────────────────────────────────────────── interface Toast { id: number message: string type: "success" | "error" } // ── Save indicator per row ──────────────────────────────────────────────────── type SaveState = "idle" | "saving" | "saved" | "error" // ── Row component ───────────────────────────────────────────────────────────── interface RowProps { threshold: AlarmThreshold onUpdate: (id: number, patch: ThresholdUpdate) => Promise onDelete: (id: number) => void } function ThresholdRow({ threshold, onUpdate, onDelete }: RowProps) { const [localValue, setLocalValue] = useState(String(threshold.threshold_value)) const [saveState, setSaveState] = useState("idle") const saveTimer = useRef | null>(null) // Keep local value in sync if parent updates (e.g. after reset) useEffect(() => { setLocalValue(String(threshold.threshold_value)) }, [threshold.threshold_value]) const signalSave = async (patch: ThresholdUpdate) => { setSaveState("saving") try { await onUpdate(threshold.id, patch) setSaveState("saved") } catch { setSaveState("error") } finally { if (saveTimer.current) clearTimeout(saveTimer.current) saveTimer.current = setTimeout(() => setSaveState("idle"), 1800) } } const handleValueBlur = () => { const parsed = parseFloat(localValue) if (isNaN(parsed)) { setLocalValue(String(threshold.threshold_value)) return } if (parsed !== threshold.threshold_value) { signalSave({ threshold_value: parsed }) } } const handleSeverityToggle = () => { const next = threshold.severity === "warning" ? "critical" : "warning" signalSave({ severity: next }) } const handleEnabledToggle = () => { signalSave({ enabled: !threshold.enabled }) } const directionBadge = threshold.direction === "above" ? ( ▲ Above ) : ( ▼ Below ) const severityBadge = threshold.severity === "critical" ? ( ) : ( ) const saveIndicator = saveState === "saving" ? ( Saving… ) : saveState === "saved" ? ( Saved ) : saveState === "error" ? ( Error ) : null return ( {/* Sensor type */} {SENSOR_TYPE_LABELS[threshold.sensor_type] ?? threshold.sensor_type} {/* Direction */} {directionBadge} {/* Severity */} {severityBadge} {/* Value */}
setLocalValue(e.target.value)} onBlur={handleValueBlur} disabled={threshold.locked} className={cn( "w-24 rounded-md border border-input bg-background px-2 py-1 text-sm text-right tabular-nums", "focus:outline-none focus:ring-2 focus:ring-ring/50", "disabled:cursor-not-allowed disabled:opacity-50" )} /> {saveIndicator}
{/* Enabled toggle */} {/* Delete */} {!threshold.locked && ( )} ) } // ── Group section ───────────────────────────────────────────────────────────── interface GroupSectionProps { group: Group thresholds: AlarmThreshold[] onUpdate: (id: number, patch: ThresholdUpdate) => Promise onDelete: (id: number) => void } function GroupSection({ group, thresholds, onUpdate, onDelete }: GroupSectionProps) { const [expanded, setExpanded] = useState(true) const rows = thresholds.filter((t) => group.types.includes(t.sensor_type)) return (
{/* Header */} {/* Table */} {expanded && (
{rows.length === 0 ? (

No rules configured for this group.

) : ( {rows.map((t) => ( ))}
Sensor Type Direction Severity Value Enabled Delete
)}
)}
) } // ── Add custom rule form ────────────────────────────────────────────────────── interface AddRuleFormProps { siteId: string onCreated: (t: AlarmThreshold) => void onError: (msg: string) => void } const EMPTY_FORM: ThresholdCreate = { sensor_type: "", threshold_value: 0, direction: "above", severity: "warning", message_template: "", } function AddRuleForm({ siteId, onCreated, onError }: AddRuleFormProps) { const [form, setForm] = useState(EMPTY_FORM) const [busy, setBusy] = useState(false) const set = (k: K, v: ThresholdCreate[K]) => setForm((f) => ({ ...f, [k]: v })) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!form.sensor_type.trim()) return setBusy(true) try { const created = await createThreshold(siteId, form) onCreated(created) setForm(EMPTY_FORM) } catch { onError("Failed to create threshold rule.") } finally { setBusy(false) } } return (

Add Custom Rule

{/* Sensor type */}
set("sensor_type", e.target.value)} className="rounded-md border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring/50" required />
{/* Direction */}
{/* Threshold value */}
set("threshold_value", parseFloat(e.target.value) || 0)} className="rounded-md border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring/50" required />
{/* Severity */}
{/* Message template */}
set("message_template", e.target.value)} className="rounded-md border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring/50" />
) } // ── Main component ──────────────────────────────────────────────────────────── interface Props { siteId: string } export function ThresholdEditor({ siteId }: Props) { const [thresholds, setThresholds] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [confirmReset, setConfirmReset] = useState(false) const [resetting, setResetting] = useState(false) const [toasts, setToasts] = useState([]) const toastId = useRef(0) // ── Toast helpers ─────────────────────────────────────────────────────────── const pushToast = useCallback((message: string, type: Toast["type"]) => { const id = ++toastId.current setToasts((prev) => [...prev, { id, message, type }]) setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 3000) }, []) // ── Data loading ──────────────────────────────────────────────────────────── const load = useCallback(async () => { setLoading(true) setError(null) try { const data = await fetchThresholds(siteId) setThresholds(data) } catch { setError("Failed to load alarm thresholds.") } finally { setLoading(false) } }, [siteId]) useEffect(() => { load() }, [load]) // ── Update handler ────────────────────────────────────────────────────────── const handleUpdate = useCallback(async (id: number, patch: ThresholdUpdate) => { // Optimistic update setThresholds((prev) => prev.map((t) => t.id === id ? { ...t, ...(patch.threshold_value !== undefined && { threshold_value: patch.threshold_value }), ...(patch.severity !== undefined && { severity: patch.severity as AlarmThreshold["severity"] }), ...(patch.enabled !== undefined && { enabled: patch.enabled }), } : t ) ) await updateThreshold(id, patch) }, []) // ── Delete handler ────────────────────────────────────────────────────────── const handleDelete = useCallback(async (id: number) => { setThresholds((prev) => prev.filter((t) => t.id !== id)) try { await deleteThreshold(id) pushToast("Threshold deleted.", "success") } catch { pushToast("Failed to delete threshold.", "error") // Reload to restore load() } }, [load, pushToast]) // ── Create handler ────────────────────────────────────────────────────────── const handleCreated = useCallback((t: AlarmThreshold) => { setThresholds((prev) => [...prev, t]) pushToast("Rule added successfully.", "success") }, [pushToast]) // ── Reset handler ─────────────────────────────────────────────────────────── const handleReset = async () => { setResetting(true) try { await resetThresholds(siteId) setConfirmReset(false) pushToast("Thresholds reset to defaults.", "success") await load() } catch { pushToast("Failed to reset thresholds.", "error") } finally { setResetting(false) } } // ── Render ────────────────────────────────────────────────────────────────── return (
{/* Toast container */} {toasts.length > 0 && (
{toasts.map((toast) => (
{toast.message}
))}
)} {/* Header */}

Alarm Thresholds

Configure threshold values that trigger alarms. Click a severity badge to toggle it; blur the value field to save.

{/* Reset controls */}
{confirmReset ? ( <> Are you sure? ) : ( )}
{/* Loading / Error */} {loading && (
Loading thresholds…
)} {!loading && error && (
{error}
)} {/* Groups */} {!loading && !error && (
{GROUPS.map((group) => ( ))}
)} {/* Add custom rule */} {!loading && !error && ( pushToast(msg, "error")} /> )}
) }