first commit
This commit is contained in:
commit
4b98219bf7
144 changed files with 31561 additions and 0 deletions
144
frontend/components/dashboard/mini-floor-map.tsx
Normal file
144
frontend/components/dashboard/mini-floor-map.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue