nexus-dashboard/app/vm/[id]/page.tsx
2026-02-01 18:42:22 +00:00

253 lines
8.9 KiB
TypeScript

'use client';
import { use, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import StatusBadge from '@/components/StatusBadge';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Skeleton from '@/components/ui/Skeleton';
import { ArrowLeft, Play, Square, RotateCw, HardDrive, Cpu, MemoryStick } from 'lucide-react';
interface VMDetails {
vmid: string;
name: string;
status: string;
mem: string;
bootdisk: string;
config: Record<string, string>;
stats: {
cpu: number;
mem: {
used: number;
total: number;
percent: number;
};
disk: {
percent: number;
};
};
}
export default function VMDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const resolvedParams = use(params);
const router = useRouter();
const [vm, setVm] = useState<VMDetails | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [actionLoading, setActionLoading] = useState(false);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const fetchVMDetails = async () => {
try {
const res = await fetch(`/api/vms/${resolvedParams.id}`);
if (!res.ok) throw new Error('Failed to fetch VM details');
const data = await res.json();
setVm(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
setActionLoading(true);
try {
const res = await fetch(`/api/vms/${resolvedParams.id}/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
});
if (!res.ok) throw new Error('Failed to perform action');
setToast({ message: `VM ${action}ing...`, type: 'success' });
setTimeout(() => setToast(null), 3000);
setTimeout(fetchVMDetails, 2000);
} catch (err) {
setToast({ message: err instanceof Error ? err.message : 'Action failed', type: 'error' });
setTimeout(() => setToast(null), 3000);
} finally {
setActionLoading(false);
}
};
useEffect(() => {
fetchVMDetails();
const interval = setInterval(fetchVMDetails, 5000);
return () => clearInterval(interval);
}, [resolvedParams.id]);
if (loading) {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 sm:px-6 py-8">
<Skeleton className="h-10 w-32 mb-6" />
<Skeleton className="h-24 w-full mb-6" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<Skeleton className="h-32" />
<Skeleton className="h-32" />
<Skeleton className="h-32" />
</div>
<Skeleton className="h-64 w-full" />
</div>
</div>
);
}
if (error || !vm) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card hover={false} className="max-w-md">
<div className="bg-danger/10 border border-danger/30 rounded-xl p-6">
<p className="text-danger">Error: {error || 'VM not found'}</p>
</div>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-background text-foreground">
{/* Toast Notification */}
{toast && (
<div className="fixed top-4 right-4 z-50 animate-slide-up">
<div className={`${toast.type === 'success' ? 'bg-success/10 border-success/30 text-success' : 'bg-danger/10 border-danger/30 text-danger'} border rounded-xl px-4 py-3 shadow-elevated`}>
{toast.message}
</div>
</div>
)}
<div className="container mx-auto px-4 sm:px-6 py-8">
{/* Back Button */}
<Button
variant="ghost"
size="sm"
icon={ArrowLeft}
onClick={() => router.push('/')}
className="mb-6"
>
Back to Dashboard
</Button>
{/* VM Header */}
<Card hover={false} className="mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl sm:text-4xl font-bold text-foreground mb-2">{vm.name}</h1>
<p className="text-foreground-muted">VM ID: {vm.vmid}</p>
</div>
<StatusBadge status={vm.status} />
</div>
</Card>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 mb-6">
<Card hover={false}>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-foreground/5 flex items-center justify-center">
<Cpu className="w-5 h-5 text-foreground-muted" />
</div>
<h3 className="text-sm text-foreground-muted">CPU Usage</h3>
</div>
<p className="text-3xl font-bold text-foreground mb-3">{vm.stats.cpu}%</p>
<div className="h-2 bg-background rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-foreground to-foreground-muted transition-all duration-500"
style={{ width: `${vm.stats.cpu}%` }}
/>
</div>
</Card>
<Card hover={false}>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-foreground/5 flex items-center justify-center">
<MemoryStick className="w-5 h-5 text-foreground-muted" />
</div>
<h3 className="text-sm text-foreground-muted">Memory Usage</h3>
</div>
<p className="text-3xl font-bold text-foreground mb-1">{vm.stats.mem.percent}%</p>
<p className="text-xs text-foreground-subtle mb-3">
{vm.stats.mem.used} MB / {vm.stats.mem.total} MB
</p>
<div className="h-2 bg-background rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-foreground to-foreground-muted transition-all duration-500"
style={{ width: `${vm.stats.mem.percent}%` }}
/>
</div>
</Card>
<Card hover={false}>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-foreground/5 flex items-center justify-center">
<HardDrive className="w-5 h-5 text-foreground-muted" />
</div>
<h3 className="text-sm text-foreground-muted">Disk Usage</h3>
</div>
<p className="text-3xl font-bold text-foreground mb-3">{vm.stats.disk.percent}%</p>
<div className="h-2 bg-background rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-foreground to-foreground-muted transition-all duration-500"
style={{ width: `${vm.stats.disk.percent}%` }}
/>
</div>
</Card>
</div>
{/* Control Panel */}
<Card hover={false} className="mb-6">
<h2 className="text-xl font-bold text-foreground mb-4">Control Panel</h2>
<div className="flex flex-col sm:flex-row gap-3">
<Button
onClick={() => handleAction('start')}
disabled={actionLoading || vm.status === 'running'}
variant="secondary"
icon={Play}
className="flex-1 border-success/30 hover:bg-success/10 text-success"
>
Start
</Button>
<Button
onClick={() => handleAction('stop')}
disabled={actionLoading || vm.status !== 'running'}
variant="danger"
icon={Square}
className="flex-1"
>
Stop
</Button>
<Button
onClick={() => handleAction('restart')}
disabled={actionLoading || vm.status !== 'running'}
variant="secondary"
icon={RotateCw}
className="flex-1"
>
Restart
</Button>
</div>
</Card>
{/* Configuration */}
<Card hover={false}>
<h2 className="text-xl font-bold text-foreground mb-4">Configuration</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.entries(vm.config).map(([key, value]) => (
<div key={key} className="pb-3 border-b border-border">
<p className="text-sm text-foreground-muted mb-1">{key}</p>
<p className="text-foreground font-mono text-sm">{value}</p>
</div>
))}
</div>
</Card>
</div>
</div>
);
}