86 lines
4.0 KiB
React
86 lines
4.0 KiB
React
import { ChevronRight, Circle } from 'lucide-react';
|
|
import { StatusCard } from './StatusCard.jsx';
|
|
|
|
export function Dashboard({ health, jobs, loading, onManage, vms }) {
|
|
const activeJobs = jobs.filter((job) => ['queued', 'running'].includes(job.status));
|
|
const runningVms = vms.filter((vm) => vm.status === 'Running');
|
|
const lastSuccess = jobs.find((job) => job.status === 'success');
|
|
|
|
return (
|
|
<section className="space-y-5">
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
|
<StatusCard label="Total VMs" value={loading ? '...' : vms.length} />
|
|
<StatusCard label="Running VMs" value={runningVms.length} tone="good" />
|
|
<StatusCard label="Active Jobs" value={activeJobs.length} tone={activeJobs.length ? 'active' : 'neutral'} />
|
|
<StatusCard label="Last Success" value={lastSuccess ? formatTime(lastSuccess.finishedAt) : 'None'} tone="good" />
|
|
<StatusCard label="Repository" value={health?.ok ? 'Ready' : 'Check'} tone={health?.ok ? 'good' : 'warn'} />
|
|
</div>
|
|
|
|
<div className="overflow-hidden rounded-md border border-zinc-800 bg-zinc-900/70">
|
|
<div className="flex items-center justify-between border-b border-zinc-800 px-4 py-3">
|
|
<h1 className="text-sm font-semibold text-zinc-100">VM Backup Status</h1>
|
|
<span className="text-xs text-zinc-500">{healthSummary(health)}</span>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[760px] text-left text-sm">
|
|
<thead className="border-b border-zinc-800 text-xs uppercase tracking-wide text-zinc-500">
|
|
<tr>
|
|
<th className="px-4 py-3 font-medium">VM</th>
|
|
<th className="px-4 py-3 font-medium">Incus</th>
|
|
<th className="px-4 py-3 font-medium">Last Job</th>
|
|
<th className="px-4 py-3 font-medium">Active Job</th>
|
|
<th className="px-4 py-3 text-right font-medium">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-zinc-800">
|
|
{vms.map((vm) => (
|
|
<tr className="hover:bg-zinc-900" key={vm.name}>
|
|
<td className="px-4 py-3 font-medium text-zinc-100">{vm.name}</td>
|
|
<td className="px-4 py-3">
|
|
<span className="inline-flex items-center gap-2 text-zinc-300">
|
|
<Circle className={`h-2.5 w-2.5 fill-current ${vm.status === 'Running' ? 'text-emerald-400' : 'text-red-400'}`} />
|
|
{vm.status || 'Unknown'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-zinc-300">{vm.lastJobStatus || 'None'}</td>
|
|
<td className="px-4 py-3 text-zinc-400">{vm.activeJob?.currentStep || 'Idle'}</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<button
|
|
className="inline-flex h-8 items-center gap-1 rounded-md border border-zinc-700 px-2.5 text-xs text-zinc-100 hover:border-cyan-500 hover:text-cyan-200"
|
|
onClick={() => onManage(vm.name)}
|
|
type="button"
|
|
>
|
|
Manage <ChevronRight className="h-4 w-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{!vms.length ? (
|
|
<tr>
|
|
<td className="px-4 py-8 text-center text-zinc-500" colSpan="5">
|
|
No VMs loaded.
|
|
</td>
|
|
</tr>
|
|
) : null}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function formatTime(value) {
|
|
if (!value) return 'None';
|
|
return new Intl.DateTimeFormat(undefined, { dateStyle: 'short', timeStyle: 'short' }).format(new Date(value));
|
|
}
|
|
|
|
function healthSummary(health) {
|
|
if (!health) return 'Health loading';
|
|
if (health.ok) return 'All checks ok';
|
|
return Object.entries(health.checks || {})
|
|
.filter(([, value]) => value !== 'ok')
|
|
.map(([key]) => key)
|
|
.join(', ') || 'Health degraded';
|
|
}
|