From 51d7fb088ed334b6ef7913f0826c83367a81bfa1 Mon Sep 17 00:00:00 2001 From: Thibaut Lapierre Date: Wed, 2 Sep 2026 13:13:15 +0200 Subject: [PATCH] web: VM page header adopts the toolbar anatomy Icon + name + pills + context line; flat imperative verbs (Console, Migrate, Restart, Pause) with Edit Settings; power stays declarative so it gets no flat button; Delete demoted into the Actions menu. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B2CJp2mhGv61yHsQQqstGP --- web/e2e/ui/shell.spec.ts | 10 ++ web/src/lib/components/ActionMenu.svelte | 8 +- web/src/lib/components/VMDetail.svelte | 134 +++++++++++++++-------- 3 files changed, 104 insertions(+), 48 deletions(-) diff --git a/web/e2e/ui/shell.spec.ts b/web/e2e/ui/shell.spec.ts index d050d225..94ad2746 100644 --- a/web/e2e/ui/shell.spec.ts +++ b/web/e2e/ui/shell.spec.ts @@ -41,6 +41,16 @@ test('base: VM detail opens with live facts and tabs', async ({ page }) => { await expect(page.locator('main').getByText('10.128.0.10').first()).toBeVisible(); await expect(page.getByRole('link', { name: 'Snapshots', exact: true })).toBeVisible(); await expect(page.getByRole('link', { name: 'Console', exact: true })).toBeVisible(); + + // The flat toolbar: imperative verbs promoted, power deliberately absent + // (declarative here), delete demoted into the Actions menu. + await expect(page.getByRole('button', { name: 'Restart', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Migrate', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: /Power (on|off)/ })).toHaveCount(0); + await expect(page.getByRole('button', { name: 'Delete VM' })).toHaveCount(0); + await page.getByRole('button', { name: 'Actions' }).click(); + await expect(page.getByRole('button', { name: 'Delete VM' })).toBeVisible(); + await page.keyboard.press('Escape'); }); test('empty: onboarding CTA instead of a dead-end blank tree', async ({ page }) => { diff --git a/web/src/lib/components/ActionMenu.svelte b/web/src/lib/components/ActionMenu.svelte index b873a04b..0f1e307c 100644 --- a/web/src/lib/components/ActionMenu.svelte +++ b/web/src/lib/components/ActionMenu.svelte @@ -8,14 +8,20 @@ let { vm, onpick, + exclude = [], }: { vm: VM; onpick: (a: VMAction) => void; + // Action ids the host already promotes as flat buttons - kept out of the + // menu so a verb never appears twice in one header. + exclude?: VMAction['id'][]; } = $props(); + + const actions = $derived(vmActions.filter((a) => !exclude.includes(a.id)));
- {#each vmActions as a (a.id)} + {#each actions as a (a.id)} {#if a.sep}
{/if} diff --git a/web/src/lib/components/VMDetail.svelte b/web/src/lib/components/VMDetail.svelte index e4dd5a0a..b100575f 100644 --- a/web/src/lib/components/VMDetail.svelte +++ b/web/src/lib/components/VMDetail.svelte @@ -15,13 +15,23 @@