Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
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<number, Component>();
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<string>();
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 (
<div className="rack-layout">
<table className="rack-layout-table">
<tbody>
{rows.map(row => (
<tr key={row.unit} className="rack-layout-row">
<td className="rack-layout-u">{row.unit}</td>
{row.isFirst && (
row.comp ? (
<td
rowSpan={row.span}
className="rack-layout-comp"
style={{
background: COMPONENT_META[row.comp.type].bg,
borderLeft: `4px solid ${COMPONENT_META[row.comp.type].color}`,
}}
onClick={() => navigate(`/components/${row.comp!.id}`)}
>
<span className="rack-layout-comp-name">{row.comp.name}</span>
<span className="rack-layout-comp-type" style={{ color: COMPONENT_META[row.comp.type].color }}>
{COMPONENT_META[row.comp.type].label}
</span>
{row.comp.model && <span className="rack-layout-comp-model">{row.comp.model}</span>}
{row.comp.ip_address && <span className="rack-layout-comp-ip">{row.comp.ip_address}</span>}
{row.span > 1 && <span className="rack-layout-comp-height">{row.span}U</span>}
</td>
) : (
<td
className="rack-layout-empty"
onClick={() => onAddAtSlot?.(row.unit)}
>
<span className="rack-layout-empty-txt">· empty · click to add</span>
</td>
)
)}
</tr>
))}
</tbody>
</table>
</div>
);
}
export default function RackPage() {
const { rackId } = useParams<{ rackId: string }>();
const navigate = useNavigate();
const [rack, setRack] = useState<Rack | null>(null);
const [components, setComponents] = useState<Component[]>([]);
const [loading, setLoading] = useState(true);
const [notes, setNotes] = useState('');
const [showAddComponent, setShowAddComponent] = useState(false);
const [addAtPosition, setAddAtPosition] = useState<number | undefined>(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<string, string | number>) => {
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 <div className="page-loading">Loading</div>;
const usedUnits = components.reduce((s, c) => s + (c.position != null ? c.height_units : 0), 0);
const freeUnits = rack.total_units - usedUnits;
return (
<div className="page page-rack">
<div className="page-header">
<div className="breadcrumb">
<Link to="/">Dashboard</Link>
{rack.site && <><span> / </span><Link to={`/sites/${rack.site.id}`}>{rack.site.name}</Link></>}
{rack.room && <><span> / </span><Link to={`/rooms/${rack.room.id}`}>{rack.room.name}</Link></>}
<span> / </span>
<span>{rack.name}</span>
</div>
<div className="page-actions">
{rack.room && (
<button className="btn-back btn-sm" onClick={() => navigate(`/rooms/${rack.room!.id}`)}> Back to Room</button>
)}
<button className="btn-graph btn-sm" onClick={() => navigate(`/graph/rack/${rackId}`)}> Graph View</button>
<button className="btn-secondary btn-sm" onClick={() => setShowEditRack(true)}> Edit</button>
<button className="btn-primary btn-sm" onClick={() => { setAddAtPosition(undefined); setShowAddComponent(true); }}>
+ Add Component
</button>
<button className="btn-danger btn-sm" onClick={handleDelete}>🗑 Delete</button>
</div>
</div>
<div className="entity-header">
<h1 className="entity-title">
<span className="entity-icon">🗄</span> {rack.name}
</h1>
<div className="entity-meta-row">
<span className="entity-meta-chip">{rack.total_units}U total</span>
<span className="entity-meta-chip" style={{ color: '#f59e0b' }}>{usedUnits}U used</span>
<span className="entity-meta-chip" style={{ color: '#34d399' }}>{freeUnits}U free</span>
{rack.manufacturer && <span className="entity-meta-chip">{rack.manufacturer}</span>}
{rack.model && <span className="entity-meta-chip">{rack.model}</span>}
</div>
</div>
{/* Two-column rack layout */}
<div className="rack-page-columns">
{/* LEFT: Component management */}
<div className="rack-page-left">
<section className="content-section">
<div className="section-header">
<h2 className="section-title">Components</h2>
<div className="section-view-toggle">
<button
className={`btn-sm ${compView === 'table' ? 'btn-secondary' : 'btn-ghost'}`}
title="Table view"
onClick={() => setCompView('table')}
> Table</button>
<button
className={`btn-sm ${compView === 'layout' ? 'btn-secondary' : 'btn-ghost'}`}
title="Rack layout view"
onClick={() => setCompView('layout')}
> Rack Map</button>
</div>
<button className="btn-primary btn-sm" onClick={() => { setAddAtPosition(undefined); setShowAddComponent(true); }}>
+ Add Component
</button>
</div>
{compView === 'layout' ? (
components.length === 0 ? (
<div className="empty-state"><p>No components yet.</p></div>
) : (
<RackLayoutView rack={rack} components={components} onAddAtSlot={handleAddAtSlot} />
)
) : (
components.length === 0 ? (
<div className="empty-state">
<p>No components yet.</p>
<p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>
Click <strong>+ Add Component</strong> or click an empty slot in the rack diagram
</p>
</div>
) : (
<table className="component-table">
<thead>
<tr>
<th>U</th>
<th>Name</th>
<th>Type</th>
<th>Model</th>
<th>IP</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{[...components]
.sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999))
.map(c => {
const meta = COMPONENT_META[c.type];
return (
<tr key={c.id} className="component-table-row" onClick={() => navigate(`/components/${c.id}`)} style={{ borderLeft: `3px solid ${meta.color}` }}>
<td className="td-position">{c.position ?? '—'}</td>
<td className="td-name">{c.name}</td>
<td className="td-type" style={{ color: meta.color }}>{meta.label}</td>
<td className="td-model">{c.model ?? '—'}</td>
<td className="td-ip">{c.ip_address ?? '—'}</td>
<td className="td-status"><span className={`status-badge status-${c.status}`}>{c.status}</span></td>
</tr>
);
})}
</tbody>
</table>
)
)}
</section>
<section className="content-section">
<h2 className="section-title">Rack Notes</h2>
<MarkdownEditor value={notes} onChange={setNotes} onSave={saveNotes} />
</section>
</div>
{/* RIGHT: Graphical front panel */}
<div className="rack-page-right">
<div className="rack-page-right-scroll">
<div className="rack-page-right-header">
<span className="rack-page-right-title">Front Panel View</span>
<span className="rack-page-right-hint">Click slot to add · Click device to open</span>
</div>
<RackGraphicView
rack={rack}
components={components}
onAddAtSlot={handleAddAtSlot}
/>
</div>
</div>
</div>
{showAddComponent && rackId && (
<AddComponentModal
rackId={rackId}
totalUnits={rack.total_units}
initialPosition={addAtPosition}
onSave={handleAddComponent}
onClose={() => setShowAddComponent(false)}
/>
)}
{showEditRack && (
<SimpleCreateModal
title="Edit Rack"
fields={[
{ key: 'name', label: 'Rack Name', defaultValue: rack.name },
{ key: 'total_units', label: 'Size (U)', type: 'number', defaultValue: rack.total_units },
{ key: 'manufacturer', label: 'Manufacturer', defaultValue: rack.manufacturer ?? '' },
{ key: 'model', label: 'Model', defaultValue: rack.model ?? '' },
]}
onSave={handleEditRack}
onClose={() => setShowEditRack(false)}
/>
)}
</div>
);
}