From 06d92460ed7bf5581e6754b74f265dc8a553133a Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Mon, 17 Aug 2026 22:37:48 +0200 Subject: [PATCH] Dashboard session rows: native open-in-new-tab via invisible cell link overlays Rows were plain so the browser offered no context-menu entry. Each non-interactive cell now contains an invisible absolutely-positioned Link (prefetch off) giving a real href for right-click/middle/ctrl+click; visuals and SPA click behavior are unchanged. Cells with own buttons or title tooltips (status, tenantId, actions) keep the row-click fallback. Co-Authored-By: Claude Fable 5 --- .../app/dashboard/components/SessionTable.tsx | 59 +++++++++++++++---- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx b/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx index 93d22bda..3a75d9bb 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/components/SessionTable.tsx @@ -2,6 +2,7 @@ import { sessionUrl } from "@/lib/routes"; import { useRouter } from "next/navigation"; +import Link from "next/link"; import type { Route } from "next"; import { useState, useEffect, useRef, useMemo, useDeferredValue } from "react"; import { Session } from "../types"; @@ -709,6 +710,7 @@ export function SessionTable({ key={col.key} columnKey={col.key} session={session} + href={linkFor(session)} adminMode={adminMode} globalAdminMode={globalAdminMode} blockedDevicesSet={blockedDevicesSet} @@ -782,9 +784,35 @@ export function SessionTable({ ); } +/** Invisible cell-filling anchor so the browser's native context menu (open in new + * tab, copy link) and middle/ctrl+click work on session rows. Visually nothing — the + * row keeps its exact current look; navigation UX is unchanged because plain clicks + * still do an SPA push via Link. Cells with their own interactive content or title + * tooltips (status badges, tenant copy button, actions) must NOT render this overlay + * as it would sit above them and swallow their hover/click. aria-hidden + tabIndex -1 + * keep it out of tab order and screen readers — the row's click target is unchanged. */ +function RowLinkOverlay({ href, session }: { href: Route; session: Session }) { + return ( + { + // Link handles the navigation itself; stop the row's onClick from double-pushing. + e.stopPropagation(); + trackEvent("session_opened", { sessionId: session.sessionId, status: session.status ?? "" }); + }} + /> + ); +} + function SessionCell({ columnKey, session, + href, globalAdminMode, blockedDevicesSet, user, @@ -795,6 +823,7 @@ function SessionCell({ }: { columnKey: string; session: Session; + href: Route; adminMode: boolean; globalAdminMode: boolean; blockedDevicesSet: Set; @@ -808,7 +837,8 @@ function SessionCell({ switch (columnKey) { case "device": return ( - + +
{session.deviceName || session.serialNumber}
@@ -849,7 +879,8 @@ function SessionCell({ case "model": return ( - + +
{session.manufacturer || "Unknown manufacturer"}
@@ -907,14 +938,16 @@ function SessionCell({ case "eventCount": return ( - + + {session.eventCount} ); case "duration": return ( - + + {Math.round(session.durationSeconds / 60)} min ); @@ -923,7 +956,8 @@ function SessionCell({ // Two-line date/time keeps the column narrow so more columns fit on one page. const started = new Date(session.startedAt); return ( - + +
{started.toLocaleDateString()}
{started.toLocaleTimeString()}
@@ -985,35 +1019,40 @@ function SessionCell({ case "osName": return ( - + + {session.osName || —} ); case "osBuild": return ( - + + {session.osBuild || —} ); case "osDisplayVersion": return ( - + + {session.osDisplayVersion || —} ); case "osEdition": return ( - + + {session.osEdition || —} ); case "osLanguage": return ( - + + {session.osLanguage || —} );