feat: Updates page UI improvements and Windows agent enhancements

- Redesigned statistics cards with combined layout
- Added quick filters for Installing, Installed, Failed, Dependencies
- Implemented column sorting for all table headers
- Added package name truncation to prevent layout stretching
- Fixed TypeScript types for new update statuses
- Updated screenshots and README
This commit is contained in:
Fimeg
2025-10-17 22:40:40 -04:00
parent 4ef5216c89
commit d1c5cb9597
17 changed files with 5933 additions and 195 deletions

4189
aggregator-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,8 @@
"axios": "^1.6.2",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"prism-react-renderer": "^2.4.1",
"prismjs": "^1.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.6.0",

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ import { getStatusColor, formatRelativeTime, isOnline, formatBytes } from '@/lib
import { cn } from '@/lib/utils';
import toast from 'react-hot-toast';
import { AgentSystemUpdates } from '@/components/AgentUpdates';
import HistoryTimeline from '@/components/HistoryTimeline';
import ChatTimeline from '@/components/ChatTimeline';
const Agents: React.FC = () => {
const { id } = useParams<{ id?: string }>();
@@ -631,7 +631,7 @@ const Agents: React.FC = () => {
</div>
) : (
<div>
<HistoryTimeline agentId={selectedAgent.id} />
<ChatTimeline agentId={selectedAgent.id} isScopedView={true} />
</div>
)}
</div>

View File

@@ -1,72 +1,91 @@
import React from 'react';
import React, { useState } from 'react';
import {
History,
Calendar,
Clock,
CheckCircle,
AlertTriangle,
Search,
RefreshCw,
} from 'lucide-react';
import HistoryTimeline from '@/components/HistoryTimeline';
import ChatTimeline from '@/components/ChatTimeline';
import { useQuery } from '@tanstack/react-query';
import { logApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
const HistoryPage: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
// Debounce search query
React.useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 300);
return () => {
clearTimeout(timer);
};
}, [searchQuery]);
const { data: historyData, isLoading, refetch, isFetching } = useQuery({
queryKey: ['history', { search: debouncedSearchQuery }],
queryFn: async () => {
try {
const params: any = {
page: 1,
page_size: 50,
};
if (debouncedSearchQuery) {
params.search = debouncedSearchQuery;
}
const response = await logApi.getAllLogs(params);
return response;
} catch (error) {
console.error('Failed to fetch history:', error);
toast.error('Failed to fetch history');
return { logs: [], total: 0, page: 1, page_size: 50 };
}
},
refetchInterval: 30000,
});
return (
<div className="px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6">
<div className="flex items-center space-x-3 mb-2">
<History className="h-8 w-8 text-indigo-600" />
<h1 className="text-2xl font-bold text-gray-900">History & Audit Log</h1>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-3">
<History className="h-8 w-8 text-indigo-600" />
<h1 className="text-2xl font-bold text-gray-900">History & Audit Log</h1>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search events..."
className="pl-10 pr-4 py-2 w-64 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<button
onClick={() => refetch()}
disabled={isFetching}
className="flex items-center space-x-2 px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium transition-colors disabled:opacity-50"
>
<RefreshCw className={cn("h-4 w-4", isFetching && "animate-spin")} />
<span>Refresh</span>
</button>
</div>
</div>
<p className="text-gray-600">
Complete chronological timeline of all system activities across all agents
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Activities</p>
<p className="text-2xl font-bold text-gray-900">--</p>
</div>
<History className="h-8 w-8 text-indigo-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-green-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Successful</p>
<p className="text-2xl font-bold text-green-600">--</p>
</div>
<CheckCircle className="h-8 w-8 text-green-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-red-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Failed</p>
<p className="text-2xl font-bold text-red-600">--</p>
</div>
<AlertTriangle className="h-8 w-8 text-red-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-blue-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Today</p>
<p className="text-2xl font-bold text-blue-600">--</p>
</div>
<Calendar className="h-8 w-8 text-blue-400" />
</div>
</div>
</div>
{/* Timeline */}
<HistoryTimeline />
<ChatTimeline isScopedView={false} externalSearch={debouncedSearchQuery} />
</div>
);
};

View File

