Commit of Current Status
This commit is contained in:
199
components/UpdateManager.tsx
Normal file
199
components/UpdateManager.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user