Files
NetworkView/frontend/src/components/AddItemModal.tsx
T
2026-05-09 01:16:30 +03:00

294 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}