Files
Redflag/aggregator-web/src/components/AgentUpdatesModal.tsx
Fimeg 1f2b1b7179 fix: repair version detection platform query format
- Fix GetLatestVersionByTypeAndArch to separate platform/architecture
- Query now correctly uses platform='linux' and architecture='amd64'
- Resolves UI showing 'no packages available' despite updates existing
2025-11-10 20:11:32 -05:00

309 lines
12 KiB
TypeScript

import { useState } from 'react';
import {
X,
Download,
CheckCircle,
AlertCircle,
RefreshCw,
Info,
Users,
Package,
Hash,
} from 'lucide-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { agentApi, updateApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
import { Agent } from '@/types';
interface AgentUpdatesModalProps {
isOpen: boolean;
onClose: () => void;
selectedAgentIds: string[];
onAgentsUpdated: () => void;
}
export function AgentUpdatesModal({
isOpen,
onClose,
selectedAgentIds,
onAgentsUpdated,
}: AgentUpdatesModalProps) {
const [selectedVersion, setSelectedVersion] = useState('');
const [selectedPlatform, setSelectedPlatform] = useState('');
const [isUpdating, setIsUpdating] = useState(false);
// Fetch selected agents details
const { data: agents = [] } = useQuery<Agent[]>({
queryKey: ['agents-details', selectedAgentIds],
queryFn: async (): Promise<Agent[]> => {
const promises = selectedAgentIds.map(id => agentApi.getAgent(id));
const results = await Promise.all(promises);
return results;
},
enabled: isOpen && selectedAgentIds.length > 0,
});
// Fetch available update packages
const { data: packagesResponse, isLoading: packagesLoading } = useQuery({
queryKey: ['update-packages'],
queryFn: () => updateApi.getPackages(),
enabled: isOpen,
});
const packages = packagesResponse?.packages || [];
// Group packages by version
const versions = [...new Set(packages.map(pkg => pkg.version))].sort((a, b) => b.localeCompare(a));
const platforms = [...new Set(packages.map(pkg => pkg.platform))].sort();
// Filter packages based on selection
const availablePackages = packages.filter(
pkg => (!selectedVersion || pkg.version === selectedVersion) &&
(!selectedPlatform || pkg.platform === selectedPlatform)
);
// Get unique platform for selected agents (simplified - assumes all agents same platform)
const agentPlatform = agents[0]?.os_type || 'linux';
const agentArchitecture = agents[0]?.os_architecture || 'amd64';
// Update agents mutation
const updateAgentsMutation = useMutation({
mutationFn: async (packageId: string) => {
const pkg = packages.find(p => p.id === packageId);
if (!pkg) throw new Error('Package not found');
// For single agent updates, use individual update with nonce for security
if (selectedAgentIds.length === 1) {
const agentId = selectedAgentIds[0];
// Generate nonce for security
const nonceData = await agentApi.generateUpdateNonce(agentId, pkg.version);
console.log('[UI] Update nonce generated for single agent:', nonceData);
// Use individual update endpoint with nonce
return agentApi.updateAgent(agentId, {
version: pkg.version,
platform: pkg.platform,
nonce: nonceData.update_nonce
});
}
// For multiple agents, use bulk update
const updateData = {
agent_ids: selectedAgentIds,
version: pkg.version,
platform: pkg.platform,
};
return agentApi.updateMultipleAgents(updateData);
},
onSuccess: (data) => {
const count = selectedAgentIds.length;
const message = count === 1 ? 'Update initiated for agent' : `Update initiated for ${count} agents`;
toast.success(message);
setIsUpdating(false);
onAgentsUpdated();
onClose();
},
onError: (error: any) => {
toast.error(`Failed to update agents: ${error.message}`);
setIsUpdating(false);
},
});
const handleUpdateAgents = async (packageId: string) => {
setIsUpdating(true);
updateAgentsMutation.mutate(packageId);
};
const canUpdate = selectedAgentIds.length > 0 && availablePackages.length > 0 && !isUpdating;
const hasUpdatingAgents = agents.some(agent => agent.is_updating);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose} />
<div className="relative w-full max-w-4xl transform overflow-hidden rounded-lg bg-white p-6 text-left shadow-xl transition-all">
{/* Header */}
<div className="flex items-center justify-between pb-4 border-b">
<div className="flex items-center space-x-3">
<Download className="h-6 w-6 text-primary-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Agent Updates</h3>
<p className="text-sm text-gray-500">
Update {selectedAgentIds.length} agent{selectedAgentIds.length !== 1 ? 's' : ''}
</p>
</div>
</div>
<button
onClick={onClose}
className="rounded-md p-2 text-gray-400 hover:text-gray-500"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Content */}
<div className="py-4">
{/* Selected Agents */}
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-900 mb-3 flex items-center">
<Users className="h-4 w-4 mr-2" />
Selected Agents
</h4>
<div className="max-h-32 overflow-y-auto space-y-2">
{agents.map((agent) => (
<div key={agent.id} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<div className="flex items-center space-x-3">
<CheckCircle className={cn(
"h-4 w-4",
agent.status === 'online' ? "text-green-500" : "text-gray-400"
)} />
<div>
<div className="text-sm font-medium text-gray-900">{agent.hostname}</div>
<div className="text-xs text-gray-500">
{agent.os_type}/{agent.os_architecture} Current: {agent.current_version || 'Unknown'}
</div>
</div>
</div>
{agent.is_updating && (
<div className="flex items-center text-amber-600 text-xs">
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
Updating to {agent.updating_to_version}
</div>
)}
</div>
))}
</div>
{hasUpdatingAgents && (
<div className="mt-2 text-xs text-amber-600 flex items-center">
<AlertCircle className="h-3 w-3 mr-1" />
Some agents are currently updating
</div>
)}
</div>
{/* Package Selection */}
<div className="mb-6">
<h4 className="text-sm font-medium text-gray-900 mb-3 flex items-center">
<Package className="h-4 w-4 mr-2" />
Update Package Selection
</h4>
{/* Filters */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Version</label>
<select
value={selectedVersion}
onChange={(e) => setSelectedVersion(e.target.value)}
className="w-full rounded-md border-gray-300 shadow-sm text-sm"
>
<option value="">All Versions</option>
{versions.map(version => (
<option key={version} value={version}>{version}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Platform</label>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
className="w-full rounded-md border-gray-300 shadow-sm text-sm"
>
<option value="">All Platforms</option>
{platforms.map(platform => (
<option key={platform} value={platform}>{platform}</option>
))}
</select>
</div>
</div>
{/* Available Packages */}
<div className="space-y-2 max-h-48 overflow-y-auto">
{packagesLoading ? (
<div className="text-center py-4 text-sm text-gray-500">
Loading packages...
</div>
) : availablePackages.length === 0 ? (
<div className="text-center py-4 text-sm text-gray-500">
No packages available for the selected criteria
</div>
) : (
availablePackages.map((pkg) => (
<div
key={pkg.id}
className={cn(
"flex items-center justify-between p-3 border rounded-lg cursor-pointer transition-colors",
"hover:bg-gray-50 border-gray-200"
)}
onClick={() => handleUpdateAgents(pkg.id)}
>
<div className="flex items-center space-x-3">
<Package className="h-4 w-4 text-primary-600" />
<div>
<div className="text-sm font-medium text-gray-900">
Version {pkg.version}
</div>
<div className="text-xs text-gray-500">
{pkg.platform} {(pkg.file_size / 1024 / 1024).toFixed(1)} MB
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-400">
<Hash className="h-3 w-3 inline mr-1" />
{pkg.checksum.slice(0, 8)}...
</div>
<button
disabled={!canUpdate}
className={cn(
"px-3 py-1 text-xs rounded-md font-medium transition-colors",
canUpdate
? "bg-primary-600 text-white hover:bg-primary-700"
: "bg-gray-100 text-gray-400 cursor-not-allowed"
)}
>
{isUpdating ? (
<RefreshCw className="h-3 w-3 animate-spin" />
) : (
'Update'
)}
</button>
</div>
</div>
))
)}
</div>
</div>
{/* Platform Compatibility Info */}
<div className="text-xs text-gray-500 flex items-start">
<Info className="h-3 w-3 mr-1 mt-0.5 flex-shrink-0" />
<span>
Detected platform: <strong>{agentPlatform}/{agentArchitecture}</strong>.
Only compatible packages will be shown.
</span>
</div>
</div>
{/* Footer */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<button
onClick={onClose}
disabled={isUpdating}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
>
Cancel
</button>
</div>
</div>
</div>
</div>
);
}