added disk restore only and changed UI

This commit is contained in:
Philipp
2026-05-21 11:10:12 +02:00
parent 05f4fda8af
commit bef27416bf
4 changed files with 15 additions and 13 deletions
+5 -5
View File
@@ -2,14 +2,14 @@ export function JobStatusPanel({ job }) {
const percent = Math.round(job?.progress?.percent || 0); const percent = Math.round(job?.progress?.percent || 0);
return ( return (
<section className="rounded-md border border-zinc-800 bg-zinc-900/70"> <section className="min-w-0 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"> <div className="flex items-center justify-between border-b border-zinc-800 px-4 py-3">
<h2 className="text-sm font-semibold">Job Status</h2> <h2 className="text-sm font-semibold">Job Status</h2>
<span className={`rounded-md border px-2 py-1 text-xs ${statusClass(job?.status)}`}> <span className={`rounded-md border px-2 py-1 text-xs ${statusClass(job?.status)}`}>
{job?.status || 'idle'} {job?.status || 'idle'}
</span> </span>
</div> </div>
<div className="grid gap-4 p-4 lg:grid-cols-[280px_1fr]"> <div className="grid min-w-0 gap-4 p-4 lg:grid-cols-[280px_minmax(0,1fr)]">
<dl className="space-y-3 text-sm"> <dl className="space-y-3 text-sm">
<Info label="Type" value={job?.type || '-'} /> <Info label="Type" value={job?.type || '-'} />
<Info label="Current Step" value={job?.currentStep || 'No active job'} /> <Info label="Current Step" value={job?.currentStep || 'No active job'} />
@@ -18,17 +18,17 @@ export function JobStatusPanel({ job }) {
<Info label="Finished" value={formatTime(job?.finishedAt)} /> <Info label="Finished" value={formatTime(job?.finishedAt)} />
{job?.error ? <Info label="Error" value={job.error} tone="text-red-300" /> : null} {job?.error ? <Info label="Error" value={job.error} tone="text-red-300" /> : null}
</dl> </dl>
<div className="space-y-3"> <div className="min-w-0 space-y-3">
<div className="rounded-md border border-zinc-800 bg-zinc-950 p-3"> <div className="rounded-md border border-zinc-800 bg-zinc-950 p-3">
<div className="mb-2 flex items-center justify-between gap-3 text-xs text-zinc-400"> <div className="mb-2 flex items-center justify-between gap-3 text-xs text-zinc-400">
<span>{job?.progress?.detail || job?.currentStep || 'No active job'}</span> <span className="min-w-0 truncate">{job?.progress?.detail || job?.currentStep || 'No active job'}</span>
<span className="font-mono text-zinc-300">{job ? `${percent}%` : '-'}</span> <span className="font-mono text-zinc-300">{job ? `${percent}%` : '-'}</span>
</div> </div>
<div className="h-2 overflow-hidden rounded-md bg-zinc-800"> <div className="h-2 overflow-hidden rounded-md bg-zinc-800">
<div className="h-full bg-cyan-400 transition-all" style={{ width: `${job ? percent : 0}%` }} /> <div className="h-full bg-cyan-400 transition-all" style={{ width: `${job ? percent : 0}%` }} />
</div> </div>
</div> </div>
<pre className="max-h-80 overflow-auto rounded-md border border-zinc-800 bg-zinc-950 p-3 text-xs leading-5 text-zinc-300"> <pre className="max-h-80 max-w-full overflow-auto rounded-md border border-zinc-800 bg-zinc-950 p-3 text-xs leading-5 text-zinc-300">
{(job?.logs || ['No logs yet.']).join('\n')} {(job?.logs || ['No logs yet.']).join('\n')}
</pre> </pre>
</div> </div>
+4 -4
View File
@@ -10,7 +10,7 @@ export function RestoreModal({ snapshot, vmName, confirmText, onCancel, onConfir
<div className="flex items-center justify-between border-b border-red-950 px-4 py-3"> <div className="flex items-center justify-between border-b border-red-950 px-4 py-3">
<div className="flex items-center gap-2 text-red-300"> <div className="flex items-center gap-2 text-red-300">
<AlertTriangle className="h-5 w-5" /> <AlertTriangle className="h-5 w-5" />
<h2 className="text-sm font-semibold">Destructive Restore</h2> <h2 className="text-sm font-semibold">Destructive Disk Restore</h2>
</div> </div>
<button className="rounded-md p-1 text-zinc-400 hover:text-zinc-100" onClick={onCancel} type="button"> <button className="rounded-md p-1 text-zinc-400 hover:text-zinc-100" onClick={onCancel} type="button">
<X className="h-5 w-5" /> <X className="h-5 w-5" />
@@ -18,8 +18,8 @@ export function RestoreModal({ snapshot, vmName, confirmText, onCancel, onConfir
</div> </div>
<div className="space-y-4 p-4"> <div className="space-y-4 p-4">
<p className="text-sm leading-6 text-zinc-300"> <p className="text-sm leading-6 text-zinc-300">
Warning: The VM will be stopped and the current disk will be irreversibly overwritten with Warning: The VM will be stopped and its current disk will be irreversibly overwritten with
snapshot <span className="font-mono text-red-200">{snapshot.id.slice(0, 8)}</span>. snapshot <span className="font-mono text-red-200">{snapshot.id.slice(0, 8)}</span>. Incus config, profiles, devices, and metadata are not restored by this action.
</p> </p>
<label className="block text-sm text-zinc-300"> <label className="block text-sm text-zinc-300">
Type <span className="font-mono text-zinc-100">{vmName}</span> to confirm. Type <span className="font-mono text-zinc-100">{vmName}</span> to confirm.
@@ -39,7 +39,7 @@ export function RestoreModal({ snapshot, vmName, confirmText, onCancel, onConfir
onClick={onConfirm} onClick={onConfirm}
type="button" type="button"
> >
Confirm Restore Confirm Disk Restore
</button> </button>
</div> </div>
</div> </div>
+4 -3
View File
@@ -1,6 +1,7 @@
import { Eye, RotateCcw } from 'lucide-react'; import { Eye, RotateCcw } from 'lucide-react';
export function SnapshotTable({ onInspect, onRestore, snapshots }) { export function SnapshotTable({ onInspect, onRestore, snapshots }) {
const dataSnapshots = snapshots.filter((snapshot) => !(snapshot.tags || []).includes('metadata'));
return ( return (
<section className="overflow-hidden rounded-md border border-zinc-800 bg-zinc-900/70"> <section className="overflow-hidden rounded-md border border-zinc-800 bg-zinc-900/70">
<div className="border-b border-zinc-800 px-4 py-3"> <div className="border-b border-zinc-800 px-4 py-3">
@@ -17,7 +18,7 @@ export function SnapshotTable({ onInspect, onRestore, snapshots }) {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-zinc-800"> <tbody className="divide-y divide-zinc-800">
{snapshots.map((snapshot) => ( {dataSnapshots.map((snapshot) => (
<tr className="hover:bg-zinc-900" key={snapshot.id}> <tr className="hover:bg-zinc-900" key={snapshot.id}>
<td className="px-4 py-3 font-mono text-zinc-100">{snapshot.id.slice(0, 8)}</td> <td className="px-4 py-3 font-mono text-zinc-100">{snapshot.id.slice(0, 8)}</td>
<td className="px-4 py-3 text-zinc-300">{formatTime(snapshot.time)}</td> <td className="px-4 py-3 text-zinc-300">{formatTime(snapshot.time)}</td>
@@ -38,13 +39,13 @@ export function SnapshotTable({ onInspect, onRestore, snapshots }) {
type="button" type="button"
> >
<RotateCcw className="h-4 w-4" /> <RotateCcw className="h-4 w-4" />
Restore Restore Disk
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
))} ))}
{!snapshots.length ? ( {!dataSnapshots.length ? (
<tr> <tr>
<td className="px-4 py-8 text-center text-zinc-500" colSpan="4"> <td className="px-4 py-8 text-center text-zinc-500" colSpan="4">
No snapshots found. No snapshots found.
+2 -1
View File
@@ -57,7 +57,8 @@ proxyRouter.get('/jobs', async (_req, res) => {
proxyRouter.get('/snapshots/:nodeId/:vmName', async (req, res, next) => { proxyRouter.get('/snapshots/:nodeId/:vmName', async (req, res, next) => {
try { try {
const node = requireNode(req.params.nodeId); const node = requireNode(req.params.nodeId);
res.json(await agentRequest(node, `/snapshots/${encodeURIComponent(req.params.vmName)}`)); const snapshots = await agentRequest(node, `/snapshots/${encodeURIComponent(req.params.vmName)}`);
res.json(snapshots.filter((snapshot) => !(snapshot.tags || []).includes('metadata')));
} catch (error) { } catch (error) {
next(error); next(error);
} }