103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import VMCard from '@/components/VMCard';
|
|
import UserMenu from '@/components/UserMenu';
|
|
import Skeleton from '@/components/ui/Skeleton';
|
|
|
|
interface VM {
|
|
vmid: string;
|
|
name: string;
|
|
status: string;
|
|
mem: string;
|
|
bootdisk: string;
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const [vms, setVms] = useState<VM[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchVMs = async () => {
|
|
try {
|
|
const res = await fetch('/api/vms');
|
|
if (!res.ok) throw new Error('Failed to fetch VMs');
|
|
const data = await res.json();
|
|
setVms(data.vms);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchVMs();
|
|
const interval = setInterval(fetchVMs, 5000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const runningCount = vms.filter(vm => vm.status === 'running').length;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background text-foreground">
|
|
{/* Header */}
|
|
<header className="sticky top-0 z-40 border-b border-border bg-background/80 backdrop-blur-xl">
|
|
<div className="container mx-auto px-4 sm:px-6 py-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div className="flex items-center justify-between sm:justify-start gap-4">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">
|
|
NEXUS
|
|
</h1>
|
|
<p className="text-sm text-foreground-muted hidden sm:block">VM Control Center</p>
|
|
</div>
|
|
|
|
{!loading && (
|
|
<div className="flex items-center gap-3 text-sm text-foreground-muted">
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="text-foreground font-semibold">{vms.length}</span>
|
|
VMs
|
|
</span>
|
|
<span className="text-foreground-subtle">·</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="w-2 h-2 rounded-full bg-success animate-pulse" />
|
|
<span className="text-foreground font-semibold">{runningCount}</span>
|
|
Running
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<UserMenu />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="container mx-auto px-4 sm:px-6 py-8">
|
|
{error && (
|
|
<div className="bg-danger/10 border border-danger/30 rounded-xl p-4 mb-6">
|
|
<p className="text-danger">Error: {error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-64 w-full" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in">
|
|
{vms.map((vm) => (
|
|
<VMCard key={vm.vmid} vm={vm} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|