Files
Ota-dashboard/components/UpdateManager.tsx

200 lines
9.7 KiB
TypeScript
Raw Permalink Normal View History

2025-12-11 20:58:10 +01:00
'use client';
import { useState } from 'react';
import { FirmwareUpdate, Vehicle, VehicleGroup, uploadUpdate, createDeployment } from '@/lib/api';
import { Upload, Rocket, HardDrive, CheckCircle2, AlertCircle, Users } from 'lucide-react';
interface UpdateManagerProps {
updates: FirmwareUpdate[];
vehicles: Vehicle[];
groups: VehicleGroup[];
onUpdateDeployed: () => void;
}
export function UpdateManager({ updates, vehicles, groups, onUpdateDeployed }: UpdateManagerProps) {
const [file, setFile] = useState<File | null>(null);
const [version, setVersion] = useState('');
const [description, setDescription] = useState('');
const [uploading, setUploading] = useState(false);
const [selectedUpdate, setSelectedUpdate] = useState<number | null>(null);
const [targetType, setTargetType] = useState<'vehicle' | 'group'>('vehicle');
const [selectedTarget, setSelectedTarget] = useState<string>(''); // VIN or Group ID
const [deploying, setDeploying] = useState(false);
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
if (!file || !version) return;
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('version', version);
formData.append('description', description);
await uploadUpdate(formData);
onUpdateDeployed();
setFile(null);
setVersion('');
setDescription('');
} catch (err) {
console.error(err);
alert('Upload failed');
} finally {
setUploading(false);
}
};
const handleDeploy = async () => {
if (!selectedUpdate || !selectedTarget) return;
setDeploying(true);
try {
if (targetType === 'vehicle') {
await createDeployment(selectedUpdate, selectedTarget, undefined);
} else {
await createDeployment(selectedUpdate, undefined, parseInt(selectedTarget));
}
onUpdateDeployed();
alert('Deployment started!');
} catch (err) {
console.error(err);
alert('Deployment failed');
} finally {
setDeploying(false);
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upload Section */}
<div className="bg-zinc-900/50 backdrop-blur-md border border-zinc-800 rounded-xl p-6 shadow-xl">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<Upload className="w-5 h-5 text-blue-400" />
Upload Firmware
</h2>
<form onSubmit={handleUpload} className="space-y-4">
<div>
<label className="block text-sm font-medium text-zinc-400 mb-1">Version</label>
<input
type="text"
value={version}
onChange={e => setVersion(e.target.value)}
placeholder="e.g. 1.2.0"
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500 outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-zinc-400 mb-1">Description</label>
<input
type="text"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Critical security patch..."
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-zinc-400 mb-1">Binary File</label>
<div className="relative border-2 border-dashed border-zinc-800 rounded-lg p-4 hover:border-zinc-700 transition-colors text-center cursor-pointer group">
<input
type="file"
onChange={e => setFile(e.target.files?.[0] || null)}
className="absolute inset-0 opacity-0 cursor-pointer"
required
/>
<HardDrive className="w-6 h-6 text-zinc-600 mx-auto mb-2 group-hover:text-zinc-400 transition-colors" />
<p className="text-sm text-zinc-500">
{file ? file.name : "Click to select binary"}
</p>
</div>
</div>
<button
type="submit"
disabled={uploading}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
{uploading ? 'Uploading...' : 'Upload Firmware'}
</button>
</form>
</div>
{/* Deploy Section */}
<div className="bg-zinc-900/50 backdrop-blur-md border border-zinc-800 rounded-xl p-6 shadow-xl">
<h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
<Rocket className="w-5 h-5 text-purple-400" />
Deploy Update
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-zinc-400 mb-1">Select Firmware</label>
<select
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-white outline-none"
onChange={e => setSelectedUpdate(Number(e.target.value))}
value={selectedUpdate || ''}
>
<option value="">-- Select Version --</option>
{updates.map(u => (
<option key={u.id} value={u.id}>{u.version} - {u.description || 'No desc'}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-2 mb-2">
<button
onClick={() => setTargetType('vehicle')}
className={`py-2 text-sm rounded-lg border transition-all ${targetType === 'vehicle' ? 'bg-zinc-800 border-zinc-700 text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
>
Single Vehicle
</button>
<button
onClick={() => setTargetType('group')}
className={`py-2 text-sm rounded-lg border transition-all ${targetType === 'group' ? 'bg-zinc-800 border-zinc-700 text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
>
Entire Group
</button>
</div>
<div>
<label className="block text-sm font-medium text-zinc-400 mb-1">Target {targetType === 'vehicle' ? 'Vehicle' : 'Group'}</label>
{targetType === 'vehicle' ? (
<select
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-white outline-none"
onChange={e => setSelectedTarget(e.target.value)}
value={selectedTarget}
>
<option value="">-- Select Vehicle --</option>
{vehicles.map(v => (
<option key={v.vin} value={v.vin}>{v.vin} (v{v.currentVersion})</option>
))}
</select>
) : (
<select
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-white outline-none"
onChange={e => setSelectedTarget(e.target.value)}
value={selectedTarget}
>
<option value="">-- Select Group --</option>
{groups.map(g => (
<option key={g.id} value={g.id}>{g.name} ({g.vehicles?.length || 0} vehicles)</option>
))}
</select>
)}
</div>
<button
onClick={handleDeploy}
disabled={deploying || !selectedUpdate || !selectedTarget}
className="w-full bg-purple-600 hover:bg-purple-500 text-white font-medium py-2 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
{deploying ? 'Deploying...' : 'Deploy Update'}
</button>
</div>
{/* Recent Deployments Snippet could go here */}
</div>
</div>
);
}