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
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:
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 (>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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user