From 0c6fafbf604473a7e4262f8dd6334012822e0bc8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 10 Jul 2026 20:23:27 -0400 Subject: [PATCH] feat(web): add an open-monitoring-column button to the server list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a server is connected, has monitor screens to show, and the monitoring column isn't open, show an "open monitoring column" button to the right of the list expand/collapse toggle on the server list. It reuses PinColumnButton (the same right-sidebar affordance the monitor screens use, now with an optional label), so opening the column is reachable from the server list — not only from a monitor screen's pin button. InspectorView gates it on `canPin && monitorAvailable.length > 0 && !effectivePinned`. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- .../PinColumnButton/PinColumnButton.test.tsx | 10 ++++++++ .../PinColumnButton/PinColumnButton.tsx | 25 ++++++++++--------- .../ServerListControls.test.tsx | 25 +++++++++++++++++++ .../ServerListControls/ServerListControls.tsx | 11 ++++++++ .../ServerListScreen/ServerListScreen.tsx | 8 ++++++ .../views/InspectorView/InspectorView.tsx | 9 +++++++ 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/clients/web/src/components/elements/PinColumnButton/PinColumnButton.test.tsx b/clients/web/src/components/elements/PinColumnButton/PinColumnButton.test.tsx index 50ce1b62..79ab1449 100644 --- a/clients/web/src/components/elements/PinColumnButton/PinColumnButton.test.tsx +++ b/clients/web/src/components/elements/PinColumnButton/PinColumnButton.test.tsx @@ -18,4 +18,14 @@ describe("PinColumnButton", () => { await user.click(screen.getByRole("button", { name: "Pin as column" })); expect(onPin).toHaveBeenCalledTimes(1); }); + + it("uses a custom accessible label when provided", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("button", { name: "Open monitoring column" }), + ).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Pin as column" })).toBeNull(); + }); }); diff --git a/clients/web/src/components/elements/PinColumnButton/PinColumnButton.tsx b/clients/web/src/components/elements/PinColumnButton/PinColumnButton.tsx index 8689eaae..d101d90a 100644 --- a/clients/web/src/components/elements/PinColumnButton/PinColumnButton.tsx +++ b/clients/web/src/components/elements/PinColumnButton/PinColumnButton.tsx @@ -4,23 +4,24 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb"; export interface PinColumnButtonProps { /** Pin this screen into the monitoring column to the right. */ onPin: () => void; + /** Accessible label. Defaults to "Pin as column" (its monitor-screen use). */ + label?: string; } /** - * Toolbar button that pins the owning monitor screen (Logs / Protocol / Network) - * into the resizable column on the right of the InspectorView (#1616). Distinct - * from `PinToggle` (which pins individual history entries) — this one opens a - * side column, so it uses a right-sidebar glyph and an "as column" label. Styled - * to match the panel's expand/collapse `ListToggle` (subtle icon button). + * Toolbar button that opens the resizable monitoring column on the right of the + * InspectorView (#1616). On a monitor screen (Logs / Protocol / Network) it pins + * that screen in as a column; on the server list it just opens the column (a + * different `label`). Distinct from `PinToggle` (which pins individual history + * entries) — this one opens a side column, so it uses a right-sidebar glyph. + * Styled to match the panel's expand/collapse `ListToggle` (subtle icon button). */ -export function PinColumnButton({ onPin }: PinColumnButtonProps) { +export function PinColumnButton({ + onPin, + label = "Pin as column", +}: PinColumnButtonProps) { return ( - ); diff --git a/clients/web/src/components/groups/ServerListControls/ServerListControls.test.tsx b/clients/web/src/components/groups/ServerListControls/ServerListControls.test.tsx index 3cfde766..247ce910 100644 --- a/clients/web/src/components/groups/ServerListControls/ServerListControls.test.tsx +++ b/clients/web/src/components/groups/ServerListControls/ServerListControls.test.tsx @@ -24,6 +24,31 @@ describe("ServerListControls", () => { ).toBeInTheDocument(); }); + it("hides the open-monitor button unless onOpenMonitor is provided", () => { + renderWithMantine(); + expect( + screen.queryByRole("button", { name: "Open monitoring column" }), + ).toBeNull(); + }); + + it("shows and wires the open-monitor button when onOpenMonitor is provided", async () => { + const user = userEvent.setup(); + const onOpenMonitor = vi.fn(); + renderWithMantine( + , + ); + const openBtn = screen.getByRole("button", { + name: "Open monitoring column", + }); + expect(openBtn).toBeInTheDocument(); + await user.click(openBtn); + expect(onOpenMonitor).toHaveBeenCalledTimes(1); + }); + it("shows the list toggle alongside Export + Add Servers when servers exist", () => { renderWithMantine(); expect(screen.getAllByRole("button")).toHaveLength(3); diff --git a/clients/web/src/components/groups/ServerListControls/ServerListControls.tsx b/clients/web/src/components/groups/ServerListControls/ServerListControls.tsx index ed6f0351..3c559a70 100644 --- a/clients/web/src/components/groups/ServerListControls/ServerListControls.tsx +++ b/clients/web/src/components/groups/ServerListControls/ServerListControls.tsx @@ -1,5 +1,6 @@ import { Button, Group } from "@mantine/core"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; +import { PinColumnButton } from "../../elements/PinColumnButton/PinColumnButton"; import { ServerAddMenu, type AddServerMenuProps, @@ -13,6 +14,12 @@ export interface ServerListControlsProps extends AddServerMenuProps { onExport: () => void; /** When false (read-only session), the Add menu is hidden. Defaults to true. */ writable?: boolean; + /** + * Open the monitoring column. Provided (by the caller) only when a server is + * connected, the column can open, and it isn't already shown — otherwise + * omitted so the open-sidebar affordance is hidden. + */ + onOpenMonitor?: () => void; } export function ServerListControls({ @@ -24,6 +31,7 @@ export function ServerListControls({ onImportServerJson, onExport, writable = true, + onOpenMonitor, }: ServerListControlsProps) { return ( @@ -40,6 +48,9 @@ export function ServerListControls({ {serverCount > 0 && ( )} + {onOpenMonitor && ( + + )} ); } diff --git a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx index f27b0d33..f282d071 100644 --- a/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx +++ b/clients/web/src/components/screens/ServerListScreen/ServerListScreen.tsx @@ -67,6 +67,12 @@ export interface ServerListScreenProps { onClearHighlight?: (id: string) => void; compact: boolean; onToggleCompact: () => void; + /** + * Open the monitoring column. Provided only when a server is connected, the + * column can open, and it isn't already shown; forwarded to the controls to + * reveal an open-sidebar button beside the list toggle. + */ + onOpenMonitor?: () => void; } const PageContainer = Stack.withProps({ @@ -99,6 +105,7 @@ export function ServerListScreen({ onClearHighlight, compact, onToggleCompact, + onOpenMonitor, }: ServerListScreenProps) { const sensors = useSensors( useSensor(PointerSensor), @@ -196,6 +203,7 @@ export function ServerListScreen({ onImportConfig={onImportConfig} onImportServerJson={onImportServerJson} onExport={onExport} + onOpenMonitor={onOpenMonitor} /> 0 && !effectivePinned; function pinMonitor(tab: MonitorTab) { setMonitorTab(tab); setMonitorPinned(true); @@ -1068,6 +1072,11 @@ export function InspectorView({ onClearHighlight={onClearHighlight} compact={serversCompact} onToggleCompact={() => setServersCompact((c) => !c)} + onOpenMonitor={ + canOpenMonitor + ? () => pinMonitor(effectiveMonitorTab) + : undefined + } />