200 lines
9.7 KiB
TypeScript
200 lines
9.7 KiB
TypeScript
'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>
|
|
);
|
|
}
|