Private
Public Access
1
0

feat: add patches missing filter and count indicator to deploy page
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 36s
CI Pipeline / Clippy Lints (push) Successful in 5m53s
CI Pipeline / Rust Unit Tests (push) Successful in 6m23s
CI Pipeline / Security Audit (push) Successful in 1m30s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 4m29s
CI Pipeline / Build .deb & Release (push) Has been skipped

This commit is contained in:
2026-05-04 18:56:52 +00:00
parent f2b5c0fad5
commit 1ba325d529
4 changed files with 41 additions and 7 deletions

View File

@ -112,6 +112,7 @@ pub struct HostSummary {
pub os_name: Option<String>, pub os_name: Option<String>,
pub health_status: HostHealthStatus, pub health_status: HostHealthStatus,
pub agent_version: Option<String>, pub agent_version: Option<String>,
pub patches_missing: i64,
pub registered_at: DateTime<Utc>, pub registered_at: DateTime<Utc>,
} }

View File

@ -109,10 +109,13 @@ async fn list_hosts(
let hosts: Vec<HostSummary> = if auth.role.is_admin() { let hosts: Vec<HostSummary> = if auth.role.is_admin() {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, fqdn, host(ip_address)::text AS ip_address, display_name, SELECT h.id, h.fqdn, host(h.ip_address)::text AS ip_address, h.display_name,
os_family, os_name, health_status, agent_version, registered_at h.os_family, h.os_name, h.health_status, h.agent_version,
FROM hosts COALESCE(hpd.patch_count, 0) AS patches_missing,
ORDER BY fqdn h.registered_at
FROM hosts h
LEFT JOIN host_patch_data hpd ON hpd.host_id = h.id
ORDER BY h.fqdn
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
"#, "#,
) )
@ -125,8 +128,11 @@ async fn list_hosts(
r#" r#"
SELECT DISTINCT h.id, h.fqdn, host(h.ip_address)::text AS ip_address, SELECT DISTINCT h.id, h.fqdn, host(h.ip_address)::text AS ip_address,
h.display_name, h.os_family, h.os_name, h.display_name, h.os_family, h.os_name,
h.health_status, h.agent_version, h.registered_at h.health_status, h.agent_version,
COALESCE(hpd.patch_count, 0) AS patches_missing,
h.registered_at
FROM hosts h FROM hosts h
LEFT JOIN host_patch_data hpd ON hpd.host_id = h.id
WHERE WHERE
-- Hosts in operator's groups -- Hosts in operator's groups
EXISTS ( EXISTS (

View File

@ -52,6 +52,7 @@ export default function PatchDeploymentPage() {
const [hostsError, setHostsError] = useState<string | null>(null) const [hostsError, setHostsError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [healthFilter, setHealthFilter] = useState<HostHealthStatus | ''>('') const [healthFilter, setHealthFilter] = useState<HostHealthStatus | ''>('')
const [patchesFilter, setPatchesFilter] = useState<'all' | 'missing' | 'uptodate'>('all')
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()) const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
// Step 1 state // Step 1 state
@ -89,7 +90,11 @@ export default function PatchDeploymentPage() {
h.display_name.toLowerCase().includes(searchQuery.toLowerCase()) || h.display_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.fqdn.toLowerCase().includes(searchQuery.toLowerCase()) h.fqdn.toLowerCase().includes(searchQuery.toLowerCase())
const matchesHealth = healthFilter === '' || h.health_status === healthFilter const matchesHealth = healthFilter === '' || h.health_status === healthFilter
return matchesSearch && matchesHealth const matchesPatches =
patchesFilter === 'all' ||
(patchesFilter === 'missing' && h.patches_missing > 0) ||
(patchesFilter === 'uptodate' && h.patches_missing === 0)
return matchesSearch && matchesHealth && matchesPatches
}) })
const handleToggleHost = (id: string) => { const handleToggleHost = (id: string) => {
@ -209,6 +214,19 @@ export default function PatchDeploymentPage() {
<option value="unreachable">Unreachable</option> <option value="unreachable">Unreachable</option>
<option value="pending">Pending</option> <option value="pending">Pending</option>
</TextField> </TextField>
<TextField
select
size="small"
label="Patches Missing"
value={patchesFilter}
onChange={(e) => setPatchesFilter(e.target.value as 'all' | 'missing' | 'uptodate')}
SelectProps={{ native: true }}
sx={{ minWidth: 160 }}
>
<option value="all">All</option>
<option value="missing">Missing (&gt;0)</option>
<option value="uptodate">Up to date (0)</option>
</TextField>
</Box> </Box>
{hostsLoading ? ( {hostsLoading ? (
@ -238,13 +256,14 @@ export default function PatchDeploymentPage() {
<TableCell>FQDN</TableCell> <TableCell>FQDN</TableCell>
<TableCell>IP Address</TableCell> <TableCell>IP Address</TableCell>
<TableCell>Health</TableCell> <TableCell>Health</TableCell>
<TableCell>Patches</TableCell>
<TableCell>OS</TableCell> <TableCell>OS</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredHosts.length === 0 ? ( {filteredHosts.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={6} align="center"> <TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary" py={2}> <Typography variant="body2" color="text.secondary" py={2}>
No hosts found No hosts found
</Typography> </Typography>
@ -272,6 +291,13 @@ export default function PatchDeploymentPage() {
<TableCell> <TableCell>
<HealthChip status={host.health_status} /> <HealthChip status={host.health_status} />
</TableCell> </TableCell>
<TableCell>
<Chip
label={host.patches_missing}
color={host.patches_missing > 0 ? 'error' : 'success'}
size="small"
/>
</TableCell>
<TableCell> <TableCell>
{host.os_name ?? host.os_family ?? '—'} {host.os_name ?? host.os_family ?? '—'}
</TableCell> </TableCell>

View File

@ -24,6 +24,7 @@ export interface Host {
os_family?: string os_family?: string
os_name?: string os_name?: string
agent_version?: string agent_version?: string
patches_missing: number
registered_at: string registered_at: string
} }