From 92ce72da6ea4a4cbc6a558bd7305e3eaaf79a2e8 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 10 May 2026 14:32:19 -0400 Subject: [PATCH] refactor(web): simplify profile sessions layout Split the unified list into separate Browser and CLI panels and drop provider, login, kind/current badges, and user agent. Each panel shows just the timestamps that matter, with revoke gated to CLI sessions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/routes/profile-sessions.test.tsx | 34 +++--- .../fabro-web/app/routes/profile-sessions.tsx | 114 ++++++++---------- 2 files changed, 71 insertions(+), 77 deletions(-) diff --git a/apps/fabro-web/app/routes/profile-sessions.test.tsx b/apps/fabro-web/app/routes/profile-sessions.test.tsx index 06b49df06..a4ae8b738 100644 --- a/apps/fabro-web/app/routes/profile-sessions.test.tsx +++ b/apps/fabro-web/app/routes/profile-sessions.test.tsx @@ -120,34 +120,40 @@ afterEach(() => { }); describe("ProfileSessions", () => { - test("renders a profile-style skeleton while loading", () => { + test("renders profile-style skeletons while loading", () => { currentResponse = undefined; const renderer = renderAndTrack(); const text = textFromNode(renderer.toJSON()); - // Skeleton has no real content, just placeholder bars; verify session - // labels haven't rendered yet. - expect(text).not.toContain("This browser"); - expect(text).not.toContain("Fabro CLI"); + // Skeletons render placeholder bars only, no panel headings or content. + expect(text).not.toContain("Browser"); + expect(text).not.toContain("CLI"); }); - test("renders browser and CLI sessions from a unified response", () => { + test("splits browser and CLI sessions into separate panels", () => { currentResponse = { sessions: [cliSession, browserSession] }; const renderer = renderAndTrack(); const text = textFromNode(renderer.toJSON()); - expect(text).toContain("This browser"); - expect(text).toContain("Fabro CLI"); - expect(text).toContain("browser"); - expect(text).toContain("cli"); - expect(text).toContain("alice"); - expect(text).toContain("fabro/0.1.0 Darwin"); + expect(text).toContain("Browser"); + expect(text).toContain("CLI"); + expect(text).toContain("Signed in"); + expect(text).toContain("Expires"); + expect(text).toContain("Last active"); + // Login, provider, user agent, and kind labels are intentionally absent. + expect(text).not.toContain("alice"); + expect(text).not.toContain("github"); + expect(text).not.toContain("fabro/0.1.0 Darwin"); + expect(text).not.toContain("This browser"); + expect(text).not.toContain("Fabro CLI"); }); - test("does not show a revoke button for non-revocable browser sessions", () => { + test("shows an empty CLI panel when no CLI sessions exist", () => { currentResponse = { sessions: [browserSession] }; const renderer = renderAndTrack(); + const text = textFromNode(renderer.toJSON()); + expect(text).toContain("No CLI sessions."); const buttons = renderer.root.findAllByType("button"); expect(buttons).toHaveLength(0); }); @@ -158,7 +164,7 @@ describe("ProfileSessions", () => { const buttons = renderer.root.findAllByType("button"); expect(buttons).toHaveLength(1); - expect(buttons[0].props["aria-label"]).toBe("Revoke Fabro CLI"); + expect(buttons[0].props["aria-label"]).toBe("Revoke CLI session"); }); test("clicking revoke calls the delete endpoint and refreshes the sessions query", async () => { diff --git a/apps/fabro-web/app/routes/profile-sessions.tsx b/apps/fabro-web/app/routes/profile-sessions.tsx index 244e7636e..958dd2222 100644 --- a/apps/fabro-web/app/routes/profile-sessions.tsx +++ b/apps/fabro-web/app/routes/profile-sessions.tsx @@ -6,11 +6,10 @@ import { ApiError, apiData, authApi } from "../lib/api-client"; import { useAuthSessions } from "../lib/queries"; import { queryKeys } from "../lib/query-keys"; import { - Badge, - Mono, Muted, Panel, PanelSkeleton, + Row, } from "../components/settings-panel"; import { formatAbsoluteTs, formatRelativeTime } from "../lib/format"; @@ -36,11 +35,15 @@ export default function ProfileSessions() { return (
+
); } - const sessions = sortSessions(data.sessions); + const browser = data.sessions.find((s) => s.kind === "browser") ?? null; + const cli = data.sessions + .filter((s) => s.kind === "cli") + .sort((a, b) => Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt)); async function revoke(id: string) { setRevokeError(null); @@ -61,12 +64,31 @@ export default function ProfileSessions() { return (
- - {sessions.length === 0 ? ( -
No sessions.
+ + {browser ? ( + <> + + + {formatRelativeTime(browser.createdAt)} + + + {formatAbsoluteTs(browser.expiresAt)} + ) : ( - sessions.map((session) => ( - + No browser session. +
+ )} + + + + {cli.length === 0 ? ( +
+ No CLI sessions. +
+ ) : ( + cli.map((session) => ( + + {revokeError ? (
{ - if (a.current !== b.current) return a.current ? -1 : 1; - return Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt); - }); -} - -function SessionRow({ +function CliRow({ session, onRevoke, pending, @@ -108,55 +124,27 @@ function SessionRow({ disabled: boolean; }) { return ( -
-
-
- {session.label} - {session.kind} - {session.current ? current : null} -
-
- - Provider {session.provider} +
+
+ + Last active{" "} + + {formatRelativeTime(session.lastSeenAt)} - - Login {session.login} - -
-
- - Last active{" "} - - {formatRelativeTime(session.lastSeenAt)} - - - - Expires{" "} - - {formatAbsoluteTs(session.expiresAt)} - - -
- {session.userAgent ? ( -
- User agent{" "} - {session.userAgent} -
- ) : null} -
-
- {session.revocable ? ( - - ) : null} + + + Expires {formatAbsoluteTs(session.expiresAt)} +
+
); }