From 651a5c7902887475d353cf689ae47ce29d3c58d5 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 11:05:38 +0200 Subject: [PATCH] fix(frontend): isolate administration rail from portal CSS --- .../e2e/administration-embed-layout.spec.ts | 107 ++++++++++++++++++ .../e2e/database-management-layout.spec.ts | 5 +- frontend/src/index.css | 15 +++ frontend/src/shell/AppShell.tsx | 23 ++-- .../shell/WorkspacePreprocessingControl.tsx | 20 ++-- 5 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 frontend/e2e/administration-embed-layout.spec.ts diff --git a/frontend/e2e/administration-embed-layout.spec.ts b/frontend/e2e/administration-embed-layout.spec.ts new file mode 100644 index 00000000..a99797ac --- /dev/null +++ b/frontend/e2e/administration-embed-layout.spec.ts @@ -0,0 +1,107 @@ +import { expect, test, type Page, type Route } from "@playwright/test"; +import { createAuthenticationStack } from "./fixtures/auth-stack.mjs"; + +test.describe.configure({ mode: "serial" }); +test.use({ viewport: { width: 1660, height: 822 } }); + +let stack: Awaited>; + +function json(route: Route, body: unknown) { + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(body), + }); +} + +async function signInAsAdmin(page: Page) { + const account = stack.localAccount("admin"); + await page.getByLabel("Username").fill(account.username); + await page.locator('input[name="password"]').fill(account.password); + await page.getByRole("button", { name: "Sign in", exact: true }).click(); + await expect(page.getByTestId("app-shell")).toBeVisible({ timeout: 30_000 }); +} + +test.beforeAll(async () => { + stack = await createAuthenticationStack({ withF1Workspace: true }); + await stack.useLocalMode(); +}); + +test.afterAll(async () => { + await stack?.close(); +}); + +test("Administration stays within its rail when hosted by the Omics portal", async ({ page }) => { + await page.route("**/api/settings", (route) => json(route, { + workspace: "fixture-workspace", + provider: "zai", + model: "glm-5.2", + thinking: "medium", + })); + await page.route("**/api/workspaces", (route) => json(route, [{ + id: "fixture-workspace", + name: "fixture-workspace", + file: "fixture-workspace/workspace.yaml", + displayName: "Fixture workspace", + configurationState: "ready", + revision: { + id: "fixture-workspace", + commit: "a".repeat(40), + blob: "b".repeat(40), + snapshotPath: `/snapshots/${"a".repeat(40)}/fixture-workspace.yaml`, + }, + }])); + await page.route("**/api/workspaces/fixture-workspace/preprocessing", (route) => json(route, { + schemaVersion: 1, + workspaceId: "fixture-workspace", + state: "running", + actionable: false, + clearable: false, + detail: "Building schema and LSH.", + progress: { + stage: "schema_index", + step: 2, + totalSteps: 4, + }, + })); + + await page.goto(stack.publicUrl); + await signInAsAdmin(page); + await expect(page.getByRole("combobox", { name: "Workspace" })).toHaveValue("fixture-workspace"); + + // Hyper/Bootstrap and Thoth currently share the portal document. These + // utility names are therefore resolved with the portal's !important values. + await page.addStyleTag({ content: ` + .px-2 { padding-right: 0.75rem !important; padding-left: 0.75rem !important; } + .px-3 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } + .px-4 { padding-right: 2.25rem !important; padding-left: 2.25rem !important; } + ` }); + + const rail = page.getByRole("complementary", { name: "Session navigation" }); + await rail.getByRole("button", { name: "Administration", exact: true }).click(); + const administration = rail.getByRole("region", { name: "Administration" }); + const boundary = rail.getByTestId("administration-boundary"); + await expect(administration).toBeVisible(); + await expect(rail.getByText("2 / 4", { exact: true })).toBeVisible(); + + const containment = await administration.evaluate((panel) => { + const elements = Array.from(panel.querySelectorAll('button, [role="progressbar"], span')) + .filter((element) => element.matches('button, [role="progressbar"]') || element.textContent?.trim() === "2 / 4"); + return elements.map((element) => { + const box = element.getBoundingClientRect(); + return { + label: element.textContent?.trim() || element.getAttribute("aria-label") || element.tagName, + left: box.left, + right: box.right, + }; + }); + }); + const boundaryBox = await boundary.boundingBox(); + expect(boundaryBox).not.toBeNull(); + if (!boundaryBox) return; + + for (const item of containment) { + expect.soft(item.left, `${item.label} crosses the Administration left edge`).toBeGreaterThanOrEqual(boundaryBox.x - 1); + expect.soft(item.right, `${item.label} crosses the Administration right edge`).toBeLessThanOrEqual(boundaryBox.x + boundaryBox.width + 1); + } +}); diff --git a/frontend/e2e/database-management-layout.spec.ts b/frontend/e2e/database-management-layout.spec.ts index 8e2b33e6..d424779e 100644 --- a/frontend/e2e/database-management-layout.spec.ts +++ b/frontend/e2e/database-management-layout.spec.ts @@ -426,12 +426,15 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb const administrationPanel = adminNavigationRail.getByRole("region", { name: "Administration" }); await expect(administrationPanel).toBeVisible(); expect(await administrationPanel.locator(":scope > *").evaluateAll((elements) => elements.map((element) => ( - element.getAttribute("role") === "separator" ? "separator" : element.textContent?.trim() + element.getAttribute("role") === "separator" + ? "separator" + : element.getAttribute("aria-label") ?? element.textContent?.trim() )))).toEqual([ "Database management", "separator", "Workspace management", "Pi management", + "Workspace preprocessing", ]); await administration.click(); diff --git a/frontend/src/index.css b/frontend/src/index.css index d85ee0b2..c287d1ff 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -181,6 +181,21 @@ * Scoped so it never leaks into widgets. */ @layer components { + /* Omics Portal and Thoth share one document in the embedded deployment. + Keep rail geometry on product-specific class names so Hyper/Bootstrap's + global spacing utilities (for example .px-4 !important) cannot resize it. */ + .thot-session-navigation__header { + padding: 1.25rem 1rem 0.75rem; + } + + .thot-session-navigation__primary-controls { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.5rem; + padding: 0 1rem 0.75rem; + } + /* Standard micro-label: panel headers, nav sections, meta rows. Set in the mono register so labels/meta read as a distinct typographic layer from the sans body prose (mono needs less tracking than the sans did). */ diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 1418703b..043a10ad 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -855,7 +855,7 @@ export function AppShell({ canLogout }: AppShellProps) { {/* Right session rail */} {(activeSurface === "database-management" || !showActivity) && (