Phase 6
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||
import type { Interface } from './lib/types';
|
||||
|
||||
interface ServerInterface {
|
||||
id: string;
|
||||
name: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onLoad: (xml: string) => void;
|
||||
onSelect?: (id: string) => void;
|
||||
onEdit?: (iface?: Interface) => void;
|
||||
}
|
||||
|
||||
export default function InterfaceList({ onLoad, onSelect, onEdit }: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [interfaces, setInterfaces] = useState<ServerInterface[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function refresh() {
|
||||
setLoading(true);
|
||||
fetch('/api/v1/interfaces')
|
||||
.then(r => r.ok ? r.json() : [])
|
||||
.then(data => setInterfaces(data))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
useEffect(() => { refresh(); }, []);
|
||||
|
||||
function triggerImport() {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
|
||||
async function handleFileChange(ev: Event) {
|
||||
const input = ev.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const xml = await file.text();
|
||||
onLoad(xml);
|
||||
} catch (err) {
|
||||
console.error('Failed to read interface file:', err);
|
||||
} finally {
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string, name: string) {
|
||||
if (!confirm(`Delete interface "${name}"?`)) return;
|
||||
const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
if (res.ok) refresh();
|
||||
}
|
||||
|
||||
async function handleClone(id: string) {
|
||||
const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(id)}/clone`, { method: 'POST' });
|
||||
if (res.ok) refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<aside class={`panel${collapsed ? ' collapsed' : ''}`}>
|
||||
<div class="panel-header">
|
||||
{!collapsed && <span class="panel-title">Interfaces</span>}
|
||||
<button
|
||||
class="icon-btn"
|
||||
onClick={() => setCollapsed(c => !c)}
|
||||
title={collapsed ? 'Expand panel' : 'Collapse panel'}
|
||||
>
|
||||
{collapsed ? '▶' : '◀'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<div class="panel-actions">
|
||||
<button class="panel-btn" onClick={() => onEdit?.()}>+ New</button>
|
||||
<button class="panel-btn" onClick={triggerImport}>Import</button>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xml,application/xml,text/xml"
|
||||
style="display:none"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel-list">
|
||||
{loading ? (
|
||||
<p class="hint">Loading…</p>
|
||||
) : interfaces.length === 0 ? (
|
||||
<p class="hint">No interfaces yet. Create one or import XML.</p>
|
||||
) : (
|
||||
<ul class="iface-list">
|
||||
{interfaces.map(iface => (
|
||||
<li key={iface.id} class="iface-item">
|
||||
<span class="iface-item-name" onClick={() => onSelect?.(iface.id)}>
|
||||
{iface.name || iface.id}
|
||||
</span>
|
||||
<div class="iface-item-actions">
|
||||
<button class="icon-btn" title="Edit" onClick={() => onEdit?.()}>✎</button>
|
||||
<button class="icon-btn" title="Clone" onClick={() => handleClone(iface.id)}>⎘</button>
|
||||
<button class="icon-btn iface-delete" title="Delete" onClick={() => handleDelete(iface.id, iface.name)}>✕</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user