From 7b2d26456914217a1c560323d944f0d6eb27d130 Mon Sep 17 00:00:00 2001 From: Andrew Chung <47454279+andrewkcchung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:22:46 -0400 Subject: [PATCH] fix(portal): keep bulk session delete visible Render the wider multi-selection toolbar on its own header row and cover the lifecycle delete path at the reported viewport size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/app/ui/react/src/web-app.js | 2 +- packages/app/web/src/index.css | 23 +++++++++++ .../web/test/e2e/fleet-work-index.spec.mjs | 38 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/app/ui/react/src/web-app.js b/packages/app/ui/react/src/web-app.js index 9bf3c3dd..2ee42ea1 100644 --- a/packages/app/ui/react/src/web-app.js +++ b/packages/app/ui/react/src/web-app.js @@ -6105,7 +6105,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur const panelActions = React.createElement(React.Fragment, null, !actionsOnly && isBulkSelection ? React.createElement("span", { - className: "ps-mini-button-label", + className: "ps-mini-button-label ps-session-selection-label", style: { padding: "0 6px", fontSize: "12px", opacity: 0.85 }, title: "Multiple sessions selected. Move to group or cancel selected sessions; click Clear to exit.", }, `${selectedCount} selected`) diff --git a/packages/app/web/src/index.css b/packages/app/web/src/index.css index 0ce77175..5c87f0da 100644 --- a/packages/app/web/src/index.css +++ b/packages/app/web/src/index.css @@ -2359,6 +2359,29 @@ body.is-resizing-pane-y iframe { } } +/* Bulk selection adds a count and Clear control to the session actions. Give + that wider toolbar its own row instead of rendering it below the clipped + fixed header. The ordinary single-selection header remains one row. */ +.ps-panel-header:has(.ps-session-selection-label) { + height: auto; + min-height: var(--ps-panel-header-h); + max-height: none; + flex: 0 0 auto; + flex-wrap: wrap; + overflow: visible; + padding-block: 4px; +} + +.ps-panel-header:has(.ps-session-selection-label) > .ps-panel-title { + flex: 1 0 100%; + overflow: hidden; +} + +.ps-panel-header:has(.ps-session-selection-label) > .ps-panel-header-right { + width: 100%; + justify-content: flex-end; +} + .ps-work-index-tab { border: 0; border-bottom: 2px solid transparent; diff --git a/packages/app/web/test/e2e/fleet-work-index.spec.mjs b/packages/app/web/test/e2e/fleet-work-index.spec.mjs index 3f285649..9bcaaf06 100644 --- a/packages/app/web/test/e2e/fleet-work-index.spec.mjs +++ b/packages/app/web/test/e2e/fleet-work-index.spec.mjs @@ -51,6 +51,44 @@ test("admin personal view keeps Session actions inside a narrow work-index heade } }); +test("bulk selection keeps the terminate and delete action visible", async ({ page }) => { + await page.setViewportSize({ width: 696, height: 452 }); + await page.goto(`http://127.0.0.1:${stub.port}`); + const rows = page.locator(".ps-session-list-button"); + await rows.first().waitFor(); + + await rows.nth(0).click({ modifiers: ["Control"] }); + await rows.nth(1).click({ modifiers: ["Control"] }); + + const header = page.locator("#ps-work-index-panel .ps-panel-header").first(); + const terminate = header.getByRole("button", { + name: /Terminate 2 selected sessions/, + }); + await expect(terminate).toBeVisible(); + + const geometry = await header.evaluate((element) => { + const bounds = element.getBoundingClientRect(); + const button = element.querySelector( + '[aria-label^="Terminate 2 selected sessions"]', + )?.getBoundingClientRect(); + return { + header: { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom }, + button: button + ? { left: button.left, top: button.top, right: button.right, bottom: button.bottom } + : null, + }; + }); + + expect(geometry.button).not.toBeNull(); + expect(geometry.button.left).toBeGreaterThanOrEqual(geometry.header.left); + expect(geometry.button.top).toBeGreaterThanOrEqual(geometry.header.top); + expect(geometry.button.right).toBeLessThanOrEqual(geometry.header.right); + expect(geometry.button.bottom).toBeLessThanOrEqual(geometry.header.bottom); + + await terminate.click(); + await expect(page.getByRole("button", { name: "Hard Delete 2 Sessions" })).toBeVisible(); +}); + test("admin Fleet view keeps Session transcripts stable and read-only", async ({ page }) => { const requests = []; page.on("request", (request) => {