fix(delegated): pass ?tenantId= on dashboard session links so delegated detail drill-in resolves

Phase 7 deleted the /fleet/[tenantId] session browser (which appended
?tenantId= via sessionLinkTarget) and re-routed delegated viewing through
the dashboard SessionTable, but the dashboard never passed sessionLinkTarget.
Row-click/search-nav went to a bare /sessions/{id}; the detail page resolves
the target tenant for a delegated admin SOLELY from ?tenantId= (no global
scope -> bare id falls back to the JWT home tenant -> wrong partition ->
empty events + "No issues detected").

- SessionTable.sessionLinkTarget signature (sessionId) -> (session): the
  target tenant is per-row (session.tenantId), not derivable from the id.
- dashboard passes sessionLinkTarget when crossTenant ->
  /sessions/{id}?tenantId=<session.tenantId>. Harmless for real GA.

tsc clean; web 485 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-29 10:43:09 +02:00
parent 4c6e63cefd
commit 7742113b29
2 changed files with 17 additions and 8 deletions
@@ -96,9 +96,10 @@ interface SessionTableProps {
onBlockDevice: (serialNumber: string, tenantId: string, deviceName?: string) => void;
fullWidth: boolean;
onToggleFullWidth: () => void;
/** Builds the row's navigation target. Defaults to `/sessions/{id}`; the Fleet drill-in overrides it to
* append `?tenantId=` so a delegated viewer opens the session in the managed tenant's (read-only) context. */
sessionLinkTarget?: (sessionId: string) => string;
/** Builds the row's navigation target. Defaults to `/sessions/{id}`; a cross-tenant viewer overrides it to
* append `?tenantId=` so a delegated viewer opens the session in the managed tenant's (read-only) context.
* Receives the whole session because the target tenant is per-row (session.tenantId), not derivable from id. */
sessionLinkTarget?: (session: Session) => string;
}
export function SessionTable({
@@ -142,8 +143,8 @@ export function SessionTable({
sessionLinkTarget,
}: SessionTableProps) {
const router = useRouter();
const linkFor = (sessionId: string) =>
sessionLinkTarget ? sessionLinkTarget(sessionId) : `/sessions/${sessionId}`;
const linkFor = (session: Session) =>
sessionLinkTarget ? sessionLinkTarget(session) : `/sessions/${session.sessionId}`;
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(getInitialVisibleColumns);
const [showColumnSelector, setShowColumnSelector] = useState(false);
const [openFilterColumn, setOpenFilterColumn] = useState<string | null>(null);
@@ -551,7 +552,7 @@ export function SessionTable({
const selected = searchSuggestions[searchSelectedIndex];
setShowSearchSuggestions(false);
setSearchSelectedIndex(-1);
router.push(linkFor(selected.session.sessionId));
router.push(linkFor(selected.session));
return;
}
if (e.key === "Escape") {
@@ -593,7 +594,7 @@ export function SessionTable({
onClick={() => {
setShowSearchSuggestions(false);
setSearchSelectedIndex(-1);
router.push(linkFor(s.session.sessionId));
router.push(linkFor(s.session));
}}
className={`w-full text-left px-4 py-2.5 flex items-center gap-3 transition-colors ${
idx === searchSelectedIndex ? "bg-blue-50" : "hover:bg-gray-50"
@@ -786,7 +787,7 @@ export function SessionTable({
paginatedSessions.map((session) => (
<tr
key={session.sessionId}
onClick={() => { trackEvent("session_opened", { sessionId: session.sessionId, status: session.status ?? "" }); router.push(linkFor(session.sessionId)); }}
onClick={() => { trackEvent("session_opened", { sessionId: session.sessionId, status: session.status ?? "" }); router.push(linkFor(session)); }}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
{activeColumns.map((col) => (
@@ -465,6 +465,14 @@ function HomeContent() {
onBlockDevice={blockDevice}
fullWidth={fullWidth}
onToggleFullWidth={toggleFullWidth}
sessionLinkTarget={
crossTenant
? (s) =>
s.tenantId
? `/sessions/${s.sessionId}?tenantId=${encodeURIComponent(s.tenantId)}`
: `/sessions/${s.sessionId}`
: undefined
}
/>
)}
</div>