Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const FULLSIZE_MAH =

export interface EmbeddableScrollAreaProps {
/**
* True when rendered inside the pinned monitoring column (#1616): the scroll
* True when rendered inside the pinned monitoring sidebar (#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
Expand Down Expand Up @@ -36,7 +36,7 @@ const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const;

/**
* The scroll region shared by the Logs / Protocol / Network stream panels, which
* render both full-size (their own tab) and embedded (the monitoring column).
* render both full-size (their own tab) and embedded (the monitoring sidebar).
* Centralizes the one layout difference between those two hosts.
*/
export function EmbeddableScrollArea({
Expand Down
22 changes: 12 additions & 10 deletions clients/web/src/components/elements/ExpandToggle/ExpandToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionIcon } from "@mantine/core";
import { ActionIcon, Tooltip } from "@mantine/core";
import { RiCollapseVerticalLine, RiExpandVerticalLine } from "react-icons/ri";

export interface ExpandToggleProps {
Expand All @@ -18,14 +18,16 @@ export function ExpandToggle({ expanded, onToggle }: ExpandToggleProps) {
const Icon = expanded ? RiCollapseVerticalLine : RiExpandVerticalLine;
const label = expanded ? "Collapse" : "Expand";
return (
<ActionIcon
variant="subtle"
color="gray"
size="md"
aria-label={label}
onClick={onToggle}
>
<Icon size={16} />
</ActionIcon>
<Tooltip label={label}>
<ActionIcon
variant="subtle"
color="gray"
size="md"
aria-label={label}
onClick={onToggle}
>
<Icon size={16} />
</ActionIcon>
</Tooltip>
);
}
40 changes: 22 additions & 18 deletions clients/web/src/components/elements/ListToggle/ListToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionIcon } from "@mantine/core";
import { ActionIcon, Tooltip } from "@mantine/core";
import { RiExpandVerticalLine, RiCollapseVerticalLine } from "react-icons/ri";

export interface ListToggleProps {
Expand All @@ -17,28 +17,32 @@ export function ListToggle({

if (variant === "subtle") {
return (
<ActionIcon
variant="subtle"
color="gray"
size="md"
aria-label={label}
onClick={onToggle}
>
<Icon size={16} />
</ActionIcon>
<Tooltip label={label}>
<ActionIcon
variant="subtle"
color="gray"
size="md"
aria-label={label}
onClick={onToggle}
>
<Icon size={16} />
</ActionIcon>
</Tooltip>
);
}

// `size={36}` matches the header's theme / client-settings ActionIcons so the
// toolbar's toggle reads as the same size icon button.
return (
<ActionIcon
variant="subtle"
size={36}
aria-label={label}
onClick={onToggle}
>
<Icon size={20} />
</ActionIcon>
<Tooltip label={label}>
<ActionIcon
variant="subtle"
size={36}
aria-label={label}
onClick={onToggle}
>
<Icon size={20} />
</ActionIcon>
</Tooltip>
);
}
25 changes: 25 additions & 0 deletions clients/web/src/components/elements/LogEntry/LogEntry.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Box } from "@mantine/core";
import { LogEntry } from "./LogEntry";

const meta: Meta<typeof LogEntry> = {
Expand Down Expand Up @@ -78,3 +79,27 @@ export const WithJsonData: Story = {
},
},
};

// The narrow monitoring-column layout (#1661): time/level/logger on the first
// line, the (potentially long) message wrapping on the line below. Rendered in a
// column-width box so the wrapping is visible.
export const Compact: Story = {
args: {
compact: true,
entry: {
receivedAt: new Date("2026-03-29T20:18:26.000Z"),
params: {
level: "warning",
data: "Connection pool nearing capacity — 92 of 100 connections in use; consider raising the limit",
logger: "pool",
},
},
},
decorators: [
(Story) => (
<Box maw={360}>
<Story />
</Box>
),
],
};
17 changes: 17 additions & 0 deletions clients/web/src/components/elements/LogEntry/LogEntry.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,21 @@ describe("LogEntry", () => {
);
expect(container.textContent).toContain("info");
});

it("renders the compact two-line layout with time, level, logger, and message", () => {
renderWithMantine(
<LogEntry
entry={baseEntry({
data: "compact message",
level: "warning",
logger: "transport",
})}
compact
/>,
);
// All four pieces still render in the compact variant.
expect(screen.getByText("warning")).toBeInTheDocument();
expect(screen.getByText("[transport]")).toBeInTheDocument();
expect(screen.getByText("compact message")).toBeInTheDocument();
});
});
57 changes: 51 additions & 6 deletions clients/web/src/components/elements/LogEntry/LogEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Group, Text } from "@mantine/core";
import { Group, Stack, Text } from "@mantine/core";
import type {
LoggingLevel,
LoggingMessageNotification,
Expand All @@ -12,6 +12,13 @@ export interface LogEntryData {

export interface LogEntryProps {
entry: LogEntryData;
/**
* Compact two-line layout for the narrow monitoring sidebar (#1661): the
* timestamp, level, and logger sit on the first line and the message wraps
* onto the line below, so a long message isn't clipped by the column width.
* The default (false) is the single-line row used on the full Logs screen.
*/
compact?: boolean;
}

const levelMessageColor: Record<LoggingLevel, string | undefined> = {
Expand Down Expand Up @@ -51,18 +58,56 @@ const LoggerText = Text.withProps({
c: "dimmed",
});

export function LogEntry({ entry }: LogEntryProps) {
// Single-line message (full Logs screen): sits inline with the meta on one row.
const MessageText = Text.withProps({
size: "sm",
ff: "monospace",
});

// Compact message (monitoring sidebar): wraps over-long lines within the narrow
// column via the `consoleLine` variant instead of overflowing its width.
const CompactMessageText = Text.withProps({
size: "sm",
ff: "monospace",
variant: "consoleLine",
});

// The compact meta row: timestamp + level + logger on one line above the
// message. `wrap: nowrap` keeps them on a single line; the message wraps below.
const MetaRow = Group.withProps({
gap: "xs",
wrap: "nowrap",
align: "center",
});

export function LogEntry({ entry, compact = false }: LogEntryProps) {
const { receivedAt, params } = entry;
const message = formatData(params.data);
const logger = params.logger ? (
<LoggerText>{formatLogger(params.logger)}</LoggerText>
) : null;

if (compact) {
return (
<Stack gap={2}>
<MetaRow>
<TimestampText>{formatTimestamp(receivedAt)}</TimestampText>
<LogLevelBadge level={params.level} />
{logger}
</MetaRow>
<CompactMessageText c={levelMessageColor[params.level]}>
{message}
</CompactMessageText>
</Stack>
);
}

return (
<Group gap="sm" wrap="nowrap">
<TimestampText>{formatTimestamp(receivedAt)}</TimestampText>
<LogLevelBadge level={params.level} />
{params.logger && <LoggerText>{formatLogger(params.logger)}</LoggerText>}
<Text size="sm" ff="monospace" c={levelMessageColor[params.level]}>
{message}
</Text>
{logger}
<MessageText c={levelMessageColor[params.level]}>{message}</MessageText>
</Group>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, within } from "storybook/test";
import { MonitoringToggle } from "./MonitoringToggle";

const meta: Meta<typeof MonitoringToggle> = {
title: "Elements/MonitoringToggle",
component: MonitoringToggle,
args: { open: false, onToggle: fn() },
};

export default meta;
type Story = StoryObj<typeof MonitoringToggle>;

export const Closed: Story = {
args: { open: false },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("button", { name: "Open monitoring sidebar" }),
).toBeInTheDocument();
},
};

export const Open: Story = {
args: { open: true },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("button", { name: "Close monitoring sidebar" }),
).toBeInTheDocument();
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { MonitoringToggle } from "./MonitoringToggle";

describe("MonitoringToggle", () => {
it("renders an open-column button when closed", () => {
renderWithMantine(<MonitoringToggle open={false} onToggle={vi.fn()} />);
expect(
screen.getByRole("button", { name: "Open monitoring sidebar" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
).toBeNull();
});

it("renders a close-column button when open", () => {
renderWithMantine(<MonitoringToggle open onToggle={vi.fn()} />);
expect(
screen.getByRole("button", { name: "Close monitoring sidebar" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Open monitoring sidebar" }),
).toBeNull();
});

it("invokes onToggle when clicked", async () => {
const user = userEvent.setup();
const onToggle = vi.fn();
renderWithMantine(<MonitoringToggle open={false} onToggle={onToggle} />);
await user.click(
screen.getByRole("button", { name: "Open monitoring sidebar" }),
);
expect(onToggle).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ActionIcon, Tooltip } from "@mantine/core";
import {
TbLayoutSidebarRightExpand,
TbLayoutSidebarRightCollapse,
} from "react-icons/tb";

export interface MonitoringToggleProps {
/** Whether the monitoring sidebar is currently open. */
open: boolean;
/** Open the sidebar when closed, close it when open. */
onToggle: () => void;
}

/**
* The single header affordance for the monitoring sidebar (#1661). It replaces
* the per-screen pin buttons, the server-list open-sidebar button, and the
* sidebar's own close button — one toggle, placed in the header to the right of
* the theme icon, that opens or closes the sidebar on demand. The glyph and
* label reflect the current state (expand when closed, collapse when open). The
* caller only mounts it when the sidebar is available (connected, or a failed
* connect attempt, on a wide viewport), so it never appears with nothing to
* toggle. `size={36}` matches the header's theme / client-settings ActionIcons.
*/
export function MonitoringToggle({ open, onToggle }: MonitoringToggleProps) {
const Icon = open ? TbLayoutSidebarRightCollapse : TbLayoutSidebarRightExpand;
const label = open ? "Close monitoring sidebar" : "Open monitoring sidebar";

return (
<Tooltip label={label}>
<ActionIcon
variant="subtle"
size={36}
aria-label={label}
onClick={onToggle}
>
<Icon size={20} />
</ActionIcon>
</Tooltip>
);
}

This file was deleted.

Loading
Loading