BMS/frontend/components/dashboard/mini-floor-map.tsx
2026-03-19 11:32:17 +00:00

144 lines
6 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Map as MapIcon, Wind } from "lucide-react";
import { cn } from "@/lib/utils";
import { useThresholds } from "@/lib/threshold-context";
import type { RackCapacity } from "@/lib/api";
type RowLayout = { label: string; racks: string[] };
type RoomLayout = { label: string; crac_id: string; rows: RowLayout[] };
type FloorLayout = Record<string, RoomLayout>;
interface Props {
layout: FloorLayout | null;
racks: RackCapacity[];
loading: boolean;
}
function tempBg(temp: number | null, warn: number, crit: number): string {
if (temp === null) return "oklch(0.22 0.02 265)";
if (temp >= crit + 4) return "oklch(0.55 0.22 25)";
if (temp >= crit) return "oklch(0.65 0.20 45)";
if (temp >= warn) return "oklch(0.72 0.18 84)";
if (temp >= warn - 2) return "oklch(0.78 0.14 140)";
if (temp >= warn - 4) return "oklch(0.68 0.14 162)";
return "oklch(0.60 0.15 212)";
}
export function MiniFloorMap({ layout, racks, loading }: Props) {
const { thresholds } = useThresholds();
const warn = thresholds.temp.warn;
const crit = thresholds.temp.critical;
const rackMap: globalThis.Map<string, RackCapacity> = new globalThis.Map(racks.map(r => [r.rack_id, r] as [string, RackCapacity]));
const roomIds = layout ? Object.keys(layout) : [];
const [activeRoom, setActiveRoom] = useState<string>(() => roomIds[0] ?? "");
const currentRoomId = activeRoom || roomIds[0] || "";
return (
<Card className="flex flex-col">
<CardHeader className="pb-2 shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<MapIcon className="w-4 h-4 text-primary" />
Floor Map Temperature
</CardTitle>
<Link
href="/floor-map"
className="text-[10px] text-primary hover:underline"
>
Full map
</Link>
</div>
{/* Room tabs */}
{roomIds.length > 1 && (
<div className="flex gap-1 mt-2">
{roomIds.map(id => (
<button
key={id}
onClick={() => setActiveRoom(id)}
className={cn(
"px-3 py-1 rounded text-[11px] font-medium transition-colors",
currentRoomId === id
? "bg-primary text-primary-foreground"
: "bg-muted/40 text-muted-foreground hover:bg-muted/60"
)}
>
{layout?.[id]?.label ?? id}
</button>
))}
</div>
)}
</CardHeader>
<CardContent className="flex-1 min-h-0">
{loading ? (
<Skeleton className="h-40 w-full rounded-lg" />
) : !layout || roomIds.length === 0 ? (
<div className="h-40 flex items-center justify-center text-sm text-muted-foreground">
No layout configured
</div>
) : (
<Link href="/floor-map" className="block group">
<div className="space-y-2">
{(layout[currentRoomId]?.rows ?? []).map((row, rowIdx, allRows) => (
<div key={row.label}>
{/* Rack row */}
<div className="flex flex-wrap gap-[3px]">
{row.racks.map(rackId => {
const rack = rackMap.get(rackId);
const bg = tempBg(rack?.temp ?? null, warn, crit);
return (
<div
key={rackId}
title={`${rackId}${rack?.temp != null ? ` · ${rack.temp}°C` : " · offline"}`}
className="rounded-[2px] shrink-0"
style={{ width: 14, height: 20, backgroundColor: bg }}
/>
);
})}
</div>
{/* Cold aisle separator between rows */}
{rowIdx < allRows.length - 1 && (
<div className="flex items-center gap-2 my-1.5 text-[9px] font-semibold uppercase tracking-widest"
style={{ color: "oklch(0.62 0.17 212 / 70%)" }}>
<div className="flex-1 h-px border-t border-dashed" style={{ borderColor: "oklch(0.62 0.17 212 / 25%)" }} />
<Wind className="w-2.5 h-2.5 shrink-0" />
<span>Cold Aisle</span>
<div className="flex-1 h-px border-t border-dashed" style={{ borderColor: "oklch(0.62 0.17 212 / 25%)" }} />
</div>
)}
</div>
))}
{/* CRAC label */}
{layout[currentRoomId]?.crac_id && (
<div className="flex items-center justify-center gap-1.5 rounded-md py-1 text-[10px] font-medium"
style={{ backgroundColor: "oklch(0.62 0.17 212 / 8%)", color: "oklch(0.62 0.17 212)" }}>
<Wind className="w-3 h-3" />
{layout[currentRoomId].crac_id.toUpperCase()}
</div>
)}
</div>
{/* Temp legend */}
<div className="flex items-center gap-1 mt-3 text-[10px] text-muted-foreground">
<span>Cool</span>
{(["oklch(0.60 0.15 212)","oklch(0.68 0.14 162)","oklch(0.78 0.14 140)","oklch(0.72 0.18 84)","oklch(0.65 0.20 45)","oklch(0.55 0.22 25)"] as string[]).map((c, i) => (
<span key={i} className="w-5 h-2.5 rounded-sm inline-block" style={{ backgroundColor: c }} />
))}
<span>Hot</span>
<span className="ml-auto opacity-60 group-hover:opacity-100 transition-opacity">Click to open full map</span>
</div>
</Link>
)}
</CardContent>
</Card>
);
}