"use client"; import { useEffect, useState } from "react"; import { fetchRackHistory, fetchRackDevices, type RackHistory, type Device, } from "@/lib/api"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { TimeRangePicker } from "@/components/ui/time-range-picker"; import { Badge } from "@/components/ui/badge"; import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { Thermometer, Zap, Droplets, Bell, Server } from "lucide-react"; import { cn } from "@/lib/utils"; interface Props { siteId: string; rackId: string | null; onClose: () => void; } // ── Helpers ─────────────────────────────────────────────────────────────────── function timeAgo(iso: string) { const diff = Date.now() - new Date(iso).getTime(); const m = Math.floor(diff / 60000); if (m < 1) return "just now"; if (m < 60) return `${m}m ago`; return `${Math.floor(m / 60)}h ago`; } function formatTime(iso: string) { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } // ── Device type styles ──────────────────────────────────────────────────────── const TYPE_STYLES: Record = { server: { bg: "bg-blue-500/15", border: "border-blue-500/40", text: "text-blue-300", label: "Server" }, switch: { bg: "bg-green-500/15", border: "border-green-500/40", text: "text-green-300", label: "Switch" }, patch_panel: { bg: "bg-slate-500/15", border: "border-slate-400/40", text: "text-slate-300", label: "Patch Panel" }, pdu: { bg: "bg-amber-500/15", border: "border-amber-500/40", text: "text-amber-300", label: "PDU" }, storage: { bg: "bg-purple-500/15", border: "border-purple-500/40", text: "text-purple-300", label: "Storage" }, firewall: { bg: "bg-red-500/15", border: "border-red-500/40", text: "text-red-300", label: "Firewall" }, kvm: { bg: "bg-teal-500/15", border: "border-teal-500/40", text: "text-teal-300", label: "KVM" }, }; const TYPE_DOT: Record = { server: "bg-blue-400", switch: "bg-green-400", patch_panel: "bg-slate-400", pdu: "bg-amber-400", storage: "bg-purple-400", firewall: "bg-red-400", kvm: "bg-teal-400", }; // ── U-diagram ───────────────────────────────────────────────────────────────── const TOTAL_U = 42; const U_PX = 20; // height per U in pixels type Segment = { u: number; height: number; device: Device | null }; function buildSegments(devices: Device[]): Segment[] { const sorted = [...devices].sort((a, b) => a.u_start - b.u_start); const segs: Segment[] = []; let u = 1; let i = 0; while (u <= TOTAL_U) { if (i < sorted.length && sorted[i].u_start === u) { const d = sorted[i]; segs.push({ u, height: d.u_height, device: d }); u += d.u_height; i++; } else { const nextU = i < sorted.length ? sorted[i].u_start : TOTAL_U + 1; const empty = Math.min(nextU, TOTAL_U + 1) - u; if (empty > 0) { segs.push({ u, height: empty, device: null }); u += empty; } else { break; } } } return segs; } function RackDiagram({ devices, loading }: { devices: Device[]; loading: boolean }) { const [selected, setSelected] = useState(null); if (loading) { return (
{Array.from({ length: 12 }).map((_, i) => ( ))}
); } const segments = buildSegments(devices); const totalPower = devices.reduce((s, d) => s + d.power_draw_w, 0); return (
{/* Legend */}
{Object.entries(TYPE_STYLES).map(([type, style]) => ( devices.some(d => d.type === type) ? (
{style.label}
) : null ))}
{/* Rack diagram */}
{/* Header */}
U {devices[0]?.rack_id.toUpperCase() ?? "Rack"} — 42U {totalPower} W total
{/* Slots */}
{segments.map((seg) => { const style = seg.device ? (TYPE_STYLES[seg.device.type] ?? TYPE_STYLES.server) : null; const isSelected = selected?.device_id === seg.device?.device_id; return (
setSelected(seg.device && isSelected ? null : seg.device)} > {/* U number */}
{seg.u}
{/* Device or empty */}
{seg.device ? (
{seg.device.name} {seg.height >= 2 && ( {seg.device.ip !== "-" ? seg.device.ip : ""} )} {seg.device.u_height}U
) : (
{seg.height > 1 && ( {seg.height}U empty )}
)}
); })}
{/* Selected device detail */} {selected && (() => { // shrink-0 via parent gap const style = TYPE_STYLES[selected.type] ?? TYPE_STYLES.server; return (

{selected.name}

{style.label}

● Online
Serial: {selected.serial}
IP: {selected.ip !== "-" ? selected.ip : "—"}
Position: U{selected.u_start}–U{selected.u_start + selected.u_height - 1}
Power: {selected.power_draw_w} W
); })()}
); } // ── History tab ─────────────────────────────────────────────────────────────── function HistoryTab({ data, hours, onHoursChange, loading }: { data: RackHistory | null; hours: number; onHoursChange: (h: number) => void; loading: boolean; }) { const chartData = (data?.history ?? []).map(p => ({ ...p, time: formatTime(p.bucket) })); const latest = chartData[chartData.length - 1]; return (
{loading ? (
) : (
{latest && (

{latest.temperature !== undefined ? `${latest.temperature}°C` : "—"}

Temperature

{latest.power_kw !== undefined ? `${latest.power_kw} kW` : "—"}

Power

{latest.humidity !== undefined ? `${latest.humidity}%` : "—"}

Humidity

)}

Temperature (°C)

[`${v}°C`, "Temp"]} />

Power Draw (kW)

[`${v} kW`, "Power"]} />
)}
); } // ── Alarms tab ──────────────────────────────────────────────────────────────── const severityColors: Record = { critical: "bg-destructive/15 text-destructive border-destructive/30", warning: "bg-amber-500/15 text-amber-400 border-amber-500/30", info: "bg-blue-500/15 text-blue-400 border-blue-500/30", }; const stateColors: Record = { active: "bg-destructive/10 text-destructive", acknowledged: "bg-amber-500/10 text-amber-400", resolved: "bg-green-500/10 text-green-400", }; function AlarmsTab({ data }: { data: RackHistory | null }) { return (
{!data || data.alarms.length === 0 ? (

No alarms on record for this rack.

) : (
{data.alarms.map((alarm) => (
{alarm.message} {alarm.state}

{timeAgo(alarm.triggered_at)}

))}
)}
); } // ── Sheet ───────────────────────────────────────────────────────────────────── export function RackDetailSheet({ siteId, rackId, onClose }: Props) { const [data, setData] = useState(null); const [devices, setDevices] = useState([]); const [hours, setHours] = useState(6); const [histLoading, setHistLoad] = useState(false); const [devLoading, setDevLoad] = useState(false); useEffect(() => { if (!rackId) { setData(null); setDevices([]); return; } setHistLoad(true); setDevLoad(true); fetchRackHistory(siteId, rackId, hours) .then(setData).catch(() => setData(null)) .finally(() => setHistLoad(false)); fetchRackDevices(siteId, rackId) .then(setDevices).catch(() => setDevices([])) .finally(() => setDevLoad(false)); }, [siteId, rackId, hours]); const alarmCount = data?.alarms.filter(a => a.state === "active").length ?? 0; return ( { if (!open) onClose(); }}> {rackId?.toUpperCase() ?? ""}

Singapore DC01

Layout History Alarms {alarmCount > 0 && ( {alarmCount} )}
); }