ui: unify per-scanner package rows into one System Update Scanner row
The agent runs apt/dnf/winget/windows on their own schedules (per-scanner
agent_subsystems rows still exist server-side) but the UI now collapses
them into a single "System Update Scanner" row. Chips on the description
line (APT, DNF, WINGET, Windows Update) light up from
agent.metadata.available_scanners — the real signal from
syncAvailableScanners — rather than the old os_type string heuristic
that sniffed "fedora" / "debian" out of the platform field.
The row is synthesized client-side from the per-scanner backers; no
'updates' DB row needed (the zombie kill landed in 7fb61a36). Toggle,
auto-run, interval, and Scan on the unified row cascade to every backing
per-scanner subsystem that actually exists for the agent.
Widened AgentSubsystem.subsystem type to string — the narrow union was
stale (DB has apt/dnf/winget/windows/storage/system/docker, plus the
synthetic 'updates' aggregate the UI produces).
Removed the os-string heuristic helper and its console.log noise.
This commit is contained in:
parent
7fb61a36ba
commit
03c72cb438
2 changed files with 87 additions and 67 deletions
|
|
@ -123,6 +123,54 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
const hasReportedScanners = reportedScanners.size > 0;
|
||||
const scannerSubsystems = new Set(['apt', 'dnf', 'winget', 'windows', 'docker']);
|
||||
|
||||
// Package-manager scanners are surfaced through one unified "System Update
|
||||
// Scanner" row. The agent runs each scanner on its own schedule (per-scanner
|
||||
// rows still exist in agent_subsystems), but the UI collapses them.
|
||||
const packageScannerNames = ['apt', 'dnf', 'winget', 'windows'] as const;
|
||||
const packageScannerSet: Set<string> = new Set(packageScannerNames);
|
||||
|
||||
// Returns the per-scanner subsystems currently backing the unified row.
|
||||
const backingScanners = subsystems.filter(s => packageScannerSet.has(s.subsystem));
|
||||
|
||||
// Build the unified row from backing per-scanner subsystems. Returns null
|
||||
// when no package scanners exist for this agent (no row to render).
|
||||
const synthesizeUpdatesRow = (): AgentSubsystem | null => {
|
||||
if (backingScanners.length === 0) return null;
|
||||
|
||||
const intervals = backingScanners.map(s => s.interval_minutes).filter(n => n > 0);
|
||||
const lastRuns = backingScanners
|
||||
.map(s => s.last_run_at)
|
||||
.filter((t): t is string => !!t)
|
||||
.sort();
|
||||
const nextRuns = backingScanners
|
||||
.map(s => s.next_run_at)
|
||||
.filter((t): t is string => !!t)
|
||||
.sort();
|
||||
|
||||
return {
|
||||
id: `synth-updates-${agentId}`,
|
||||
agent_id: agentId,
|
||||
subsystem: 'updates',
|
||||
enabled: backingScanners.some(s => s.enabled),
|
||||
auto_run: backingScanners.some(s => s.auto_run && s.enabled),
|
||||
interval_minutes: intervals.length > 0 ? Math.min(...intervals) : 60,
|
||||
last_run_at: lastRuns.length > 0 ? lastRuns[lastRuns.length - 1] : null,
|
||||
next_run_at: nextRuns.length > 0 ? nextRuns[0] : null,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
};
|
||||
|
||||
// Final list rendered in the table: synthesized updates row first (when
|
||||
// backing scanners exist), then every non-package-scanner subsystem. The
|
||||
// per-scanner backing rows are not rendered — they're folded into the
|
||||
// unified row above.
|
||||
const updatesRow = synthesizeUpdatesRow();
|
||||
const displaySubsystems: AgentSubsystem[] = [
|
||||
...(updatesRow ? [updatesRow] : []),
|
||||
...subsystems.filter(s => !packageScannerSet.has(s.subsystem) && s.subsystem !== 'updates'),
|
||||
];
|
||||
|
||||
// Get security icon for subsystem type
|
||||
const getSecurityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
|
|
@ -215,46 +263,31 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
},
|
||||
});
|
||||
|
||||
// When the unified 'updates' row is acted on, cascade to every backing
|
||||
// per-scanner subsystem that actually exists for this agent.
|
||||
const cascadeTargets = (subsystem: string): string[] => {
|
||||
if (subsystem !== 'updates') return [subsystem];
|
||||
return backingScanners.map(s => s.subsystem);
|
||||
};
|
||||
|
||||
const handleToggleEnabled = (subsystem: string, currentEnabled: boolean) => {
|
||||
toggleSubsystemMutation.mutate({ subsystem, enabled: !currentEnabled });
|
||||
const targets = cascadeTargets(subsystem);
|
||||
targets.forEach(t => toggleSubsystemMutation.mutate({ subsystem: t, enabled: !currentEnabled }));
|
||||
};
|
||||
|
||||
const handleIntervalChange = (subsystem: string, intervalMinutes: number) => {
|
||||
updateIntervalMutation.mutate({ subsystem, intervalMinutes });
|
||||
const targets = cascadeTargets(subsystem);
|
||||
targets.forEach(t => updateIntervalMutation.mutate({ subsystem: t, intervalMinutes }));
|
||||
};
|
||||
|
||||
const handleToggleAutoRun = (subsystem: string, currentAutoRun: boolean) => {
|
||||
toggleAutoRunMutation.mutate({ subsystem, autoRun: !currentAutoRun });
|
||||
const targets = cascadeTargets(subsystem);
|
||||
targets.forEach(t => toggleAutoRunMutation.mutate({ subsystem: t, autoRun: !currentAutoRun }));
|
||||
};
|
||||
|
||||
const handleTriggerScan = async (subsystem: string) => {
|
||||
// Handle 'updates' subsystem - map to correct platform-specific scanner
|
||||
if (subsystem === 'updates') {
|
||||
const os = agent?.os_type?.toLowerCase() || '';
|
||||
console.log('[AgentHealth] Triggering updates scan, OS type:', agent?.os_type);
|
||||
|
||||
if (os.includes('debian') || os.includes('ubuntu')) {
|
||||
console.log('[AgentHealth] Triggering scan_apt');
|
||||
triggerScanMutation.mutate('apt');
|
||||
} else if (os.includes('fedora') || os.includes('rhel') || os.includes('centos')) {
|
||||
console.log('[AgentHealth] Triggering scan_dnf');
|
||||
triggerScanMutation.mutate('dnf');
|
||||
} else if (os.includes('windows')) {
|
||||
console.log('[AgentHealth] Triggering scan_windows + scan_winget');
|
||||
// Windows has two scanners - trigger both
|
||||
try {
|
||||
await triggerScanMutation.mutateAsync('windows');
|
||||
await triggerScanMutation.mutateAsync('winget');
|
||||
} catch (err) {
|
||||
// handled by mutation onError
|
||||
}
|
||||
} else {
|
||||
console.log('[AgentHealth] Unknown OS type, triggering scan_dnf as fallback');
|
||||
triggerScanMutation.mutate('dnf');
|
||||
}
|
||||
} else {
|
||||
triggerScanMutation.mutate(subsystem);
|
||||
}
|
||||
const handleTriggerScan = (subsystem: string) => {
|
||||
const targets = cascadeTargets(subsystem);
|
||||
targets.forEach(t => triggerScanMutation.mutate(t));
|
||||
};
|
||||
|
||||
const frequencyOptions = [
|
||||
|
|
@ -269,36 +302,12 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
{ value: 20160, label: '2 weeks' },
|
||||
];
|
||||
|
||||
// Calculate counts directly without useMemo
|
||||
const enabledCount = subsystems.filter(s => s.enabled).length;
|
||||
const autoRunCount = subsystems.filter(s => s.auto_run && s.enabled).length;
|
||||
|
||||
// Helper functions for package manager status (used in updates row description)
|
||||
const getPackageManagerStatus = (pm: string, osType: string) => {
|
||||
const os = (osType || '').toLowerCase();
|
||||
console.log('[AgentHealth] Package manager check:', pm, 'for OS:', osType);
|
||||
|
||||
let enabled = false;
|
||||
switch (pm) {
|
||||
case 'apt':
|
||||
enabled = os.includes('debian') || os.includes('ubuntu');
|
||||
break;
|
||||
case 'dnf':
|
||||
enabled = os.includes('fedora') || os.includes('rhel') || os.includes('red hat') || os.includes('centos');
|
||||
break;
|
||||
case 'winget':
|
||||
enabled = os.includes('windows');
|
||||
break;
|
||||
case 'windows':
|
||||
enabled = os.includes('windows');
|
||||
break;
|
||||
default:
|
||||
enabled = false;
|
||||
}
|
||||
console.log('[AgentHealth] Package manager', pm, 'enabled:', enabled);
|
||||
return enabled;
|
||||
};
|
||||
// Counts reflect what the user sees in the table (unified row + non-package
|
||||
// subsystems), not raw DB backer rows.
|
||||
const enabledCount = displaySubsystems.filter(s => s.enabled).length;
|
||||
const autoRunCount = displaySubsystems.filter(s => s.auto_run && s.enabled).length;
|
||||
|
||||
// Chip palette for the unified row's per-scanner availability indicators.
|
||||
const getPackageManagerBadgeStyle = (pm: string) => {
|
||||
switch (pm) {
|
||||
case 'apt': return 'bg-purple-100 text-purple-700';
|
||||
|
|
@ -317,7 +326,7 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-900">Subsystems</h3>
|
||||
<p className="text-xs text-gray-600 mt-0.5">
|
||||
{enabledCount} enabled • {autoRunCount} auto-running • {subsystems.length} total
|
||||
{enabledCount} enabled • {autoRunCount} auto-running • {displaySubsystems.length} total
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -358,7 +367,7 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{subsystems.map((subsystem: AgentSubsystem) => {
|
||||
{displaySubsystems.map((subsystem: AgentSubsystem) => {
|
||||
const config = subsystemConfig[subsystem.subsystem] || {
|
||||
icon: <Activity className="h-4 w-4" />,
|
||||
name: subsystem.subsystem,
|
||||
|
|
@ -400,16 +409,22 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
|
|||
{subsystem.subsystem === 'updates' ? (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span>Scans for available package updates (</span>
|
||||
{['apt', 'dnf', 'winget', 'windows'].map((pm, index) => {
|
||||
const isEnabled = getPackageManagerStatus(pm, agent?.os_type || '');
|
||||
const isLast = index === 3;
|
||||
{packageScannerNames.map((pm, index) => {
|
||||
// Chip lights up when the agent has reported
|
||||
// this scanner as available. When no scanners
|
||||
// have been reported yet, fall back to the
|
||||
// presence of a backing subsystem row.
|
||||
const isAvailable = hasReportedScanners
|
||||
? reportedScanners.has(pm)
|
||||
: backingScanners.some(b => b.subsystem === pm);
|
||||
const isLast = index === packageScannerNames.length - 1;
|
||||
|
||||
return (
|
||||
<span key={pm}>
|
||||
{index > 0 && ', '}
|
||||
<span className={cn(
|
||||
'text-[10px] px-1 py-0.5 rounded',
|
||||
isEnabled
|
||||
isAvailable
|
||||
? getPackageManagerBadgeStyle(pm)
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
)}>
|
||||
|
|
|
|||
|
|
@ -395,10 +395,15 @@ export interface RateLimitSummary {
|
|||
}
|
||||
|
||||
// Subsystem types
|
||||
//
|
||||
// `subsystem` is a string because the server creates per-scanner rows (apt,
|
||||
// dnf, winget, windows) in addition to the original aggregate subsystems
|
||||
// (storage, system, docker). The 'updates' value is now only produced by the
|
||||
// UI as a synthetic aggregate row over the per-scanner backers — no DB row.
|
||||
export interface AgentSubsystem {
|
||||
id: string;
|
||||
agent_id: string;
|
||||
subsystem: 'updates' | 'storage' | 'system' | 'docker';
|
||||
subsystem: string;
|
||||
enabled: boolean;
|
||||
interval_minutes: number;
|
||||
auto_run: boolean;
|
||||
|
|
|
|||
Loading…
Reference in a new issue