feat(web): pin Logging/History/Network as a resizable column (#1616)
Add the ability to "pin" the Logging, History, and Network screens into a second column on the right of the InspectorView, so a primary screen and a monitor screen are visible at once (e.g. watch Logs while calling a Tool). - A "pin as column" button on the Logs/History/Network panel toolbars opens the column (shown only when wide + connected). - The column hosts a MonitoringControls tab row (available monitor tabs + close) over the selected embedded screen. Pinning removes the monitor group from the header tab bar; closing (x) returns it. - The divider between the primary area and the column is draggable (pointer + keyboard); width is clamped and persisted. - The split collapses to one column below ~1040px (the ServerListScreen single-card breakpoint) and while disconnected, without discarding the pin preference — it reopens when wide + connected again. State (pinned/tab/width) is view-local and persisted via the existing useLocalStorage pattern, so no new props thread through App and ViewHeader is unchanged (it just receives a filtered tab list). Screens/panels gain an `embedded` mode (fills the parent height, drops the 340px filter sidebar) via a shared EmbeddableScrollArea element. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4ee9bbce0f
commit
081fd789f1
6 changed files with 1728 additions and 241 deletions
|
|
@ -111,4 +111,19 @@ describe("HistoryScreen", () => {
|
|||
expect.objectContaining({ methodFilter: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<HistoryScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the request list", () => {
|
||||
renderWithMantine(<HistoryScreen {...baseProps} embedded />);
|
||||
expect(screen.getByText("Requests")).toBeInTheDocument();
|
||||
// The sidebar (HistoryControls, with its Search box) is not rendered.
|
||||
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export interface HistoryScreenProps {
|
|||
onSortChange: (next: SortDirection) => void;
|
||||
compact: boolean;
|
||||
onToggleCompact: () => void;
|
||||
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
|
||||
onPin?: () => void;
|
||||
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
// Search text, method filter, and per-direction visibility — controlled by the
|
||||
|
|
@ -69,6 +73,8 @@ export function HistoryScreen({
|
|||
onSortChange,
|
||||
compact,
|
||||
onToggleCompact,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: HistoryScreenProps) {
|
||||
const { search, methodFilter, visibleDirections } = ui;
|
||||
|
||||
|
|
@ -101,23 +107,25 @@ export function HistoryScreen({
|
|||
}, [ui, visibleDirections, onUiChange]);
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<HistoryControls
|
||||
searchText={search}
|
||||
methodFilter={methodFilter}
|
||||
availableMethods={availableMethods}
|
||||
visibleDirections={visibleDirections}
|
||||
onSearchChange={(value) => onUiChange({ ...ui, search: value })}
|
||||
onMethodFilterChange={(value) =>
|
||||
onUiChange({ ...ui, methodFilter: value })
|
||||
}
|
||||
onToggleDirection={handleToggleDirection}
|
||||
onToggleAllDirections={handleToggleAllDirections}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
<ScreenLayout h={embedded ? "100%" : undefined}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<HistoryControls
|
||||
searchText={search}
|
||||
methodFilter={methodFilter}
|
||||
availableMethods={availableMethods}
|
||||
visibleDirections={visibleDirections}
|
||||
onSearchChange={(value) => onUiChange({ ...ui, search: value })}
|
||||
onMethodFilterChange={(value) =>
|
||||
onUiChange({ ...ui, methodFilter: value })
|
||||
}
|
||||
onToggleDirection={handleToggleDirection}
|
||||
onToggleAllDirections={handleToggleAllDirections}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
)}
|
||||
<HistoryListPanel
|
||||
entries={entries}
|
||||
pinnedIds={pinnedIds}
|
||||
|
|
@ -134,6 +142,8 @@ export function HistoryScreen({
|
|||
onSortChange={onSortChange}
|
||||
compact={compact}
|
||||
onToggleCompact={onToggleCompact}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -131,4 +131,19 @@ describe("LoggingScreen", () => {
|
|||
await user.click(screen.getByRole("button", { name: "Set" }));
|
||||
expect(onSetLevel).toHaveBeenCalledWith("info");
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<LoggingScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the stream", () => {
|
||||
renderWithMantine(<LoggingScreen {...baseProps} embedded />);
|
||||
expect(screen.getByText("Log Stream")).toBeInTheDocument();
|
||||
// The sidebar (LogControls, with its Set button) is not rendered.
|
||||
expect(screen.queryByRole("button", { name: "Set" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,17 @@ export interface LoggingScreenProps {
|
|||
onExport: () => void;
|
||||
sortDirection: SortDirection;
|
||||
onSortChange: (next: SortDirection) => void;
|
||||
/**
|
||||
* When set, the stream panel shows a "pin as column" button (#1616). Omitted
|
||||
* when already embedded, or when pinning isn't currently available.
|
||||
*/
|
||||
onPin?: () => void;
|
||||
/**
|
||||
* True when rendered inside the monitoring column: the screen fills its
|
||||
* parent's height (instead of the viewport calc) and drops the filter
|
||||
* sidebar so the narrow column is stream-only.
|
||||
*/
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
// Filter text + visible-level set — controlled by the parent (App) as one
|
||||
|
|
@ -52,6 +63,8 @@ export function LoggingScreen({
|
|||
onExport,
|
||||
sortDirection,
|
||||
onSortChange,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: LoggingScreenProps) {
|
||||
const { filterText, visibleLevels } = ui;
|
||||
|
||||
|
|
@ -71,20 +84,24 @@ export function LoggingScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<LogControls
|
||||
currentLevel={currentLevel}
|
||||
filterText={filterText}
|
||||
visibleLevels={visibleLevels}
|
||||
onSetLevel={onSetLevel}
|
||||
onFilterChange={(value) => onUiChange({ ...ui, filterText: value })}
|
||||
onToggleLevel={handleToggleLevel}
|
||||
onToggleAllLevels={handleToggleAllLevels}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
<ScreenLayout h={embedded ? "100%" : undefined}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<LogControls
|
||||
currentLevel={currentLevel}
|
||||
filterText={filterText}
|
||||
visibleLevels={visibleLevels}
|
||||
onSetLevel={onSetLevel}
|
||||
onFilterChange={(value) =>
|
||||
onUiChange({ ...ui, filterText: value })
|
||||
}
|
||||
onToggleLevel={handleToggleLevel}
|
||||
onToggleAllLevels={handleToggleAllLevels}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
)}
|
||||
<LogStreamPanel
|
||||
entries={entries}
|
||||
filterText={filterText}
|
||||
|
|
@ -93,6 +110,8 @@ export function LoggingScreen({
|
|||
onExport={onExport}
|
||||
sortDirection={sortDirection}
|
||||
onSortChange={onSortChange}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -163,4 +163,20 @@ describe("NetworkScreen", () => {
|
|||
expect(screen.getByRole("button", { name: "Clear" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Export" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("renders a pin-as-column button when onPin is provided and invokes it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPin = vi.fn();
|
||||
renderWithMantine(<NetworkScreen {...baseProps} onPin={onPin} />);
|
||||
await user.click(screen.getByRole("button", { name: "Pin as column" }));
|
||||
expect(onPin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops the filter sidebar when embedded, keeping the request list", () => {
|
||||
renderWithMantine(<NetworkScreen {...baseProps} embedded />);
|
||||
// The sidebar (NetworkControls, with its Search box) is not rendered.
|
||||
expect(screen.queryByPlaceholderText("Search...")).toBeNull();
|
||||
// The stream toolbar (Export) is still present.
|
||||
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export interface NetworkScreenProps {
|
|||
onSortChange: (next: SortDirection) => void;
|
||||
compact: boolean;
|
||||
onToggleCompact: () => void;
|
||||
/** See LoggingScreen: shows a "pin as column" button when set (#1616). */
|
||||
onPin?: () => void;
|
||||
/** See LoggingScreen: fills the parent height and drops the filter sidebar. */
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
// Filter text + visible-category set — controlled by the parent (App) as one
|
||||
|
|
@ -57,6 +61,8 @@ export function NetworkScreen({
|
|||
onSortChange,
|
||||
compact,
|
||||
onToggleCompact,
|
||||
onPin,
|
||||
embedded = false,
|
||||
}: NetworkScreenProps) {
|
||||
const { filterText, visibleCategories } = ui;
|
||||
|
||||
|
|
@ -81,18 +87,22 @@ export function NetworkScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<NetworkControls
|
||||
filterText={filterText}
|
||||
visibleCategories={visibleCategories}
|
||||
onFilterChange={(value) => onUiChange({ ...ui, filterText: value })}
|
||||
onToggleCategory={handleToggleCategory}
|
||||
onToggleAllCategories={handleToggleAllCategories}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
<ScreenLayout h={embedded ? "100%" : undefined}>
|
||||
{embedded ? null : (
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<NetworkControls
|
||||
filterText={filterText}
|
||||
visibleCategories={visibleCategories}
|
||||
onFilterChange={(value) =>
|
||||
onUiChange({ ...ui, filterText: value })
|
||||
}
|
||||
onToggleCategory={handleToggleCategory}
|
||||
onToggleAllCategories={handleToggleAllCategories}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
)}
|
||||
<NetworkStreamPanel
|
||||
entries={entries}
|
||||
filterText={filterText}
|
||||
|
|
@ -103,6 +113,8 @@ export function NetworkScreen({
|
|||
onSortChange={onSortChange}
|
||||
compact={compact}
|
||||
onToggleCompact={onToggleCompact}
|
||||
onPin={onPin}
|
||||
embedded={embedded}
|
||||
/>
|
||||
</ScreenLayout>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue