Private
Public Access
1
0

feat: add host editing endpoint and frontend UI
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 3s
CI Pipeline / Clippy Lints (push) Successful in 53s
CI Pipeline / Rust Unit Tests (push) Successful in 1m12s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 14s
CI Pipeline / Build .deb & Release (push) Successful in 3m51s

This commit is contained in:
2026-05-18 21:52:00 +00:00
parent b3ae42215b
commit f70c5e53f9
26 changed files with 254 additions and 65 deletions

View File

@ -152,6 +152,8 @@ 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),
update: (id: string, body: Record<string, string | undefined>) =>
apiClient.put(`/hosts/${id}`, body),
delete: (id: string) => apiClient.delete(`/hosts/${id}`),
refresh: (id: string) => apiClient.post(`/hosts/${id}/refresh`),
}

View File

@ -614,6 +614,46 @@ export default function HostDetailPage() {
// Hosts list for target_host_id dropdown
const [hosts, setHosts] = useState<{ id: string; display_name: string; fqdn: string }[]>([])
// ── Host editing state ────────────────────────────────────────────────────
const [editing, setEditing] = useState(false)
const [editFqdn, setEditFqdn] = useState('')
const [editIp, setEditIp] = useState('')
const [editDisplayName, setEditDisplayName] = useState('')
const [savingHost, setSavingHost] = useState(false)
const enterEdit = () => {
setEditFqdn(String(host?.fqdn ?? ''))
setEditIp(String(host?.ip_address ?? ''))
setEditDisplayName(String(host?.display_name ?? ''))
setEditing(true)
}
const cancelEdit = () => {
setEditing(false)
setSavingHost(false)
}
const handleSaveHost = async () => {
if (!id) return
setSavingHost(true)
try {
const res = await hostsApi.update(id, {
fqdn: editFqdn !== String(host?.fqdn ?? '') ? editFqdn : undefined,
ip_address: editIp !== String(host?.ip_address ?? '') ? editIp : undefined,
display_name: editDisplayName !== String(host?.display_name ?? '') ? editDisplayName : undefined,
})
setHost(res.data)
setEditing(false)
showSnack('Host updated', 'success')
} catch (e: unknown) {
const msg = (e as { response?: { data?: { error?: { message?: string } } } })
?.response?.data?.error?.message ?? 'Failed to update host'
showSnack(msg, 'error')
} finally {
setSavingHost(false)
}
}
// ── Fetch host ────────────────────────────────────────────────────────────
useEffect(() => {
if (id === 'new') { setLoading(false); return }
@ -899,7 +939,39 @@ export default function HostDetailPage() {
{String(host?.fqdn ?? '')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{canWrite && !certExists && (
{canWrite && !editing && (
<Button
variant="outlined"
size="small"
startIcon={<EditIcon />}
onClick={enterEdit}
>
Edit
</Button>
)}
{canWrite && editing && (
<>
<Button
variant="contained"
size="small"
startIcon={<CheckCircleIcon />}
onClick={handleSaveHost}
disabled={savingHost}
>
{savingHost ? <CircularProgress size={16} /> : 'Save'}
</Button>
<Button
variant="outlined"
size="small"
startIcon={<CancelIcon />}
onClick={cancelEdit}
disabled={savingHost}
>
Cancel
</Button>
</>
)}
{!editing && canWrite && !certExists && (
<Button
variant="contained"
size="small"
@ -909,7 +981,7 @@ export default function HostDetailPage() {
Issue Certificate
</Button>
)}
{canWrite && certExists && (
{!editing && canWrite && certExists && (
<Button
variant="outlined"
size="small"
@ -920,21 +992,46 @@ export default function HostDetailPage() {
Re-issue Certificate
</Button>
)}
</Box>
</Box>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{host && Object.entries(host).map(([k, v]) =>
v !== null && v !== '' ? (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={k}>
<Typography variant="caption" color="text.secondary" display="block">
{k.replace(/_/g, ' ').toUpperCase()}
</Typography>
<Typography variant="body2">{String(v)}</Typography>
</Grid>
) : null
)}
{host && (<>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Typography variant="caption" color="text.secondary" display="block">FQDN</Typography>
{editing ? (
<TextField size="small" fullWidth value={editFqdn} onChange={e => setEditFqdn(e.target.value)} />
) : (
<Typography variant="body2">{String(host.fqdn)}</Typography>
)}
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Typography variant="caption" color="text.secondary" display="block">IP ADDRESS</Typography>
{editing ? (
<TextField size="small" fullWidth value={editIp} onChange={e => setEditIp(e.target.value)} />
) : (
<Typography variant="body2">{String(host.ip_address)}</Typography>
)}
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Typography variant="caption" color="text.secondary" display="block">DISPLAY NAME</Typography>
{editing ? (
<TextField size="small" fullWidth value={editDisplayName} onChange={e => setEditDisplayName(e.target.value)} />
) : (
<Typography variant="body2">{String(host.display_name)}</Typography>
)}
</Grid>
{Object.entries(host).filter(([k]) => !['fqdn','ip_address','display_name'].includes(k)).map(([k, v]) =>
v !== null && v !== '' ? (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={k}>
<Typography variant="caption" color="text.secondary" display="block">
{k.replace(/_/g, ' ').toUpperCase()}
</Typography>
<Typography variant="body2">{String(v)}</Typography>
</Grid>
) : null
)}
</>)}
</Grid>
</Paper>

View File

@ -37,6 +37,12 @@ export interface CreateHostRequest {
group_ids?: string[]
}
export interface UpdateHostRequest {
fqdn?: string
ip_address?: string
display_name?: string
}
export interface Group {
id: string
name: string