Skip to content

Commit 89b1812

Browse files
committed
adding configuration UI
1 parent ec27436 commit 89b1812

File tree

1 file changed

+350
-0
lines changed

1 file changed

+350
-0
lines changed
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
import React, { useEffect, useState } from 'react'
2+
3+
interface Device {
4+
id: string
5+
name: string
6+
driver: string
7+
port: string
8+
baud: number
9+
serial: string
10+
model: string
11+
}
12+
13+
interface DriverInfo {
14+
vendor: string
15+
family: string
16+
}
17+
18+
interface ConfigModalProps {
19+
isOpen: boolean
20+
onClose: () => void
21+
apiBase: string
22+
onConfigUpdated: () => void
23+
}
24+
25+
// Cache for drivers list - never changes during runtime
26+
let driversCache: Record<string, DriverInfo> | null = null
27+
// Cache for models per driver - never changes during runtime
28+
let modelsCache: Record<string, string[]> = {}
29+
30+
export function ConfigModal({ isOpen, onClose, apiBase, onConfigUpdated }: ConfigModalProps) {
31+
const [devices, setDevices] = useState<Device[]>([])
32+
const [drivers, setDrivers] = useState<Record<string, DriverInfo>>({})
33+
const [driverModels, setDriverModels] = useState<Record<string, string[]>>({})
34+
const [loading, setLoading] = useState(false)
35+
const [saving, setSaving] = useState(false)
36+
const [error, setError] = useState<string | null>(null)
37+
const [saveSuccess, setSaveSuccess] = useState(false)
38+
39+
useEffect(() => {
40+
if (isOpen) {
41+
loadConfig()
42+
loadDrivers()
43+
}
44+
}, [isOpen, apiBase])
45+
46+
async function loadDrivers() {
47+
// Use cache if available
48+
if (driversCache) {
49+
setDrivers(driversCache)
50+
// Also preload models for all drivers
51+
await preloadAllModels(Object.keys(driversCache))
52+
return
53+
}
54+
55+
try {
56+
const resp = await fetch(`${apiBase}/drivers`)
57+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
58+
const data = await resp.json()
59+
driversCache = data
60+
setDrivers(data)
61+
// Preload models for all drivers
62+
await preloadAllModels(Object.keys(data))
63+
} catch (e: any) {
64+
console.error('Failed to load drivers:', e)
65+
// Non-fatal - user can still type driver name manually
66+
}
67+
}
68+
69+
async function preloadAllModels(driverIds: string[]) {
70+
// Load models for all drivers in parallel
71+
const promises = driverIds.map(driverId => loadModelsForDriver(driverId))
72+
await Promise.all(promises)
73+
}
74+
75+
async function loadModelsForDriver(driverId: string) {
76+
// Check cache first
77+
if (modelsCache[driverId]) {
78+
setDriverModels(prev => ({ ...prev, [driverId]: modelsCache[driverId] }))
79+
return
80+
}
81+
82+
try {
83+
const resp = await fetch(`${apiBase}/drivers/${driverId}`)
84+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
85+
const models = await resp.json()
86+
modelsCache[driverId] = models
87+
setDriverModels(prev => ({ ...prev, [driverId]: models }))
88+
} catch (e: any) {
89+
console.error(`Failed to load models for ${driverId}:`, e)
90+
// Non-fatal
91+
}
92+
}
93+
94+
async function loadConfig() {
95+
setLoading(true)
96+
setError(null)
97+
try {
98+
const resp = await fetch(`${apiBase}/config`)
99+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
100+
const data = await resp.json()
101+
setDevices(data.devices || [])
102+
} catch (e: any) {
103+
setError(`Failed to load config: ${e.message}`)
104+
} finally {
105+
setLoading(false)
106+
}
107+
}
108+
109+
async function saveConfig() {
110+
setSaving(true)
111+
setError(null)
112+
setSaveSuccess(false)
113+
try {
114+
const resp = await fetch(`${apiBase}/config`, {
115+
method: 'POST',
116+
headers: { 'Content-Type': 'application/json' },
117+
body: JSON.stringify({ devices })
118+
})
119+
if (!resp.ok) {
120+
const errorData = await resp.json().catch(() => ({ detail: 'Unknown error' }))
121+
throw new Error(errorData.detail || `HTTP ${resp.status}`)
122+
}
123+
setSaveSuccess(true)
124+
setTimeout(() => {
125+
onConfigUpdated()
126+
onClose()
127+
}, 1000)
128+
} catch (e: any) {
129+
setError(`Failed to save config: ${e.message}`)
130+
} finally {
131+
setSaving(false)
132+
}
133+
}
134+
135+
function addDevice() {
136+
setDevices([...devices, {
137+
id: `device-${Date.now()}`,
138+
name: 'New Device',
139+
driver: '',
140+
port: '/dev/ttyUSB0',
141+
baud: 115200,
142+
serial: '8N1',
143+
model: ''
144+
}])
145+
}
146+
147+
function removeDevice(index: number) {
148+
setDevices(devices.filter((_, i) => i !== index))
149+
}
150+
151+
function updateDevice(index: number, field: keyof Device, value: any) {
152+
const updated = [...devices]
153+
updated[index] = { ...updated[index], [field]: value }
154+
setDevices(updated)
155+
}
156+
157+
if (!isOpen) return null
158+
159+
return (
160+
<div className="modal-overlay">
161+
<div className="modal-content">
162+
<div className="modal-header">
163+
<h2>Configuration</h2>
164+
<button className="modal-close" onClick={onClose} title="Close"></button>
165+
</div>
166+
167+
<div className="modal-body">
168+
{loading && <p className="subtle">Loading configuration...</p>}
169+
{error && <div className="error-message">{error}</div>}
170+
{saveSuccess && <div className="success-message">✓ Configuration saved and applied!</div>}
171+
172+
{!loading && (
173+
<>
174+
<div style={{ marginBottom: '16px', color: 'var(--text-2)', fontSize: '14px' }}>
175+
Configure your instruments below. Each device requires a unique ID, driver, and serial port.
176+
Changes will restart all instrument connections.
177+
</div>
178+
179+
{devices.length === 0 && (
180+
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-2)' }}>
181+
No devices configured. Click "Add Device" to get started.
182+
</div>
183+
)}
184+
185+
{devices.map((device, index) => (
186+
<div key={index} className="device-form">
187+
<div className="device-form-header">
188+
<h3>Device {index + 1}</h3>
189+
<button
190+
className="btn-remove"
191+
onClick={() => removeDevice(index)}
192+
title="Remove this device"
193+
>
194+
Remove
195+
</button>
196+
</div>
197+
198+
{/* Row 1: ID and Name */}
199+
<div className="form-grid">
200+
<div className="form-field">
201+
<label>
202+
ID<span className="required">*</span>
203+
<span className="field-hint">Unique identifier (e.g., psu-1, dmm-1)</span>
204+
</label>
205+
<input
206+
type="text"
207+
value={device.id}
208+
onChange={(e) => updateDevice(index, 'id', e.target.value)}
209+
placeholder="device-id"
210+
/>
211+
</div>
212+
213+
<div className="form-field">
214+
<label>
215+
Name<span className="required">*</span>
216+
<span className="field-hint">Display name for this device</span>
217+
</label>
218+
<input
219+
type="text"
220+
value={device.name}
221+
onChange={(e) => updateDevice(index, 'name', e.target.value)}
222+
placeholder="My Device"
223+
/>
224+
</div>
225+
</div>
226+
227+
{/* Row 2: Port, Baud Rate, Data Bits/Parity/Stop */}
228+
<div className="form-grid">
229+
<div className="form-field">
230+
<label>
231+
Port<span className="required">*</span>
232+
<span className="field-hint">Serial port path</span>
233+
</label>
234+
<input
235+
type="text"
236+
value={device.port}
237+
onChange={(e) => updateDevice(index, 'port', e.target.value)}
238+
placeholder="/dev/ttyUSB0"
239+
/>
240+
</div>
241+
242+
<div className="form-field">
243+
<label>
244+
Baud Rate<span className="required">*</span>
245+
<span className="field-hint">Communication speed (bps)</span>
246+
</label>
247+
<select
248+
value={device.baud}
249+
onChange={(e) => updateDevice(index, 'baud', parseInt(e.target.value))}
250+
>
251+
<option value={9600}>9600</option>
252+
<option value={19200}>19200</option>
253+
<option value={38400}>38400</option>
254+
<option value={57600}>57600</option>
255+
<option value={115200}>115200</option>
256+
</select>
257+
</div>
258+
259+
<div className="form-field">
260+
<label>
261+
Data Bits/Parity/Stop<span className="required">*</span>
262+
<span className="field-hint">Serial port configuration</span>
263+
</label>
264+
<select
265+
value={device.serial}
266+
onChange={(e) => updateDevice(index, 'serial', e.target.value)}
267+
>
268+
<option value="8N1">8N1 (8 bits, no parity, 1 stop)</option>
269+
<option value="8N2">8N2 (8 bits, no parity, 2 stop)</option>
270+
<option value="8E1">8E1 (8 bits, even parity, 1 stop)</option>
271+
<option value="8O1">8O1 (8 bits, odd parity, 1 stop)</option>
272+
<option value="7E1">7E1 (7 bits, even parity, 1 stop)</option>
273+
<option value="7O1">7O1 (7 bits, odd parity, 1 stop)</option>
274+
</select>
275+
</div>
276+
</div>
277+
278+
{/* Row 3: Driver and Model */}
279+
<div className="form-grid">
280+
<div className="form-field">
281+
<label>
282+
Driver<span className="required">*</span>
283+
<span className="field-hint">Driver module</span>
284+
</label>
285+
<select
286+
value={device.driver}
287+
onChange={(e) => updateDevice(index, 'driver', e.target.value)}
288+
>
289+
<option value="">Select driver...</option>
290+
{Object.keys(drivers).sort().map((driverId) => (
291+
<option key={driverId} value={driverId}>
292+
{drivers[driverId].vendor} {drivers[driverId].family} ({driverId})
293+
</option>
294+
))}
295+
</select>
296+
</div>
297+
298+
<div className="form-field">
299+
<label>
300+
Model<span className="required">*</span>
301+
<span className="field-hint">Device model number</span>
302+
</label>
303+
{device.driver && driverModels[device.driver] && driverModels[device.driver].length > 0 ? (
304+
<select
305+
value={device.model}
306+
onChange={(e) => updateDevice(index, 'model', e.target.value)}
307+
>
308+
<option value="">Select model...</option>
309+
{driverModels[device.driver].map((model) => (
310+
<option key={model} value={model}>
311+
{model}
312+
</option>
313+
))}
314+
</select>
315+
) : (
316+
<input
317+
type="text"
318+
value={device.model}
319+
onChange={(e) => updateDevice(index, 'model', e.target.value)}
320+
placeholder="SPM3103"
321+
/>
322+
)}
323+
</div>
324+
</div>
325+
</div>
326+
))}
327+
328+
<button className="btn-add" onClick={addDevice}>
329+
+ Add Device
330+
</button>
331+
</>
332+
)}
333+
</div>
334+
335+
<div className="modal-footer">
336+
<button className="btn-secondary" onClick={onClose} disabled={saving}>
337+
Cancel
338+
</button>
339+
<button
340+
className="btn-primary"
341+
onClick={saveConfig}
342+
disabled={saving || loading || devices.length === 0}
343+
>
344+
{saving ? 'Saving...' : 'Save & Apply'}
345+
</button>
346+
</div>
347+
</div>
348+
</div>
349+
)
350+
}

0 commit comments

Comments
 (0)