import { useEffect, useState, useCallback } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import * as api from '../api'; import type { Rack, Component } from '../types'; import { COMPONENT_META } from '../types'; import RackGraphicView from '../components/RackGraphicView'; import MarkdownEditor from '../components/MarkdownEditor'; import { AddComponentModal, SimpleCreateModal } from '../components/AddItemModal'; import type { ComponentFormData } from '../components/AddItemModal'; // ── Rack Layout Table ───────────────────────────────────────────────────────── function RackLayoutView({ rack, components, onAddAtSlot }: { rack: Rack; components: Component[]; onAddAtSlot?: (position: number) => void; }) { const navigate = useNavigate(); // Map each unit number → component (for multi-U components, repeated) const byPos = new Map(); for (const c of components) { if (c.position != null) { for (let u = c.position; u < c.position + c.height_units; u++) { byPos.set(u, c); } } } // Build rows for the table interface RowInfo { unit: number; comp?: Component; isFirst: boolean; span: number } const rows: RowInfo[] = []; const seen = new Set(); for (let u = 1; u <= rack.total_units; u++) { const comp = byPos.get(u); if (comp) { if (!seen.has(comp.id)) { seen.add(comp.id); rows.push({ unit: u, comp, isFirst: true, span: comp.height_units }); } else { rows.push({ unit: u, comp, isFirst: false, span: 0 }); } } else { rows.push({ unit: u, comp: undefined, isFirst: true, span: 1 }); } } return (
{rows.map(row => ( {row.isFirst && ( row.comp ? ( ) : ( ) )} ))}
{row.unit} navigate(`/components/${row.comp!.id}`)} > {row.comp.name} {COMPONENT_META[row.comp.type].label} {row.comp.model && {row.comp.model}} {row.comp.ip_address && {row.comp.ip_address}} {row.span > 1 && {row.span}U} onAddAtSlot?.(row.unit)} > · empty · click to add
); } export default function RackPage() { const { rackId } = useParams<{ rackId: string }>(); const navigate = useNavigate(); const [rack, setRack] = useState(null); const [components, setComponents] = useState([]); const [loading, setLoading] = useState(true); const [notes, setNotes] = useState(''); const [showAddComponent, setShowAddComponent] = useState(false); const [addAtPosition, setAddAtPosition] = useState(undefined); const [showEditRack, setShowEditRack] = useState(false); const [compView, setCompView] = useState<'table' | 'layout'>('table'); const loadRack = useCallback(async () => { if (!rackId) return; const data = await api.getRack(rackId); setRack(data); setComponents(data.components ?? []); setNotes(data.notes ?? ''); setLoading(false); }, [rackId]); useEffect(() => { loadRack(); }, [loadRack]); const saveNotes = async (val: string) => { if (!rackId) return; await api.updateRack(rackId, { notes: val }); }; const handleAddComponent = async (formData: ComponentFormData) => { await api.createComponent(formData); setShowAddComponent(false); await loadRack(); }; const handleAddAtSlot = (position: number) => { setAddAtPosition(position); setShowAddComponent(true); }; const handleEditRack = async (data: Record) => { if (!rackId) return; await api.updateRack(rackId, { name: data.name as string, total_units: Number(data.total_units), manufacturer: data.manufacturer as string, model: data.model as string, }); setShowEditRack(false); await loadRack(); }; const handleDelete = async () => { if (!rackId || !rack || !confirm(`Delete rack "${rack.name}" and all its components?`)) return; await api.deleteRack(rackId); navigate(rack.room ? `/rooms/${rack.room.id}` : '/'); }; if (loading || !rack) return
Loading…
; const usedUnits = components.reduce((s, c) => s + (c.position != null ? c.height_units : 0), 0); const freeUnits = rack.total_units - usedUnits; return (
Dashboard {rack.site && <> / {rack.site.name}} {rack.room && <> / {rack.room.name}} / {rack.name}
{rack.room && ( )}

🗄️ {rack.name}

{rack.total_units}U total {usedUnits}U used {freeUnits}U free {rack.manufacturer && {rack.manufacturer}} {rack.model && {rack.model}}
{/* Two-column rack layout */}
{/* LEFT: Component management */}

Components

{compView === 'layout' ? ( components.length === 0 ? (

No components yet.

) : ( ) ) : ( components.length === 0 ? (

No components yet.

Click + Add Component or click an empty slot in the rack diagram →

) : ( {[...components] .sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999)) .map(c => { const meta = COMPONENT_META[c.type]; return ( navigate(`/components/${c.id}`)} style={{ borderLeft: `3px solid ${meta.color}` }}> ); })}
U Name Type Model IP Status
{c.position ?? '—'} {c.name} {meta.label} {c.model ?? '—'} {c.ip_address ?? '—'} {c.status}
) )}

Rack Notes

{/* RIGHT: Graphical front panel */}
Front Panel View Click slot to add · Click device to open
{showAddComponent && rackId && ( setShowAddComponent(false)} /> )} {showEditRack && ( setShowEditRack(false)} /> )}
); }