import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Search, Package, Download, Upload, CheckCircle, RefreshCw, Terminal, Filter, ChevronDown, ChevronRight, Check, X, } from 'lucide-react'; import { formatRelativeTime, formatBytes } from '@/lib/utils'; import { updateApi, agentApi } from '@/lib/api'; import toast from 'react-hot-toast'; import { cn } from '@/lib/utils'; import type { UpdatePackage } from '@/types'; import { AgentUpdatesModal } from './AgentUpdatesModal'; interface AgentUpdatesEnhancedProps { agentId: string; } interface AgentUpdateResponse { updates: UpdatePackage[]; total: number; } interface CommandResponse { command_id: string; status: string; message: string; } interface LogResponse { stdout: string; stderr: string; exit_code: number; duration_seconds: number; result: string; } type StatusTab = 'pending' | 'approved' | 'installing' | 'installed'; export function AgentUpdatesEnhanced({ agentId }: AgentUpdatesEnhancedProps) { const [activeStatus, setActiveStatus] = useState('pending'); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [searchTerm, setSearchTerm] = useState(''); const [selectedSeverity, setSelectedSeverity] = useState('all'); const [showLogsModal, setShowLogsModal] = useState(false); const [logsData, setLogsData] = useState(null); const [showUpdateModal, setShowUpdateModal] = useState(false); const [expandedUpdates, setExpandedUpdates] = useState>(new Set()); const [selectedUpdates, setSelectedUpdates] = useState([]); const queryClient = useQueryClient(); // Fetch updates with status filter const { data: updateData, isLoading, error, refetch } = useQuery({ queryKey: ['agent-updates', agentId, activeStatus, currentPage, pageSize, searchTerm, selectedSeverity], queryFn: async () => { const params = { page: currentPage, page_size: pageSize, agent_id: agentId, status: activeStatus, ...(searchTerm && { search: searchTerm }), ...(selectedSeverity !== 'all' && { severity: selectedSeverity }), }; const response = await updateApi.getUpdates(params); return response; }, refetchInterval: 30000, }); // Mutations const approveMutation = useMutation({ mutationFn: async (updateId: string) => { const response = await updateApi.approveUpdate(updateId); return response; }, onSuccess: () => { toast.success('Update approved'); refetch(); queryClient.invalidateQueries({ queryKey: ['agent-updates'] }); }, onError: (error: any) => { toast.error(`Failed to approve: ${error.message || 'Unknown error'}`); }, }); const installMutation = useMutation({ mutationFn: async (updateId: string) => { const response = await agentApi.installUpdate(agentId, updateId); return response; }, onSuccess: () => { toast.success('Installation started'); setTimeout(() => { refetch(); queryClient.invalidateQueries({ queryKey: ['active-commands'] }); }, 2000); }, onError: (error: any) => { toast.error(`Failed to install: ${error.message || 'Unknown error'}`); }, }); const bulkApproveMutation = useMutation({ mutationFn: async (updateIds: string[]) => { const response = await updateApi.approveMultiple(updateIds); return response; }, onSuccess: () => { toast.success(`${selectedUpdates.length} updates approved`); setSelectedUpdates([]); refetch(); }, onError: (error: any) => { toast.error(`Failed to approve: ${error.message || 'Unknown error'}`); }, }); const getLogsMutation = useMutation({ mutationFn: async (commandId: string) => { setIsLoadingLogs(true); const response = await agentApi.getCommandLogs(agentId, commandId); return response; }, onSuccess: (data: LogResponse) => { setLogsData(data); setShowLogsModal(true); }, onError: (error: any) => { toast.error(`Failed to fetch logs: ${error.message || 'Unknown error'}`); }, onSettled: () => { setIsLoadingLogs(false); }, }); const updates = updateData?.updates || []; const totalCount = updateData?.total || 0; const totalPages = Math.ceil(totalCount / pageSize); const getSeverityColor = (severity: string) => { switch (severity.toLowerCase()) { case 'critical': return 'text-red-600 bg-red-50'; case 'important': case 'high': return 'text-orange-600 bg-orange-50'; case 'moderate': case 'medium': return 'text-yellow-600 bg-yellow-50'; case 'low': case 'none': return 'text-blue-600 bg-blue-50'; default: return 'text-gray-600 bg-gray-50'; } }; const handleSelectUpdate = (updateId: string, checked: boolean) => { if (checked) { setSelectedUpdates([...selectedUpdates, updateId]); } else { setSelectedUpdates(selectedUpdates.filter(id => id !== updateId)); } }; const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedUpdates(updates.map((update: UpdatePackage) => update.id)); } else { setSelectedUpdates([]); } }; const handleApprove = async (updateId: string) => { approveMutation.mutate(updateId); }; const handleInstall = async (updateId: string) => { installMutation.mutate(updateId); }; const handleBulkApprove = async () => { if (selectedUpdates.length === 0) { toast.error('Select at least one update'); return; } bulkApproveMutation.mutate(selectedUpdates); }; const handleViewLogs = async (update: UpdatePackage) => { const recentCommand = update.recent_command_id; if (recentCommand) { getLogsMutation.mutate(recentCommand); } else { toast.error('No recent command logs available for this package'); } }; const toggleExpanded = (updateId: string) => { const newExpanded = new Set(expandedUpdates); if (newExpanded.has(updateId)) { newExpanded.delete(updateId); } else { newExpanded.add(updateId); } setExpandedUpdates(newExpanded); }; if (isLoading) { return (
{[...Array(5)].map((_, i) => (
))}
); } if (error) { return (
Error loading updates: {(error as Error).message}
); } return (
{/* Tabs */}
{[ { key: 'pending', label: 'Pending' }, { key: 'approved', label: 'Approved' }, { key: 'installing', label: 'Installing' }, { key: 'installed', label: 'Installed' }, ].map((tab) => ( ))}
{/* Filters and Actions */}
{totalCount} update{totalCount !== 1 ? 's' : ''} {['critical', 'high', 'medium', 'low'].map((severity) => { const count = updates.filter(u => u.severity?.toLowerCase() === severity).length; if (count === 0) return null; return ( {count} {severity} ); })}
{selectedUpdates.length > 0 && activeStatus === 'pending' && ( )} {/* Update Agent Button */}
{/* Search and Filters */}
setSearchTerm(e.target.value)} placeholder="Search packages..." className="pl-9 pr-3 py-1.5 w-full border border-gray-300 rounded text-sm" />
{/* Updates List */} {updates.length === 0 ? (
{activeStatus === 'installed' ? (

Installed updates are shown in History

) : ( `No ${activeStatus} updates` )}
) : (
{updates.map((update) => { const isExpanded = expandedUpdates.has(update.id); return (
{/* Checkbox for pending */} {activeStatus === 'pending' && ( handleSelectUpdate(update.id, e.target.checked)} onClick={(e) => e.stopPropagation()} className="h-4 w-4 rounded border-gray-300" /> )} {/* Main content */}
toggleExpanded(update.id)} >
{update.severity.toUpperCase()} {update.package_name} {update.current_version} → {update.available_version}
{activeStatus === 'pending' && ( )} {activeStatus === 'approved' && ( )} {update.recent_command_id && ( )} {isExpanded ? ( ) : ( )}
{/* Expanded Details */} {isExpanded && (
{update.metadata?.description && (

{update.metadata.description}

)}
Type: {update.package_type}
Severity: {update.severity}
{update.metadata?.size_bytes && (
Size: {formatBytes(update.metadata.size_bytes)}
)} {update.last_discovered_at && (
Discovered: {formatRelativeTime(update.last_discovered_at)}
)} {update.approved_at && (
Approved: {formatRelativeTime(update.approved_at)}
)} {update.installed_at && (
Installed: {formatRelativeTime(update.installed_at)}
)}
)}
); })}
)} {/* Pagination */} {totalPages > 1 && (
{Math.min((currentPage - 1) * pageSize + 1, totalCount)} - {Math.min(currentPage * pageSize, totalCount)} of {totalCount}
Page {currentPage} of {totalPages}
)} {/* Logs Modal */} {showLogsModal && logsData && (

Installation Logs

Result: {logsData.result || 'Unknown'}
Exit Code: {logsData.exit_code}
Duration: {logsData.duration_seconds}s
{logsData.stdout && (

Standard Output

                    {logsData.stdout}
                  
)} {logsData.stderr && (

Standard Error

                    {logsData.stderr}
                  
)}
)} {/* Agent Update Modal */} setShowUpdateModal(false)} selectedAgentIds={[agentId]} onAgentsUpdated={() => { setShowUpdateModal(false); queryClient.invalidateQueries({ queryKey: ['agents'] }); }} />
); }