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
3 changed files with 1728 additions and 241 deletions
|
|
@ -0,0 +1,43 @@
|
|||
import { createRef } from "react";
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, within } from "storybook/test";
|
||||
import { EmbeddableScrollArea } from "./EmbeddableScrollArea";
|
||||
|
||||
const meta: Meta<typeof EmbeddableScrollArea> = {
|
||||
title: "Elements/EmbeddableScrollArea",
|
||||
component: EmbeddableScrollArea,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EmbeddableScrollArea>;
|
||||
|
||||
const rows = Array.from({ length: 40 }, (_, i) => (
|
||||
<Text key={i}>Row {i + 1}</Text>
|
||||
));
|
||||
|
||||
export const FullSize: Story = {
|
||||
render: () => (
|
||||
<EmbeddableScrollArea embedded={false} viewportRef={createRef()}>
|
||||
<Stack gap="xs">{rows}</Stack>
|
||||
</EmbeddableScrollArea>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const Embedded: Story = {
|
||||
render: () => (
|
||||
<Stack h={240} gap={0}>
|
||||
<EmbeddableScrollArea embedded viewportRef={createRef()}>
|
||||
<Stack gap="xs">{rows}</Stack>
|
||||
</EmbeddableScrollArea>
|
||||
</Stack>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Row 1")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { createRef } from "react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||
import { EmbeddableScrollArea } from "./EmbeddableScrollArea";
|
||||
|
||||
describe("EmbeddableScrollArea", () => {
|
||||
it("renders its children full-size", () => {
|
||||
renderWithMantine(
|
||||
<EmbeddableScrollArea embedded={false} viewportRef={createRef()}>
|
||||
<div>full-size content</div>
|
||||
</EmbeddableScrollArea>,
|
||||
);
|
||||
expect(screen.getByText("full-size content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders its children when embedded", () => {
|
||||
renderWithMantine(
|
||||
<EmbeddableScrollArea embedded={true} viewportRef={createRef()}>
|
||||
<div>embedded content</div>
|
||||
</EmbeddableScrollArea>,
|
||||
);
|
||||
expect(screen.getByText("embedded content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("attaches the viewport ref to the scroll viewport", () => {
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
renderWithMantine(
|
||||
<EmbeddableScrollArea embedded={false} viewportRef={ref}>
|
||||
<div>content</div>
|
||||
</EmbeddableScrollArea>,
|
||||
);
|
||||
expect(ref.current).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import type { ReactNode, Ref } from "react";
|
||||
import { ScrollArea, Stack } from "@mantine/core";
|
||||
|
||||
// Full-size, the monitor stream panels bound their scroll to the viewport
|
||||
// minus the header and the panel's own chrome.
|
||||
const FULLSIZE_MAH =
|
||||
"calc(100vh - var(--app-shell-header-height, 0px) - 150px)";
|
||||
|
||||
export interface EmbeddableScrollAreaProps {
|
||||
/**
|
||||
* True when rendered inside the pinned monitoring column (#1616): the scroll
|
||||
* region fills its flex parent instead of using the viewport calc. A
|
||||
* `flex:1 / mih:0` wrapper caps the inner `ScrollArea` at the space remaining
|
||||
* below the column's controls (via `mah:100%`), so no viewport math is needed
|
||||
* and the final rows never clip.
|
||||
*/
|
||||
embedded: boolean;
|
||||
viewportRef: Ref<HTMLDivElement>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll region shared by the Logs / History / Network stream panels, which
|
||||
* render both full-size (their own tab) and embedded (the monitoring column).
|
||||
* Centralizes the one layout difference between those two hosts.
|
||||
*/
|
||||
export function EmbeddableScrollArea({
|
||||
embedded,
|
||||
viewportRef,
|
||||
children,
|
||||
}: EmbeddableScrollAreaProps) {
|
||||
if (embedded) {
|
||||
return (
|
||||
<Stack flex={1} mih={0} gap={0}>
|
||||
<ScrollArea.Autosize
|
||||
viewportRef={viewportRef}
|
||||
mah="100%"
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
{children}
|
||||
</ScrollArea.Autosize>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ScrollArea.Autosize
|
||||
viewportRef={viewportRef}
|
||||
mah={FULLSIZE_MAH}
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
{children}
|
||||
</ScrollArea.Autosize>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue