feat(web): monitoring column search, close-to-fullscreen, and open/close animation (#1616)

- Add a search box to MonitoringControls (right of the tabs). It's a single
  shared search kept across column tab switches, filtering whichever screen is
  shown. Embedded panels apply this text but still ignore the sidebar-only
  filters (levels/categories/directions/method), which have no control there.
- Close button now uses the "collapse sidebar" icon (TbLayoutSidebarRightCollapse),
  the mirror of the pin button's expand icon.
- Closing the column carries its selection to the primary view: the screen the
  user was watching becomes the active header tab (rather than snapping back to
  the pre-pin selection).
- Animate the column open/close: the handle + column slide in from the right and
  fade (reversing on close) via a Mantine Transition.
- HistoryListPanel: drop the redundant "History (N)" header when there are no
  pinned requests (lone unpinned section renders headerless).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-08 20:16:15 -04:00
commit c5e064ff71
3 changed files with 448 additions and 169 deletions

View file

@ -35,6 +35,8 @@ const meta: Meta<typeof MonitoringScreen> = {
tabs: ["Logs", "History", "Network"],
value: "Logs",
onChange: fn(),
searchValue: "",
onSearchChange: fn(),
onClose: fn(),
screens: demoScreens,
},

View file

@ -1,7 +1,10 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { MonitoringScreen } from "./MonitoringScreen";
import {
MonitoringScreen,
type MonitoringScreenProps,
} from "./MonitoringScreen";
const TABS = ["Logs", "History", "Network"];
@ -13,32 +16,31 @@ function screens() {
};
}
function renderScreen(overrides: Partial<MonitoringScreenProps> = {}) {
const props: MonitoringScreenProps = {
tabs: TABS,
value: "Logs",
onChange: vi.fn(),
searchValue: "",
onSearchChange: vi.fn(),
onClose: vi.fn(),
screens: screens(),
...overrides,
};
renderWithMantine(<MonitoringScreen {...props} />);
return props;
}
describe("MonitoringScreen", () => {
it("renders the screen for the active tab", () => {
renderWithMantine(
<MonitoringScreen
tabs={TABS}
value="History"
onChange={vi.fn()}
onClose={vi.fn()}
screens={screens()}
/>,
);
renderScreen({ value: "History" });
expect(screen.getByText("history-body")).toBeInTheDocument();
expect(screen.queryByText("logs-body")).toBeNull();
expect(screen.queryByText("network-body")).toBeNull();
});
it("renders the controls tab row", () => {
renderWithMantine(
<MonitoringScreen
tabs={TABS}
value="Logs"
onChange={vi.fn()}
onClose={vi.fn()}
screens={screens()}
/>,
);
renderScreen();
expect(screen.getByRole("radio", { name: "Logs" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Close monitoring column" }),
@ -49,15 +51,7 @@ describe("MonitoringScreen", () => {
const user = userEvent.setup();
const onChange = vi.fn();
const onClose = vi.fn();
renderWithMantine(
<MonitoringScreen
tabs={TABS}
value="Logs"
onChange={onChange}
onClose={onClose}
screens={screens()}
/>,
);
renderScreen({ onChange, onClose });
await user.click(screen.getByRole("radio", { name: "Network" }));
expect(onChange).toHaveBeenCalledWith("Network");
await user.click(
@ -66,16 +60,18 @@ describe("MonitoringScreen", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
it("forwards the search value and changes from the controls", async () => {
const user = userEvent.setup();
const onSearchChange = vi.fn();
renderScreen({ searchValue: "hi", onSearchChange });
const box = screen.getByRole("textbox", { name: "Search" });
expect(box).toHaveValue("hi");
await user.type(box, "!");
expect(onSearchChange).toHaveBeenCalledWith("hi!");
});
it("renders nothing in the slot when the active tab has no screen", () => {
renderWithMantine(
<MonitoringScreen
tabs={TABS}
value="Network"
onChange={vi.fn()}
onClose={vi.fn()}
screens={{ Logs: <div>logs-body</div> }}
/>,
);
renderScreen({ value: "Network", screens: { Logs: <div>logs-body</div> } });
expect(screen.queryByText("logs-body")).toBeNull();
});
});

View file

@ -8,6 +8,9 @@ export interface MonitoringScreenProps {
/** Active monitor tab; keys into `screens` to pick what renders below. */
value: string;
onChange: (tab: string) => void;
/** Search text for the active screen; wired by the caller to its filter state. */
searchValue: string;
onSearchChange: (next: string) => void;
onClose: () => void;
/**
* The embedded screen node for each tab, keyed by tab label. The caller builds
@ -38,6 +41,8 @@ export function MonitoringScreen({
tabs,
value,
onChange,
searchValue,
onSearchChange,
onClose,
screens,
}: MonitoringScreenProps) {
@ -47,6 +52,8 @@ export function MonitoringScreen({
tabs={tabs}
value={value}
onChange={onChange}
searchValue={searchValue}
onSearchChange={onSearchChange}
onClose={onClose}
/>
<Divider />