forked from wrenn/wrenn
Added snapshot name dialogue on the UI
This commit is contained in:
@ -53,6 +53,12 @@
|
|||||||
let creating = $state(false);
|
let creating = $state(false);
|
||||||
let createError = $state<string | null>(null);
|
let createError = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Snapshot dialog state
|
||||||
|
let snapshotTarget = $state<{ capsule: Capsule; pauseFirst: boolean } | null>(null);
|
||||||
|
let snapshotName = $state('');
|
||||||
|
let snapshotting = $state(false);
|
||||||
|
let snapshotError = $state<string | null>(null);
|
||||||
|
|
||||||
// Destroy confirmation state
|
// Destroy confirmation state
|
||||||
let destroyTarget = $state<Capsule | null>(null);
|
let destroyTarget = $state<Capsule | null>(null);
|
||||||
let destroying = $state(false);
|
let destroying = $state(false);
|
||||||
@ -172,30 +178,32 @@
|
|||||||
actionLoading = null;
|
actionLoading = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSnapshot(id: string) {
|
function handleSnapshot(capsule: Capsule) {
|
||||||
openMenuId = null;
|
openMenuId = null;
|
||||||
actionLoading = id;
|
snapshotName = '';
|
||||||
const result = await createSnapshot(id);
|
snapshotError = null;
|
||||||
if (result.ok) {
|
snapshotTarget = { capsule, pauseFirst: false };
|
||||||
// Snapshot may have paused the capsule — refresh to get updated status
|
|
||||||
await fetchCapsules();
|
|
||||||
} else {
|
|
||||||
toast.error(result.error);
|
|
||||||
}
|
|
||||||
actionLoading = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePauseAndSnapshot(id: string) {
|
function handlePauseAndSnapshot(capsule: Capsule) {
|
||||||
openMenuId = null;
|
openMenuId = null;
|
||||||
actionLoading = id;
|
snapshotName = '';
|
||||||
// Snapshot endpoint pauses automatically if running
|
snapshotError = null;
|
||||||
const result = await createSnapshot(id);
|
snapshotTarget = { capsule, pauseFirst: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSnapshotConfirm() {
|
||||||
|
if (!snapshotTarget) return;
|
||||||
|
snapshotting = true;
|
||||||
|
snapshotError = null;
|
||||||
|
const result = await createSnapshot(snapshotTarget.capsule.id, snapshotName.trim() || undefined);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
snapshotTarget = null;
|
||||||
await fetchCapsules();
|
await fetchCapsules();
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.error);
|
snapshotError = result.error;
|
||||||
}
|
}
|
||||||
actionLoading = null;
|
snapshotting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDestroy() {
|
async function handleDestroy() {
|
||||||
@ -640,7 +648,7 @@
|
|||||||
Pause
|
Pause
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handlePauseAndSnapshot(openCapsule.id)}
|
onclick={() => handlePauseAndSnapshot(openCapsule)}
|
||||||
class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
|
class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
|
||||||
>
|
>
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
|
||||||
@ -660,7 +668,7 @@
|
|||||||
Resume
|
Resume
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleSnapshot(openCapsule.id)}
|
onclick={() => handleSnapshot(openCapsule)}
|
||||||
class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
|
class="flex w-full items-center gap-2.5 px-3 py-2 text-meta text-[var(--color-text-secondary)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] hover:text-[var(--color-text-primary)]"
|
||||||
>
|
>
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
|
||||||
@ -685,6 +693,96 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Snapshot Dialog -->
|
||||||
|
{#if snapshotTarget}
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/60"
|
||||||
|
onclick={() => { if (!snapshotting) snapshotTarget = null; }}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Escape' && !snapshotting) snapshotTarget = null; }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] overflow-hidden" style="animation: fadeUp 0.2s ease both">
|
||||||
|
<!-- Header band -->
|
||||||
|
<div class="flex items-center gap-4 border-b border-[var(--color-border)] bg-[var(--color-bg-3)] px-6 py-5">
|
||||||
|
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-[var(--radius-input)] bg-[var(--color-accent)]/15 text-[var(--color-accent)] shadow-[0_0_12px_var(--color-accent-glow)]">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" />
|
||||||
|
<circle cx="12" cy="15" r="3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Capture snapshot</h2>
|
||||||
|
<p class="mt-0.5 text-meta text-[var(--color-text-muted)] font-mono">{snapshotTarget.capsule.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-6 pt-5 pb-6 space-y-4">
|
||||||
|
{#if snapshotTarget.pauseFirst}
|
||||||
|
<div class="flex items-start gap-2.5 rounded-[var(--radius-input)] border border-[var(--color-amber)]/25 bg-[var(--color-amber)]/8 px-3 py-2.5">
|
||||||
|
<svg class="mt-px shrink-0 text-[var(--color-amber)]" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-meta text-[var(--color-amber)] leading-relaxed">This capsule will be <strong class="font-semibold">paused first</strong> — memory state is captured at rest.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-ui text-[var(--color-text-tertiary)]">The capsule's current memory state will be captured and stored as a reusable snapshot.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if snapshotError}
|
||||||
|
<div class="rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
|
||||||
|
{snapshotError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 flex items-baseline justify-between">
|
||||||
|
<label class="text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-tertiary)]" for="snapshot-name">Snapshot name</label>
|
||||||
|
<span class="text-meta text-[var(--color-text-muted)]">optional</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="snapshot-name"
|
||||||
|
type="text"
|
||||||
|
bind:value={snapshotName}
|
||||||
|
disabled={snapshotting}
|
||||||
|
class="w-full rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-4)] px-3 py-2 font-mono text-ui text-[var(--color-text-bright)] outline-none placeholder:text-[var(--color-text-muted)] transition-colors duration-150 focus:border-[var(--color-accent)] disabled:opacity-50"
|
||||||
|
placeholder="e.g. after-apt-install, pre-migration"
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter' && !snapshotting) handleSnapshotConfirm(); }}
|
||||||
|
/>
|
||||||
|
<p class="mt-1.5 text-meta text-[var(--color-text-muted)]">Leave blank to use an auto-generated name.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-1">
|
||||||
|
<button
|
||||||
|
onclick={() => { snapshotTarget = null; }}
|
||||||
|
disabled={snapshotting}
|
||||||
|
class="rounded-[var(--radius-button)] border border-[var(--color-border)] px-4 py-2 text-ui text-[var(--color-text-secondary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleSnapshotConfirm}
|
||||||
|
disabled={snapshotting}
|
||||||
|
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0"
|
||||||
|
>
|
||||||
|
{#if snapshotting}
|
||||||
|
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||||
|
</svg>
|
||||||
|
Capturing...
|
||||||
|
{:else}
|
||||||
|
Capture snapshot
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Create Capsule Dialog -->
|
<!-- Create Capsule Dialog -->
|
||||||
{#if showCreateDialog}
|
{#if showCreateDialog}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user