feat(web): add an open-monitoring-column button to the server list
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
13a1d774de
commit
0c6fafbf60
6 changed files with 76 additions and 12 deletions
|
|
@ -18,4 +18,14 @@ describe("PinColumnButton", () => {
|
||||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||||
expect(onPin).toHaveBeenCalledTimes(1);
|
expect(onPin).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses a custom accessible label when provided", () => {
|
||||||
|
renderWithMantine(
|
||||||
|
<PinColumnButton onPin={vi.fn()} label="Open monitoring column" />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Open monitoring column" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "Pin as column" })).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,24 @@ import { TbLayoutSidebarRightExpand } from "react-icons/tb";
|
||||||
export interface PinColumnButtonProps {
|
export interface PinColumnButtonProps {
|
||||||
/** Pin this screen into the monitoring column to the right. */
|
/** Pin this screen into the monitoring column to the right. */
|
||||||
onPin: () => void;
|
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)
|
* Toolbar button that opens the resizable monitoring column on the right of the
|
||||||
* into the resizable column on the right of the InspectorView (#1616). Distinct
|
* InspectorView (#1616). On a monitor screen (Logs / Protocol / Network) it pins
|
||||||
* from `PinToggle` (which pins individual history entries) — this one opens a
|
* that screen in as a column; on the server list it just opens the column (a
|
||||||
* side column, so it uses a right-sidebar glyph and an "as column" label. Styled
|
* different `label`). Distinct from `PinToggle` (which pins individual history
|
||||||
* to match the panel's expand/collapse `ListToggle` (subtle icon button).
|
* 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 (
|
return (
|
||||||
<Button
|
<Button size="sm" variant="subtle" aria-label={label} onClick={onPin}>
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
aria-label="Pin as column"
|
|
||||||
onClick={onPin}
|
|
||||||
>
|
|
||||||
<TbLayoutSidebarRightExpand size={20} />
|
<TbLayoutSidebarRightExpand size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,31 @@ describe("ServerListControls", () => {
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides the open-monitor button unless onOpenMonitor is provided", () => {
|
||||||
|
renderWithMantine(<ServerListControls {...baseProps} serverCount={2} />);
|
||||||
|
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(
|
||||||
|
<ServerListControls
|
||||||
|
{...baseProps}
|
||||||
|
serverCount={2}
|
||||||
|
onOpenMonitor={onOpenMonitor}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
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", () => {
|
it("shows the list toggle alongside Export + Add Servers when servers exist", () => {
|
||||||
renderWithMantine(<ServerListControls {...baseProps} serverCount={2} />);
|
renderWithMantine(<ServerListControls {...baseProps} serverCount={2} />);
|
||||||
expect(screen.getAllByRole("button")).toHaveLength(3);
|
expect(screen.getAllByRole("button")).toHaveLength(3);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Button, Group } from "@mantine/core";
|
import { Button, Group } from "@mantine/core";
|
||||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
|
import { PinColumnButton } from "../../elements/PinColumnButton/PinColumnButton";
|
||||||
import {
|
import {
|
||||||
ServerAddMenu,
|
ServerAddMenu,
|
||||||
type AddServerMenuProps,
|
type AddServerMenuProps,
|
||||||
|
|
@ -13,6 +14,12 @@ export interface ServerListControlsProps extends AddServerMenuProps {
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
/** When false (read-only session), the Add menu is hidden. Defaults to true. */
|
/** When false (read-only session), the Add menu is hidden. Defaults to true. */
|
||||||
writable?: boolean;
|
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({
|
export function ServerListControls({
|
||||||
|
|
@ -24,6 +31,7 @@ export function ServerListControls({
|
||||||
onImportServerJson,
|
onImportServerJson,
|
||||||
onExport,
|
onExport,
|
||||||
writable = true,
|
writable = true,
|
||||||
|
onOpenMonitor,
|
||||||
}: ServerListControlsProps) {
|
}: ServerListControlsProps) {
|
||||||
return (
|
return (
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
|
|
@ -40,6 +48,9 @@ export function ServerListControls({
|
||||||
{serverCount > 0 && (
|
{serverCount > 0 && (
|
||||||
<ListToggle compact={compact} onToggle={onToggleList} />
|
<ListToggle compact={compact} onToggle={onToggleList} />
|
||||||
)}
|
)}
|
||||||
|
{onOpenMonitor && (
|
||||||
|
<PinColumnButton onPin={onOpenMonitor} label="Open monitoring column" />
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,12 @@ export interface ServerListScreenProps {
|
||||||
onClearHighlight?: (id: string) => void;
|
onClearHighlight?: (id: string) => void;
|
||||||
compact: boolean;
|
compact: boolean;
|
||||||
onToggleCompact: () => void;
|
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({
|
const PageContainer = Stack.withProps({
|
||||||
|
|
@ -99,6 +105,7 @@ export function ServerListScreen({
|
||||||
onClearHighlight,
|
onClearHighlight,
|
||||||
compact,
|
compact,
|
||||||
onToggleCompact,
|
onToggleCompact,
|
||||||
|
onOpenMonitor,
|
||||||
}: ServerListScreenProps) {
|
}: ServerListScreenProps) {
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor),
|
useSensor(PointerSensor),
|
||||||
|
|
@ -196,6 +203,7 @@ export function ServerListScreen({
|
||||||
onImportConfig={onImportConfig}
|
onImportConfig={onImportConfig}
|
||||||
onImportServerJson={onImportServerJson}
|
onImportServerJson={onImportServerJson}
|
||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
|
onOpenMonitor={onOpenMonitor}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollArea.Autosize
|
<ScrollArea.Autosize
|
||||||
|
|
|
||||||
|
|
@ -850,6 +850,10 @@ export function InspectorView({
|
||||||
// column. Gated by `canPin` so the pin affordance only appears when a click
|
// column. Gated by `canPin` so the pin affordance only appears when a click
|
||||||
// would actually take effect (wide + connected).
|
// would actually take effect (wide + connected).
|
||||||
const canPin = !!isWide && connected;
|
const canPin = !!isWide && connected;
|
||||||
|
// Offer an "open monitoring column" affordance on the server list when a
|
||||||
|
// connected server has monitor screens to show but the column isn't open yet.
|
||||||
|
const canOpenMonitor =
|
||||||
|
canPin && monitorAvailable.length > 0 && !effectivePinned;
|
||||||
function pinMonitor(tab: MonitorTab) {
|
function pinMonitor(tab: MonitorTab) {
|
||||||
setMonitorTab(tab);
|
setMonitorTab(tab);
|
||||||
setMonitorPinned(true);
|
setMonitorPinned(true);
|
||||||
|
|
@ -1068,6 +1072,11 @@ export function InspectorView({
|
||||||
onClearHighlight={onClearHighlight}
|
onClearHighlight={onClearHighlight}
|
||||||
compact={serversCompact}
|
compact={serversCompact}
|
||||||
onToggleCompact={() => setServersCompact((c) => !c)}
|
onToggleCompact={() => setServersCompact((c) => !c)}
|
||||||
|
onOpenMonitor={
|
||||||
|
canOpenMonitor
|
||||||
|
? () => pinMonitor(effectiveMonitorTab)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ScreenStage>
|
</ScreenStage>
|
||||||
<ScreenStage active={activeTab === "Tools"}>
|
<ScreenStage active={activeTab === "Tools"}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue