Private
Public Access
1
0

fix: add host creation form for Add Host button
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 5s
CI Pipeline / Clippy Lints (push) Successful in 46s
CI Pipeline / Rust Unit Tests (push) Successful in 1m3s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 10s
CI Pipeline / Build .deb & Release (push) Has been skipped

This commit is contained in:
2026-05-05 20:48:14 +00:00
parent 0cfe8ba891
commit 1aa90c7eb0
3 changed files with 117 additions and 1 deletions

View File

@ -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<string, unknown>) => 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`),
}

View File

@ -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<CreateHostRequest>({
fqdn: '',
display_name: '',
agent_port: 12443,
notes: '',
})
const [saving, setSaving] = useState(false)
const [err, setErr] = useState<string | null>(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 (
<Container maxWidth="sm" sx={{ mt: 3, mb: 6 }}>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/hosts')} sx={{ mb: 2 }}>
Back to Hosts
</Button>
<Paper sx={{ p: 3 }}>
<Typography variant="h5" fontWeight={700} gutterBottom>
Register New Host
</Typography>
<Divider sx={{ mb: 3 }} />
{err && <Alert severity="error" sx={{ mb: 2 }}>{err}</Alert>}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="FQDN"
value={form.fqdn}
onChange={e => set('fqdn', e.target.value)}
required
fullWidth
helperText="Fully qualified domain name (IP address resolved automatically)"
/>
<TextField
label="Display Name"
value={form.display_name ?? ''}
onChange={e => set('display_name', e.target.value)}
fullWidth
helperText="Optional friendly name for this host"
/>
<TextField
label="Agent Port"
type="number"
value={form.agent_port ?? 12443}
onChange={e => 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)"
/>
<TextField
label="Notes"
value={form.notes ?? ''}
onChange={e => set('notes', e.target.value)}
fullWidth
multiline
rows={3}
helperText="Optional notes about this host"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 3 }}>
<Button onClick={() => navigate('/hosts')} disabled={saving}>
Cancel
</Button>
<Button variant="contained" onClick={handleSubmit} disabled={saving}>
{saving ? <CircularProgress size={20} /> : 'Register Host'}
</Button>
</Box>
</Paper>
</Container>
)
}
// ── 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 <Box display="flex" justifyContent="center" mt={8}><CircularProgress /></Box>
if (error) return <Container sx={{ mt: 4 }}><Alert severity="error">{error}</Alert></Container>
// ── New host creation form ─────────────────────────────────────────────────
if (id === 'new') {
return <CreateHostForm />
}
return (
<Container maxWidth="lg" sx={{ mt: 3, mb: 6 }}>
<Button startIcon={<ArrowBack />} onClick={() => navigate('/hosts')} sx={{ mb: 2 }}>

View File

@ -29,6 +29,14 @@ export interface Host {
health_check_status?: 'all_healthy' | 'some_unhealthy' | 'none'
}
export interface CreateHostRequest {
fqdn: string
display_name?: string
agent_port?: number
notes?: string
group_ids?: string[]
}
export interface Group {
id: string
name: string