Files
Redflag/aggregator-web/src/components/AgentUpdate.tsx

229 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { Upload, CheckCircle, XCircle, RotateCw, Download } from 'lucide-react';
import { useAgentUpdate } from '@/hooks/useAgentUpdate';
import { Agent } from '@/types';
import { cn } from '@/lib/utils';
import toast from 'react-hot-toast';
interface AgentUpdateProps {
agent: Agent;
onUpdateComplete?: () => void;
className?: string;
}
export function AgentUpdate({ agent, onUpdateComplete, className }: AgentUpdateProps) {
const {
checkForUpdate,
triggerAgentUpdate,
updateStatus,
checkingUpdate,
updatingAgent,
hasUpdate,
availableVersion,
currentVersion
} = useAgentUpdate();
const [isChecking, setIsChecking] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
const handleCheckUpdate = async (e: React.MouseEvent) => {
e.stopPropagation();
setIsChecking(true);
try {
await checkForUpdate(agent.id);
setHasChecked(true);
if (hasUpdate && availableVersion) {
setShowConfirmDialog(true);
} else if (!hasUpdate && hasChecked) {
toast.info('Agent is already at latest version');
}
} catch (error) {
console.error('[UI] Failed to check for updates:', error);
toast.error('Failed to check for available updates');
} finally {
setIsChecking(false);
}
};
const handleConfirmUpdate = async () => {
if (!hasUpdate || !availableVersion) {
toast.error('No update available');
return;
}
setShowConfirmDialog(false);
try {
await triggerAgentUpdate(agent, availableVersion);
if (onUpdateComplete) {
onUpdateComplete();
}
} catch (error) {
console.error('[UI] Update failed:', error);
}
};
const buttonContent = () => {
if (updatingAgent) {
return (
<>
<RotateCw className="h-4 w-4 animate-spin" />
<span>
{updateStatus.status === 'downloading' && 'Downloading...'}
{updateStatus.status === 'installing' && 'Installing...'}
{updateStatus.status === 'pending' && 'Starting update...'}
</span>
</>
);
}
if (agent.is_updating) {
return (
<>
<RotateCw className="h-4 w-4 animate-pulse" />
<span>Updating...</span>
</>
);
}
if (isChecking) {
return (
<>
<RotateCw className="h-4 w-4" />
<span>Checking...</span>
</>
);
}
if (hasChecked && hasUpdate) {
return (
<>
<Download className="h-4 w-4" />
<span>Update to {availableVersion}</span>
</>
);
}
return (
<>
<Upload className="h-4 w-4" />
<span>Check for Update</span>
</>
);
};
return (
<div className="inline-flex items-center">
<button
onClick={handleCheckUpdate}
disabled={updatingAgent || agent.is_updating || isChecking}
className={cn(
"text-sm px-3 py-1 rounded border flex items-center space-x-2 transition-colors",
{
"bg-green-50 border-green-300 text-green-700 hover:bg-green-100": hasChecked && hasUpdate,
"bg-amber-50 border-amber-300 text-amber-700": updatingAgent || agent.is_updating,
"text-gray-600 hover:text-primary-600 border-gray-300 bg-white hover:bg-primary-50":
!updatingAgent && !agent.is_updating && !hasUpdate
},
className
)}
title={updatingAgent ? "Updating agent..." : agent.is_updating ? "Agent is updating..." : "Check for available updates"}
>
{buttonContent()}
</button>
{/* Progress indicator */}
{updatingAgent && updateStatus.progress && (
<div className="ml-2 w-16 h-2 bg-gray-200 rounded">
<div
className="h-2 bg-green-500 rounded"
style={{ width: `${updateStatus.progress}%` }}
/>
</div>
)}
{/* Status icon */}
{hasChecked && !updatingAgent && (
<div className="ml-2">
{hasUpdate ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-gray-400" />
)}
</div>
)}
{/* Version info popup */}
{hasChecked && (
<div className="ml-2 text-xs text-gray-500">
{currentVersion} {hasUpdate ? availableVersion : 'Latest'}
</div>
)}
{/* Confirmation Dialog */}
{showConfirmDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg max-w-md mx-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900">
Update Agent: {agent.hostname}
</h3>
{/* Warning for same-version updates */}
{currentVersion === availableVersion ? (
<>
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded">
<p className="text-amber-800 font-medium mb-2">
Version appears identical
</p>
<p className="text-sm text-amber-700 mb-2">
Current: <strong>{currentVersion}</strong> Target: <strong>{availableVersion}</strong>
</p>
<p className="text-xs text-amber-600">
This will reinstall the current version. Useful if the binary was rebuilt or corrupted.
</p>
</div>
<p className="mb-4 text-sm text-gray-600">
The agent will be temporarily offline during reinstallation.
</p>
</>
) : (
<>
<p className="mb-4 text-gray-600">
Update agent from <strong>{currentVersion}</strong> to <strong>{availableVersion}</strong>?
</p>
<p className="mb-4 text-sm text-gray-500">
This will temporarily take the agent offline during the update process.
</p>
</>
)}
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowConfirmDialog(false)}
className="px-4 py-2 text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleConfirmUpdate}
className={cn(
"px-4 py-2 rounded hover:bg-primary-700",
currentVersion === availableVersion
? "bg-amber-600 text-white hover:bg-amber-700"
: "bg-primary-600 text-white"
)}
>
{currentVersion === availableVersion ? 'Reinstall Agent' : 'Update Agent'}
</button>
</div>
</div>
</div>
)}
</div>
);
}