Initial commit

This commit is contained in:
2026-05-09 01:16:30 +03:00
commit eed5f39a10
32 changed files with 10267 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
import { useState, useEffect } from 'react';
import { COMPONENT_META, COMPONENT_TYPES } from '../types';
import type { ComponentType, ComponentStatus } from '../types';
interface AddComponentProps {
rackId: string;
totalUnits: number;
initialPosition?: number;
onSave: (data: ComponentFormData) => Promise<void>;
onClose: () => void;
}
export interface ComponentFormData {
rack_id: string;
name: string;
type: ComponentType;
position?: number | null;
height_units: number;
port_count?: number | null;
sfp_count?: number | null;
manufacturer?: string;
model?: string;
serial_number?: string;
asset_tag?: string;
ip_address?: string;
mac_address?: string;
status: ComponentStatus;
notes: string;
}
export function AddComponentModal({ rackId, totalUnits, initialPosition, onSave, onClose }: AddComponentProps) {
const [form, setForm] = useState<ComponentFormData>({
rack_id: rackId,
name: '',
type: 'server',
position: initialPosition ?? null,
height_units: 1,
port_count: null,
sfp_count: null,
manufacturer: '',
model: '',
serial_number: '',
asset_tag: '',
ip_address: '',
mac_address: '',
status: 'active',
notes: '',
});
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
function set(key: keyof ComponentFormData, value: unknown) {
setForm(f => ({ ...f, [key]: value }));
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!form.name.trim()) { setError('Name is required'); return; }
setSaving(true);
setError('');
try {
await onSave({
...form,
position: form.position ? Number(form.position) : null,
height_units: Number(form.height_units),
port_count: (form.type === 'switch' || form.type === 'patch_panel') && form.port_count
? Number(form.port_count) : null,
sfp_count: form.type === 'switch' && form.sfp_count ? Number(form.sfp_count) : null,
manufacturer: form.manufacturer || undefined,
model: form.model || undefined,
serial_number: form.serial_number || undefined,
asset_tag: form.asset_tag || undefined,
ip_address: form.ip_address || undefined,
mac_address: form.mac_address || undefined,
});
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to save');
setSaving(false);
}
}
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">Add Component</h2>
<button className="modal-close" onClick={onClose}></button>
</div>
<form onSubmit={handleSubmit} className="modal-form">
<div className="form-row">
<div className="form-group form-group-lg">
<label>Name *</label>
<input
autoFocus
type="text"
value={form.name}
onChange={e => set('name', e.target.value)}
placeholder="e.g. Core Switch SW-01"
className="form-input"
/>
</div>
<div className="form-group">
<label>Type</label>
<select value={form.type} onChange={e => set('type', e.target.value)} className="form-input">
{COMPONENT_TYPES.map(t => (
<option key={t} value={t}>{COMPONENT_META[t].label}</option>
))}
</select>
</div>
</div>
<div className="form-row">
<div className="form-group">
<label>Rack Position (U)</label>
<input
type="number"
min={1}
max={totalUnits}
value={form.position ?? ''}
onChange={e => set('position', e.target.value ? Number(e.target.value) : null)}
placeholder="auto"
className="form-input"
/>
<span className="form-hint">Leave empty to leave unpositioned</span>
</div>
<div className="form-group">
<label>Height (U)</label>
<input
type="number"
min={1}
max={totalUnits}
value={form.height_units}
onChange={e => set('height_units', Number(e.target.value))}
className="form-input"
/>
</div>
<div className="form-group">
<label>Status</label>
<select value={form.status} onChange={e => set('status', e.target.value)} className="form-input">
<option value="active">Active</option>
<option value="maintenance">Maintenance</option>
<option value="decommissioned">Decommissioned</option>
</select>
</div>
</div>
{(form.type === 'switch' || form.type === 'patch_panel') && (
<div className="form-row port-count-row">
<div className="form-group form-group-lg">
<label>
{form.type === 'patch_panel' ? '🔌 Available Patches (port count)' : '🔌 Number of Switch Ports'}
</label>
<input
type="number"
min={1}
max={96}
value={form.port_count ?? 24}
onChange={e => set('port_count', Number(e.target.value))}
className="form-input"
placeholder="24"
/>
<span className="form-hint">
{form.type === 'patch_panel'
? 'Number of RJ45 jacks on the front panel (e.g. 12, 24, 48)'
: 'Total ethernet ports shown on the rack diagram (e.g. 8, 16, 24, 48)'}
</span>
</div>
{form.type === 'switch' && (
<div className="form-group">
<label>🔶 Fiber / SFP Ports</label>
<input
type="number"
min={0}
max={16}
value={form.sfp_count ?? 0}
onChange={e => set('sfp_count', Number(e.target.value))}
className="form-input"
placeholder="0"
/>
<span className="form-hint">SFP / fiber uplink slots (016)</span>
</div>
)}
</div>
)}
<div className="form-row">
<div className="form-group">
<label>Manufacturer</label>
<input type="text" value={form.manufacturer} onChange={e => set('manufacturer', e.target.value)} placeholder="e.g. Cisco" className="form-input" />
</div>
<div className="form-group">
<label>Model</label>
<input type="text" value={form.model} onChange={e => set('model', e.target.value)} placeholder="e.g. Catalyst 2960" className="form-input" />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label>IP Address</label>
<input type="text" value={form.ip_address} onChange={e => set('ip_address', e.target.value)} placeholder="192.168.1.1" className="form-input" />
</div>
<div className="form-group">
<label>MAC Address</label>
<input type="text" value={form.mac_address} onChange={e => set('mac_address', e.target.value)} placeholder="AA:BB:CC:DD:EE:FF" className="form-input" />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label>Serial Number</label>
<input type="text" value={form.serial_number} onChange={e => set('serial_number', e.target.value)} className="form-input" />
</div>
<div className="form-group">
<label>Asset Tag</label>
<input type="text" value={form.asset_tag} onChange={e => set('asset_tag', e.target.value)} className="form-input" />
</div>
</div>
{error && <div className="form-error">{error}</div>}
<div className="modal-footer">
<button type="button" className="btn-secondary" onClick={onClose}>Cancel</button>
<button type="submit" className="btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Add Component'}
</button>
</div>
</form>
</div>
</div>
);
}
// Generic modal for creating sites/rooms/racks
interface SimpleCreateProps {
title: string;
fields: { key: string; label: string; placeholder?: string; type?: string; defaultValue?: string | number }[];
onSave: (data: Record<string, string | number>) => Promise<void>;
onClose: () => void;
}
export function SimpleCreateModal({ title, fields, onSave, onClose }: SimpleCreateProps) {
const [values, setValues] = useState<Record<string, string | number>>(
Object.fromEntries(fields.map(f => [f.key, f.defaultValue ?? '']))
);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setError('');
try {
await onSave(values);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to save');
setSaving(false);
}
}
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal modal-sm">
<div className="modal-header">
<h2 className="modal-title">{title}</h2>
<button className="modal-close" onClick={onClose}></button>
</div>
<form onSubmit={handleSubmit} className="modal-form">
{fields.map(f => (
<div className="form-group" key={f.key}>
<label>{f.label}</label>
<input
autoFocus={fields[0].key === f.key}
type={f.type ?? 'text'}
value={values[f.key] as string}
onChange={e => setValues(v => ({ ...v, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))}
placeholder={f.placeholder}
className="form-input"
/>
</div>
))}
{error && <div className="form-error">{error}</div>}
<div className="modal-footer">
<button type="button" className="btn-secondary" onClick={onClose}>Cancel</button>
<button type="submit" className="btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Create'}
</button>
</div>
</form>
</div>
</div>
);
}