From 1aa90c7eb0ab5f34e8133d1a59385191d22eeeda Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 5 May 2026 20:48:14 +0000 Subject: [PATCH] fix: add host creation form for Add Host button --- frontend/src/api/client.ts | 2 + frontend/src/pages/HostDetailPage.tsx | 108 +++++++++++++++++++++++++- frontend/src/types/index.ts | 8 ++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 6d0afd9..d1f7290 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2,6 +2,7 @@ import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios' import { useAuthStore } from '../store/authStore' import type { FleetStatus, + CreateHostRequest, CreateJobRequest, CreateMaintenanceWindowRequest, UpdateMaintenanceWindowRequest, @@ -110,6 +111,7 @@ export const fleetApi = { export const hostsApi = { list: (params?: Record) => apiClient.get('/hosts', { params }), get: (id: string) => apiClient.get(`/hosts/${id}`), + register: (body: CreateHostRequest) => apiClient.post('/hosts', body), delete: (id: string) => apiClient.delete(`/hosts/${id}`), refresh: (id: string) => apiClient.post(`/hosts/${id}/refresh`), } diff --git a/frontend/src/pages/HostDetailPage.tsx b/frontend/src/pages/HostDetailPage.tsx index 4cbfa8e..cab0e04 100644 --- a/frontend/src/pages/HostDetailPage.tsx +++ b/frontend/src/pages/HostDetailPage.tsx @@ -44,8 +44,9 @@ import { Schedule as ScheduleIcon, VpnKey as VpnKeyIcon, } from '@mui/icons-material' -import { apiClient, maintenanceWindowsApi, healthChecksApi, certsApi } from '../api/client' +import { apiClient, hostsApi, maintenanceWindowsApi, healthChecksApi, certsApi } from '../api/client' import type { + CreateHostRequest, MaintenanceWindow, WindowRecurrence, HealthCheckType, @@ -305,6 +306,105 @@ function HealthCheckFormDialog({ open, title, initial, onClose, onSubmit }: Heal ) } +// ── Create Host Form ────────────────────────────────────────────────────────── + +function CreateHostForm() { + const navigate = useNavigate() + const [form, setForm] = useState({ + fqdn: '', + display_name: '', + agent_port: 12443, + notes: '', + }) + const [saving, setSaving] = useState(false) + const [err, setErr] = useState(null) + + const set = (field: keyof CreateHostRequest, value: CreateHostRequest[keyof CreateHostRequest]) => + setForm(prev => ({ ...prev, [field]: value })) + + const handleSubmit = async () => { + if (!form.fqdn.trim()) { setErr('FQDN is required'); return } + setSaving(true); setErr(null) + try { + const body: CreateHostRequest = { + fqdn: form.fqdn.trim(), + } + if (form.display_name?.trim()) body.display_name = form.display_name.trim() + if (form.agent_port) body.agent_port = form.agent_port + if (form.notes?.trim()) body.notes = form.notes.trim() + const res = await hostsApi.register(body) + const newId = res.data?.id ?? res.data?.host?.id + if (newId) { + navigate(`/hosts/${newId}`) + } else { + navigate('/hosts') + } + } catch (e: unknown) { + const msg = (e as { response?: { data?: { error?: { message?: string } } } }) + ?.response?.data?.error?.message ?? 'Failed to register host' + setErr(msg) + } finally { setSaving(false) } + } + + return ( + + + + + Register New Host + + + {err && {err}} + + set('fqdn', e.target.value)} + required + fullWidth + helperText="Fully qualified domain name (IP address resolved automatically)" + /> + set('display_name', e.target.value)} + fullWidth + helperText="Optional friendly name for this host" + /> + set('agent_port', parseInt(e.target.value, 10) || 12443)} + fullWidth + slotProps={{ htmlInput: { min: 1, max: 65535 } }} + helperText="Port the patch agent listens on (default 12443)" + /> + set('notes', e.target.value)} + fullWidth + multiline + rows={3} + helperText="Optional notes about this host" + /> + + + + + + + + ) +} + // ── Main page ────────────────────────────────────────────────────────────────── export default function HostDetailPage() { @@ -354,6 +454,7 @@ export default function HostDetailPage() { // ── Fetch host ──────────────────────────────────────────────────────────── useEffect(() => { + if (id === 'new') { setLoading(false); return } apiClient.get(`/hosts/${id}`) .then(r => setHost(r.data)) .catch(() => setError('Host not found or access denied.')) @@ -566,6 +667,11 @@ export default function HostDetailPage() { if (loading) return if (error) return {error} + // ── New host creation form ───────────────────────────────────────────────── + if (id === 'new') { + return + } + return (