feat(M3): Host Management, Groups, Users, CIDR Discovery
- pm-core::models: Host, HostSummary, Group, User, DiscoveryResult types + request payloads for all CRUD operations - pm-core::audit: Tamper-evident hash-chained audit log writer (SHA-256 chain, non-fatal, covers all M3 events) - pm-web/routes/hosts: Full host CRUD with RBAC scoping; FQDN DNS resolution on registration; host↔group membership; operator group-scoped access enforcement; audit on register/remove - pm-web/routes/groups: Full group CRUD; host↔group and user↔group membership management; admin-only create/delete/update - pm-web/routes/users: Full user CRUD (admin); current user profile; password hashing (Argon2id); role management; session revocation - pm-web/routes/discovery: CIDR scan with bounded concurrency (128 workers), TCP probe with 2s timeout, reverse DNS lookup, scan results table, register-from-discovery flow with audit log - Frontend: HostsPage (filterable table with health chips), HostDetailPage, GroupsPage (create/delete dialog), UsersPage (create/revoke sessions) - App.tsx updated with all M3 routes wired to real pages - cargo check --workspace: zero errors Closes M3.
This commit is contained in:
41
frontend/src/pages/HostDetailPage.tsx
Normal file
41
frontend/src/pages/HostDetailPage.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Alert, Box, Button, Chip, CircularProgress, Container, Divider, Grid, Paper, Typography } from '@mui/material'
|
||||
import { ArrowBack } from '@mui/icons-material'
|
||||
import { apiClient } from '../api/client'
|
||||
|
||||
export default function HostDetailPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [host, setHost] = useState<Record<string, unknown> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
apiClient.get(`/hosts/${id}`)
|
||||
.then(r => setHost(r.data))
|
||||
.catch(() => setError('Host not found or access denied.'))
|
||||
.finally(() => setLoading(false))
|
||||
}, [id])
|
||||
|
||||
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>
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 3 }}>
|
||||
<Button startIcon={<ArrowBack />} onClick={() => navigate('/hosts')} sx={{ mb: 2 }}>Back to Hosts</Button>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h5" fontWeight={700} mb={2}>{String(host?.fqdn ?? '')}</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Grid container spacing={2}>
|
||||
{host && Object.entries(host).map(([k, v]) => v !== null && v !== '' ? (
|
||||
<Grid item 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>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user