@@ -12,10 +12,12 @@ import {
ChevronRight,
AlertTriangle,
Clock,
Calendar,
X,
Loader2,
RotateCcw,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from 'lucide-react';
import { useUpdates, useUpdate, useApproveUpdate, useRejectUpdate, useInstallUpdate, useApproveMultipleUpdates, useRetryCommand, useCancelCommand } from '@/hooks/useUpdates';
import { useRecentCommands } from '@/hooks/useCommands';
@@ -38,6 +40,8 @@ const Updates: React.FC = () => {
const [severityFilter, setSeverityFilter] = useState(searchParams.get('severity') || '');
const [typeFilter, setTypeFilter] = useState(searchParams.get('type') || '');
const [agentFilter, setAgentFilter] = useState(searchParams.get('agent') || '');
const [sortBy, setSortBy] = useState(searchParams.get('sort_by') || '');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(searchParams.get('sort_order') as 'asc' | 'desc' || 'desc');
// Debounce search query to avoid API calls on every keystroke
useEffect(() => {
@@ -70,6 +74,8 @@ const Updates: React.FC = () => {
if (severityFilter) params.set('severity', severityFilter);
if (typeFilter) params.set('type', typeFilter);
if (agentFilter) params.set('agent', agentFilter);
if (sortBy) params.set('sort_by', sortBy);
if (sortOrder) params.set('sort_order', sortOrder);
if (currentPage > 1) params.set('page', currentPage.toString());
if (pageSize !== 100) params.set('page_size', pageSize.toString());
@@ -77,7 +83,7 @@ const Updates: React.FC = () => {
if (newUrl !== window.location.href) {
window.history.replaceState({}, '', newUrl);
}
}, [debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, currentPage, pageSize]);
}, [debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, sortBy, sortOrder, currentPage, pageSize]);
// Fetch updates list
const { data: updatesData, isPending, error } = useUpdates({
@@ -86,6 +92,8 @@ const Updates: React.FC = () => {
severity: severityFilter || undefined,
type: typeFilter || undefined,
agent: agentFilter || undefined,
sort_by: sortBy || undefined,
sort_order: sortOrder || undefined,
page: currentPage,
page_size: pageSize,
});
@@ -262,6 +270,22 @@ const Updates: React.FC = () => {
setStatusFilter('approved');
setSeverityFilter('');
break;
case 'installing':
setStatusFilter('installing');
setSeverityFilter('');
break;
case 'installed':
setStatusFilter('installed');
setSeverityFilter('');
break;
case 'failed':
setStatusFilter('failed');
setSeverityFilter('');
break;
case 'dependencies':
setStatusFilter('pending_dependencies');
setSeverityFilter('');
break;
default:
// Clear all filters
setStatusFilter('');
@@ -273,35 +297,32 @@ const Updates: React.FC = () => {
setCurrentPage(1);
};
// Group updates
const groupUpdates = (updates: UpdatePackage[], groupBy: string) => {
const groups: Record<string, UpdatePackage[]> = {};
updates.forEach(update => {
let key: string;
switch (groupBy) {
case 'severity':
key = update.severity;
break;
case 'type':
key = update.package_type;
break;
case 'status':
key = update.status;
break;
default:
key = 'all';
}
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(update);
});
return groups;
// Handle column sorting
const handleSort = (column: string) => {
if (sortBy === column) {
// Toggle sort order if clicking the same column
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
// Set new column with default desc order
setSortBy(column);
setSortOrder('desc');
}
setCurrentPage(1);
};
// Render sort icon for column headers
const renderSortIcon = (column: string) => {
if (sortBy !== column) {
return <ArrowUpDown className="h-4 w-4 ml-1 text-gray-400" />;
}
return sortOrder === 'asc' ? (
<ArrowUp className="h-4 w-4 ml-1 text-primary-600" />
) : (
<ArrowDown className="h-4 w-4 ml-1 text-primary-600" />
);
};
// Get total statistics from API (not just current page)
const totalStats = {
total: totalCount,
@@ -890,8 +911,9 @@ const Updates: React.FC = () => {
</div>
</div>
{/* Statistics Cards - Show total counts across all updates */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
{/* Statistics Cards - Compact design with combined visual boxes */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{/* Total Updates - Standalone */}
<div className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
@@ -902,43 +924,51 @@ const Updates: React.FC = () => {
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-orange-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Pending</p>
<p className="text-2xl font-bold text-orange-600">{totalStats.pending}</p>
{/* Approved / Pending - Combined with divider */}
<div className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center justify-between divide-x divide-gray-200">
<div className="flex-1 pr-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-gray-600">Approved</p>
<p className="text-xl font-bold text-green-600">{totalStats.approved}</p>
</div>
<CheckCircle className="h-6 w-6 text-green-400" />
</div>
</div>
<div className="flex-1 pl-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-gray-600">Pending</p>
<p className="text-xl font-bold text-orange-600">{totalStats.pending}</p>
</div>
<Clock className="h-6 w-6 text-orange-400" />
</div>
</div>
<Clock className="h-8 w-8 text-orange-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-green-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Approved</p>
<p className="text-2xl font-bold text-green-600">{totalStats.approved}</p>
{/* Critical / High Priority - Combined with divider */}
<div className="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center justify-between divide-x divide-gray-200">
<div className="flex-1 pr-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-gray-600">Critical</p>
<p className="text-xl font-bold text-red-600">{totalStats.critical}</p>
</div>
<AlertTriangle className="h-6 w-6 text-red-400" />
</div>
</div>
<CheckCircle className="h-8 w-8 text-green-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-red-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Critical</p>
<p className="text-2xl font-bold text-red-600">{totalStats.critical}</p>
<div className="flex-1 pl-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs font-medium text-gray-600">High Priority</p>
<p className="text-xl font-bold text-yellow-600">{totalStats.high}</p>
</div>
<AlertTriangle className="h-6 w-6 text-yellow-400" />
</div>
</div>
<AlertTriangle className="h-8 w-8 text-red-400" />
</div>
</div>
<div className="bg-white p-4 rounded-lg border border-yellow-200 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">High Priority</p>
<p className="text-2xl font-bold text-yellow-600">{totalStats.high}</p>
</div>
<AlertTriangle className="h-8 w-8 text-yellow-400" />
</div>
</div>
</div>
@@ -992,6 +1022,54 @@ const Updates: React.FC = () => {
<CheckCircle className="h-4 w-4 mr-1 inline" />
Approved
</button>
<button
onClick={() => handleQuickFilter('installing')}
className={cn(
"px-4 py-2 text-sm font-medium rounded-lg border transition-colors",
statusFilter === 'installing'
? "bg-blue-100 border-blue-300 text-blue-700"
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
)}
>
<Loader2 className="h-4 w-4 mr-1 inline" />
Installing
</button>
<button
onClick={() => handleQuickFilter('installed')}
className={cn(
"px-4 py-2 text-sm font-medium rounded-lg border transition-colors",
statusFilter === 'installed'
? "bg-emerald-100 border-emerald-300 text-emerald-700"
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
)}
>
<CheckCircle className="h-4 w-4 mr-1 inline" />
Installed
</button>
<button
onClick={() => handleQuickFilter('failed')}
className={cn(
"px-4 py-2 text-sm font-medium rounded-lg border transition-colors",
statusFilter === 'failed'
? "bg-red-100 border-red-300 text-red-700"
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
)}
>
<XCircle className="h-4 w-4 mr-1 inline" />
Failed
</button>
<button
onClick={() => handleQuickFilter('dependencies')}
className={cn(
"px-4 py-2 text-sm font-medium rounded-lg border transition-colors",
statusFilter === 'pending_dependencies'
? "bg-amber-100 border-amber-300 text-amber-700"
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
)}
>
<AlertTriangle className="h-4 w-4 mr-1 inline" />
Dependencies
</button>
</div>
</div>
@@ -1161,13 +1239,69 @@ const Updates: React.FC = () => {
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
</th>
<th className="table-header">Package</th>
<th className="table-header">Type</th>
<th className="table-header">Versions</th>
<th className="table-header">Severity</th>
<th className="table-header">Status</th>
<th className="table-header">Agent</th>
<th className="table-header">Discovered</th>
<th className="table-header">
<button
onClick={() => handleSort('package_name')}
className="flex items-center hover:text-primary-600 font-medium"
>
Package
{renderSortIcon('package_name')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('package_type')}
className="flex items-center hover:text-primary-600 font-medium"
>
Type
{renderSortIcon('package_type')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('available_version')}
className="flex items-center hover:text-primary-600 font-medium"
>
Versions
{renderSortIcon('available_version')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('severity')}
className="flex items-center hover:text-primary-600 font-medium"
>
Severity
{renderSortIcon('severity')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('status')}
className="flex items-center hover:text-primary-600 font-medium"
>
Status
{renderSortIcon('status')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('agent_id')}
className="flex items-center hover:text-primary-600 font-medium"
>
Agent
{renderSortIcon('agent_id')}
</button>
</th>
<th className="table-header">
<button
onClick={() => handleSort('created_at')}
className="flex items-center hover:text-primary-600 font-medium"
>
Discovered
{renderSortIcon('created_at')}
</button>
</th>
<th className="table-header">Actions</th>
</tr>
</thead>
@@ -1185,11 +1319,12 @@ const Updates: React.FC = () => {
<td className="table-cell">
<div className="flex items-center space-x-3">
<span className="text-xl">{getPackageTypeIcon(update.package_type)}</span>
<div>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-gray-900">
<button
onClick={() => navigate(`/updates/${update.id}`)}
className="hover:text-primary-600"
className="hover:text-primary-600 truncate block max-w-xs"
title={update.package_name}
>
{update.package_name}
</button>

View File

@@ -46,7 +46,7 @@ export interface UpdatePackage {
current_version: string;
available_version: string;
severity: 'low' | 'medium' | 'high' | 'critical';
status: 'pending' | 'approved' | 'scheduled' | 'installing' | 'installed' | 'failed';
status: 'pending' | 'approved' | 'scheduled' | 'installing' | 'installed' | 'failed' | 'checking_dependencies' | 'pending_dependencies';
created_at: string;
updated_at: string;
approved_at: string | null;
@@ -248,6 +248,7 @@ export interface ScanRequest {
// Query parameters
export interface ListQueryParams {
page?: number;
page_size?: number;
limit?: number;
status?: string;
severity?: string;