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:
parent
9b76272ad1
commit
c5e064ff71
3 changed files with 448 additions and 169 deletions
|
|
@ -35,6 +35,8 @@ const meta: Meta<typeof MonitoringScreen> = {
|
|||
tabs: ["Logs", "History", "Network"],
|
||||
value: "Logs",
|
||||
onChange: fn(),
|
||||
searchValue: "",
|
||||
onSearchChange: fn(),
|
||||
onClose: fn(),
|
||||
screens: demoScreens,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
Loading…
Reference in a new issue