Merge pull request #1634 from modelcontextprotocol/1623-rename-history-to-protocol
refactor(web): rename "History" tab/feature to "Protocol" (#1623)
This commit is contained in:
commit
50e780ff71
52 changed files with 524 additions and 429 deletions
|
|
@ -309,10 +309,10 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
onClearCompletedTasks: () => void;
|
||||
onRefreshTasks: () => void;
|
||||
onServerSettings: (id: string) => void;
|
||||
onClearHistory: () => void;
|
||||
onReplayHistory: (id: string) => void;
|
||||
onTogglePinHistory: (id: string) => void;
|
||||
pinnedHistoryIds?: Set<string>;
|
||||
onClearProtocol: () => void;
|
||||
onReplayProtocol: (id: string) => void;
|
||||
onTogglePinProtocol: (id: string) => void;
|
||||
pinnedProtocolIds?: Set<string>;
|
||||
}) => (
|
||||
<div>
|
||||
<span data-testid="tool-status">
|
||||
|
|
@ -419,15 +419,15 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
<button onClick={() => props.onSetLogLevel("debug")}>set-level</button>
|
||||
<button onClick={() => props.onServerSettings("A")}>open-settings</button>
|
||||
<span data-testid="pinned-history">
|
||||
{Array.from(props.pinnedHistoryIds ?? []).join(",")}
|
||||
{Array.from(props.pinnedProtocolIds ?? []).join(",")}
|
||||
</span>
|
||||
<button onClick={() => props.onTogglePinHistory("hist-1")}>
|
||||
<button onClick={() => props.onTogglePinProtocol("hist-1")}>
|
||||
toggle-pin
|
||||
</button>
|
||||
<button onClick={() => props.onReplayHistory("hist-1")}>
|
||||
<button onClick={() => props.onReplayProtocol("hist-1")}>
|
||||
replay-history
|
||||
</button>
|
||||
<button onClick={() => props.onClearHistory()}>clear-history</button>
|
||||
<button onClick={() => props.onClearProtocol()}>clear-history</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ import {
|
|||
EMPTY_APPS_UI,
|
||||
EMPTY_TASKS_UI,
|
||||
EMPTY_LOGS_UI,
|
||||
EMPTY_HISTORY_UI,
|
||||
EMPTY_PROTOCOL_UI,
|
||||
EMPTY_NETWORK_UI,
|
||||
EMPTY_CONSOLE_UI,
|
||||
} from "./components/screens/screenUiState";
|
||||
|
|
@ -140,7 +140,7 @@ import { ConnectionInfoModal } from "./components/groups/ConnectionInfoModal/Con
|
|||
import { oauthDetailsFromConnectionState } from "./components/groups/ConnectionInfoContent/oauthDetailsFromConnectionState";
|
||||
import { OutputValidationModal } from "./components/groups/OutputValidationModal/OutputValidationModal";
|
||||
import { UrlElicitationErrorModal } from "./components/groups/UrlElicitationErrorModal/UrlElicitationErrorModal";
|
||||
import { isReplayableHistoryMethod } from "./components/groups/historyUtils.js";
|
||||
import { isReplayableProtocolMethod } from "./components/groups/protocolUtils.js";
|
||||
import type { OAuthDetails } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent";
|
||||
import { ServerRemoveConfirmModal } from "./components/groups/ServerRemoveConfirmModal/ServerRemoveConfirmModal";
|
||||
import { StepUpAuthModal } from "./components/groups/StepUpAuthModal/StepUpAuthModal";
|
||||
|
|
@ -267,21 +267,21 @@ function messagesToLogEntries(messages: MessageEntry[]): LogEntryData[] {
|
|||
return out;
|
||||
}
|
||||
|
||||
// Re-issue the original request behind a History entry. The call goes through
|
||||
// Re-issue the original request behind a Protocol entry. The call goes through
|
||||
// InspectorClient → tracked transport → message log, so the replayed
|
||||
// request+response surface as a fresh History entry (history-local) — it
|
||||
// request+response surface as a fresh Protocol entry (protocol-local) — it
|
||||
// intentionally does NOT touch the Tools/Prompts/Resources panels. Returns a
|
||||
// human-readable reason when the entry can't be replayed (unsupported method,
|
||||
// or a tool that's no longer present), or null on a dispatched replay.
|
||||
async function replayHistoryRequest(
|
||||
async function replayProtocolRequest(
|
||||
client: InspectorClient,
|
||||
method: string,
|
||||
params: Record<string, unknown> | undefined,
|
||||
tools: Tool[],
|
||||
): Promise<string | null> {
|
||||
// Gate on the shared replayable-method set (the same one HistoryEntry uses to
|
||||
// Gate on the shared replayable-method set (the same one ProtocolEntry uses to
|
||||
// show/hide the Replay button) so the two can't drift.
|
||||
if (!isReplayableHistoryMethod(method)) {
|
||||
if (!isReplayableProtocolMethod(method)) {
|
||||
return `Replay isn't supported for "${method}".`;
|
||||
}
|
||||
// Pagination cursor carried by the */list requests; replaying the same page
|
||||
|
|
@ -755,11 +755,11 @@ function App() {
|
|||
const [appsUi, setAppsUi] = useState(EMPTY_APPS_UI);
|
||||
const [tasksUi, setTasksUi] = useState(EMPTY_TASKS_UI);
|
||||
const [logsUi, setLogsUi] = useState(EMPTY_LOGS_UI);
|
||||
const [historyUi, setHistoryUi] = useState(EMPTY_HISTORY_UI);
|
||||
// History entries the user pinned (by entry id). Session-scoped — the ids
|
||||
const [protocolUi, setProtocolUi] = useState(EMPTY_PROTOCOL_UI);
|
||||
// Protocol entries the user pinned (by entry id). Session-scoped — the ids
|
||||
// reference message-log entries, which clear on disconnect, so this resets
|
||||
// with the rest of the per-screen state.
|
||||
const [pinnedHistoryIds, setPinnedHistoryIds] = useState<Set<string>>(
|
||||
const [pinnedProtocolIds, setPinnedProtocolIds] = useState<Set<string>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const [networkUi, setNetworkUi] = useState(EMPTY_NETWORK_UI);
|
||||
|
|
@ -1014,8 +1014,8 @@ function App() {
|
|||
setAppsUi(EMPTY_APPS_UI);
|
||||
setTasksUi(EMPTY_TASKS_UI);
|
||||
setLogsUi(EMPTY_LOGS_UI);
|
||||
setHistoryUi(EMPTY_HISTORY_UI);
|
||||
setPinnedHistoryIds(new Set());
|
||||
setProtocolUi(EMPTY_PROTOCOL_UI);
|
||||
setPinnedProtocolIds(new Set());
|
||||
setNetworkUi(EMPTY_NETWORK_UI);
|
||||
// Only the search filter resets here; the stderr entries themselves live in
|
||||
// StderrLogState, which deliberately survives connect/disconnect so a failed
|
||||
|
|
@ -1059,7 +1059,7 @@ function App() {
|
|||
// Surface incoming `notifications/progress` as toasts so the user can watch a
|
||||
// long-running tool's progress while staying on the tool view — the v2
|
||||
// replacement for v1's always-visible "Server Notifications" shelf (#1414).
|
||||
// The full notification history still lives in the History tab; these toasts
|
||||
// The full notification history still lives in the Protocol tab; these toasts
|
||||
// are the at-a-glance, in-context signal. Toasts are keyed by progress stream
|
||||
// (see `progressToastId`) and replaced per tick so a chatty server updates one
|
||||
// toast rather than stacking one per tick.
|
||||
|
|
@ -1171,7 +1171,7 @@ function App() {
|
|||
// with `requestorTaskUpdated` so we skip it to avoid double-firing. One toast per
|
||||
// taskId, replaced per tick, dismissed on terminal status (which also prunes the
|
||||
// task's progress entry) and on client teardown. The full status history still
|
||||
// lives in the History view.
|
||||
// lives in the Protocol view.
|
||||
useEffect(() => {
|
||||
if (!inspectorClient) return;
|
||||
const liveToastIds = taskToastIdsRef.current;
|
||||
|
|
@ -1560,7 +1560,7 @@ function App() {
|
|||
appsUi,
|
||||
tasksUi,
|
||||
logsUi,
|
||||
historyUi,
|
||||
protocolUi,
|
||||
networkUi,
|
||||
}),
|
||||
...(remoteSessionId && { remoteSessionId }),
|
||||
|
|
@ -1578,7 +1578,7 @@ function App() {
|
|||
appsUi,
|
||||
tasksUi,
|
||||
logsUi,
|
||||
historyUi,
|
||||
protocolUi,
|
||||
networkUi,
|
||||
onBeforeOAuthRedirect,
|
||||
],
|
||||
|
|
@ -2189,7 +2189,7 @@ function App() {
|
|||
setAppsUi,
|
||||
setTasksUi,
|
||||
setLogsUi,
|
||||
setHistoryUi,
|
||||
setProtocolUi,
|
||||
setNetworkUi,
|
||||
setActiveTab,
|
||||
clearToolCallState: () => setToolCallState(undefined),
|
||||
|
|
@ -2987,7 +2987,7 @@ function App() {
|
|||
const onClearLogs = useCallback(() => {
|
||||
if (!messageLogState) return;
|
||||
// Clear only the log notifications, not the entire request/response
|
||||
// history (which the History screen renders from the same source).
|
||||
// history (which the Protocol screen renders from the same source).
|
||||
messageLogState.clearMessages(
|
||||
(m) =>
|
||||
m.direction === "notification" &&
|
||||
|
|
@ -2999,10 +2999,10 @@ function App() {
|
|||
// Panel-level Clear clears the (unpinned) history and keeps pinned entries —
|
||||
// pinning is the way to protect an entry from Clear. This matches the button's
|
||||
// `disabled={unpinnedEntries.length === 0}` gating and the per-section model,
|
||||
// and leaves pinnedHistoryIds valid (the pins it references still exist).
|
||||
const onClearHistory = useCallback(() => {
|
||||
messageLogState?.clearMessages((m) => !pinnedHistoryIds.has(m.id));
|
||||
}, [messageLogState, pinnedHistoryIds]);
|
||||
// and leaves pinnedProtocolIds valid (the pins it references still exist).
|
||||
const onClearProtocol = useCallback(() => {
|
||||
messageLogState?.clearMessages((m) => !pinnedProtocolIds.has(m.id));
|
||||
}, [messageLogState, pinnedProtocolIds]);
|
||||
|
||||
const onClearNetwork = useCallback(() => {
|
||||
fetchRequestLogState?.clearFetchRequests();
|
||||
|
|
@ -3016,50 +3016,50 @@ function App() {
|
|||
);
|
||||
}, [fetchRequests, activeServerId]);
|
||||
|
||||
const onExportHistory = useCallback(() => {
|
||||
const onExportProtocol = useCallback(() => {
|
||||
if (messages.length === 0) return;
|
||||
downloadJsonFile(
|
||||
buildExportFilename("history", activeServerId),
|
||||
buildExportFilename("protocol", activeServerId),
|
||||
JSON.stringify(messages, null, 2),
|
||||
);
|
||||
}, [messages, activeServerId]);
|
||||
|
||||
// Clear just one section: remove its entries from the log by pin membership.
|
||||
// Clearing the pinned section also drops the (now-stale) pinned id set.
|
||||
const onClearHistorySection = useCallback(
|
||||
const onClearProtocolSection = useCallback(
|
||||
(section: "pinned" | "history") => {
|
||||
const isPinned = section === "pinned";
|
||||
messageLogState?.clearMessages((m) =>
|
||||
isPinned ? pinnedHistoryIds.has(m.id) : !pinnedHistoryIds.has(m.id),
|
||||
isPinned ? pinnedProtocolIds.has(m.id) : !pinnedProtocolIds.has(m.id),
|
||||
);
|
||||
if (isPinned) setPinnedHistoryIds(new Set());
|
||||
if (isPinned) setPinnedProtocolIds(new Set());
|
||||
},
|
||||
[messageLogState, pinnedHistoryIds],
|
||||
[messageLogState, pinnedProtocolIds],
|
||||
);
|
||||
|
||||
// Export just one section's entries (by pin membership) to a JSON file.
|
||||
const onExportHistorySection = useCallback(
|
||||
const onExportProtocolSection = useCallback(
|
||||
(section: "pinned" | "history") => {
|
||||
const isPinned = section === "pinned";
|
||||
const subset = messages.filter((m) =>
|
||||
isPinned ? pinnedHistoryIds.has(m.id) : !pinnedHistoryIds.has(m.id),
|
||||
isPinned ? pinnedProtocolIds.has(m.id) : !pinnedProtocolIds.has(m.id),
|
||||
);
|
||||
if (subset.length === 0) return;
|
||||
downloadJsonFile(
|
||||
buildExportFilename(
|
||||
isPinned ? "history-pinned" : "history-unpinned",
|
||||
isPinned ? "protocol-pinned" : "protocol-unpinned",
|
||||
activeServerId,
|
||||
),
|
||||
JSON.stringify(subset, null, 2),
|
||||
);
|
||||
},
|
||||
[messages, pinnedHistoryIds, activeServerId],
|
||||
[messages, pinnedProtocolIds, activeServerId],
|
||||
);
|
||||
|
||||
// Pin/unpin a history entry by id. HistoryListPanel sorts pinned entries to
|
||||
// Pin/unpin a Protocol entry by id. ProtocolListPanel sorts pinned entries to
|
||||
// the top; the set is session-scoped (see resetSessionScopedUiState).
|
||||
const onTogglePinHistory = useCallback((id: string) => {
|
||||
setPinnedHistoryIds((prev) => {
|
||||
const onTogglePinProtocol = useCallback((id: string) => {
|
||||
setPinnedProtocolIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
|
|
@ -3070,12 +3070,12 @@ function App() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
// Replay a history entry: re-issue its original request so the fresh
|
||||
// request+response appear as a new History entry (history-local). A reason
|
||||
// Replay a Protocol entry: re-issue its original request so the fresh
|
||||
// request+response appear as a new Protocol entry (protocol-local). A reason
|
||||
// string (unsupported method / missing tool) surfaces as a toast; a genuine
|
||||
// call error already shows up as the replayed entry's Error status, so only a
|
||||
// pre-flight failure (nothing logged) needs the fallback toast.
|
||||
const onReplayHistory = useCallback(
|
||||
const onReplayProtocol = useCallback(
|
||||
(id: string) => {
|
||||
if (!inspectorClient) return;
|
||||
const entry = messages.find((m) => m.id === id);
|
||||
|
|
@ -3085,7 +3085,7 @@ function App() {
|
|||
"params" in entry.message
|
||||
? (entry.message.params as Record<string, unknown> | undefined)
|
||||
: undefined;
|
||||
void replayHistoryRequest(inspectorClient, method, params, tools)
|
||||
void replayProtocolRequest(inspectorClient, method, params, tools)
|
||||
.then((reason) => {
|
||||
if (reason) {
|
||||
notifications.show({
|
||||
|
|
@ -3687,7 +3687,7 @@ function App() {
|
|||
logs={logs}
|
||||
tasks={tasks}
|
||||
progressByTaskId={progressByTaskId}
|
||||
history={messages}
|
||||
protocol={messages}
|
||||
network={fetchRequests}
|
||||
stderrLogs={stderrLogs}
|
||||
toolCallState={toolCallState}
|
||||
|
|
@ -3699,7 +3699,7 @@ function App() {
|
|||
appsUi={appsUi}
|
||||
tasksUi={tasksUi}
|
||||
logsUi={logsUi}
|
||||
historyUi={historyUi}
|
||||
protocolUi={protocolUi}
|
||||
networkUi={networkUi}
|
||||
consoleUi={consoleUi}
|
||||
activeTab={activeTab}
|
||||
|
|
@ -3792,14 +3792,14 @@ function App() {
|
|||
onLogsUiChange={setLogsUi}
|
||||
onClearLogs={onClearLogs}
|
||||
onExportLogs={onExportLogs}
|
||||
onHistoryUiChange={setHistoryUi}
|
||||
onClearHistory={onClearHistory}
|
||||
onExportHistory={onExportHistory}
|
||||
onClearHistorySection={onClearHistorySection}
|
||||
onExportHistorySection={onExportHistorySection}
|
||||
onReplayHistory={onReplayHistory}
|
||||
onTogglePinHistory={onTogglePinHistory}
|
||||
pinnedHistoryIds={pinnedHistoryIds}
|
||||
onProtocolUiChange={setProtocolUi}
|
||||
onClearProtocol={onClearProtocol}
|
||||
onExportProtocol={onExportProtocol}
|
||||
onClearProtocolSection={onClearProtocolSection}
|
||||
onExportProtocolSection={onExportProtocolSection}
|
||||
onReplayProtocol={onReplayProtocol}
|
||||
onTogglePinProtocol={onTogglePinProtocol}
|
||||
pinnedProtocolIds={pinnedProtocolIds}
|
||||
onNetworkUiChange={setNetworkUi}
|
||||
onClearNetwork={onClearNetwork}
|
||||
onExportNetwork={onExportNetwork}
|
||||
|
|
|
|||
|
|
@ -31,4 +31,27 @@ describe("EmbeddableScrollArea", () => {
|
|||
);
|
||||
expect(ref.current).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"constrains the content width when asked (embedded=%s)",
|
||||
(embedded) => {
|
||||
const ref = createRef<HTMLDivElement>();
|
||||
renderWithMantine(
|
||||
<EmbeddableScrollArea
|
||||
embedded={embedded}
|
||||
viewportRef={ref}
|
||||
constrainContentWidth
|
||||
>
|
||||
<div>constrained content</div>
|
||||
</EmbeddableScrollArea>,
|
||||
);
|
||||
expect(screen.getByText("constrained content")).toBeInTheDocument();
|
||||
// The Mantine ScrollArea `content` slot has its default
|
||||
// `min-width: max-content` relaxed to 0 so a long non-wrapping row
|
||||
// (e.g. a network URL) can't stretch the list past its viewport (#1623).
|
||||
const content = document.querySelector(".mantine-ScrollArea-content");
|
||||
expect(content).not.toBeNull();
|
||||
expect((content as HTMLElement).style.minWidth).toBe("0");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,10 +17,25 @@ export interface EmbeddableScrollAreaProps {
|
|||
embedded: boolean;
|
||||
viewportRef: Ref<HTMLDivElement>;
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Constrain the scrolled content to the viewport width instead of letting it
|
||||
* grow to its own `max-content`. Mantine's ScrollArea `content` slot defaults
|
||||
* to `min-width: max-content`, so a row with non-wrapping content (e.g. a long
|
||||
* network URL) stretches every card past the column and it bleeds out (#1623).
|
||||
* When true, the content can shrink to the viewport and each row must manage
|
||||
* its own overflow (the Network URL scrolls inside its own inner ScrollArea).
|
||||
* Left off for panels whose rows already wrap/truncate (Logs, Protocol), where
|
||||
* the default lets a long line scroll the list horizontally instead.
|
||||
*/
|
||||
constrainContentWidth?: boolean;
|
||||
}
|
||||
|
||||
// Relax the `content` slot's default `min-width: max-content` so the list can't
|
||||
// grow wider than its viewport; see `constrainContentWidth`.
|
||||
const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const;
|
||||
|
||||
/**
|
||||
* The scroll region shared by the Logs / History / Network stream panels, which
|
||||
* The scroll region shared by the Logs / Protocol / 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.
|
||||
*/
|
||||
|
|
@ -28,7 +43,9 @@ export function EmbeddableScrollArea({
|
|||
embedded,
|
||||
viewportRef,
|
||||
children,
|
||||
constrainContentWidth = false,
|
||||
}: EmbeddableScrollAreaProps) {
|
||||
const styles = constrainContentWidth ? CONSTRAIN_CONTENT_STYLES : undefined;
|
||||
if (embedded) {
|
||||
return (
|
||||
<Stack flex={1} mih={0} gap={0}>
|
||||
|
|
@ -37,6 +54,7 @@ export function EmbeddableScrollArea({
|
|||
mah="100%"
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
styles={styles}
|
||||
>
|
||||
{children}
|
||||
</ScrollArea.Autosize>
|
||||
|
|
@ -49,6 +67,7 @@ export function EmbeddableScrollArea({
|
|||
mah={FULLSIZE_MAH}
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
styles={styles}
|
||||
>
|
||||
{children}
|
||||
</ScrollArea.Autosize>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export interface ExpandToggleProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Icon toggle for a per-entry expand/collapse control (History, Network, and
|
||||
* Icon toggle for a per-entry expand/collapse control (Protocol, Network, and
|
||||
* Task cards). Uses the same expand/collapse-vertical icons as the list-level
|
||||
* ListToggle: collapsed shows the expand icon, expanded shows the collapse
|
||||
* icon. The aria-label stays "Expand"/"Collapse" so it reads the same as the
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const ToggleLabel = Text.withProps({
|
|||
});
|
||||
|
||||
/**
|
||||
* A single full-width filter toggle used by the Logging, History, and Network
|
||||
* A single full-width filter toggle used by the Logging, Protocol, and Network
|
||||
* controls. The `filterToggle` theme variant + `.filter-toggle` rules own the
|
||||
* styling: hover shows a thin border, the active (`aria-pressed`) state shows a
|
||||
* filled background. Keeping hover as a border (not a fill) means toggling a
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ export const Incoming: Story = {
|
|||
args: { direction: "incoming" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("client ← server")).toBeInTheDocument();
|
||||
expect(canvas.getByText("server → client")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ describe("MessageDirectionBadge", () => {
|
|||
expect(screen.getByText("client → server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders client ← server for incoming", () => {
|
||||
it("renders server → client for incoming", () => {
|
||||
renderWithMantine(<MessageDirectionBadge direction="incoming" />);
|
||||
expect(screen.getByText("client ← server")).toBeInTheDocument();
|
||||
expect(screen.getByText("server → client")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ export interface MessageDirectionBadgeProps {
|
|||
/**
|
||||
* Direction of travel for the entry: "outgoing" = the inspector sent it to
|
||||
* the server (client → server); "incoming" = the server sent it to the
|
||||
* inspector (client ← server).
|
||||
* inspector (server → client).
|
||||
*/
|
||||
direction: "outgoing" | "incoming";
|
||||
}
|
||||
|
||||
const LABEL: Record<MessageDirectionBadgeProps["direction"], string> = {
|
||||
outgoing: "client → server",
|
||||
incoming: "client ← server",
|
||||
incoming: "server → client",
|
||||
};
|
||||
|
||||
/**
|
||||
* Dual-state badge showing which way a History/Network entry traveled. Outgoing
|
||||
* (client → server) is green; incoming (client ← server) is purple — not yellow,
|
||||
* Dual-state badge showing which way a Protocol/Network entry traveled. Outgoing
|
||||
* (client → server) is green; incoming (server → client) is purple — not yellow,
|
||||
* which (paired with green) reads as caution/ok status rather than direction.
|
||||
* Shared by `HistoryEntry` and `NetworkEntry`.
|
||||
* Shared by `ProtocolEntry` and `NetworkEntry`.
|
||||
*/
|
||||
export function MessageDirectionBadge({
|
||||
direction,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface PinColumnButtonProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Toolbar button that pins the owning monitor screen (Logs / History / Network)
|
||||
* Toolbar button that pins the owning monitor screen (Logs / Protocol / Network)
|
||||
* into the resizable column on the right of the InspectorView (#1616). Distinct
|
||||
* from `PinToggle` (which pins individual history entries) — this one opens a
|
||||
* side column, so it uses a right-sidebar glyph and an "as column" label. Styled
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export interface ReplayButtonProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Icon form of the "Replay" action, used in the compact (column) HistoryEntry
|
||||
* Icon form of the "Replay" action, used in the compact (column) ProtocolEntry
|
||||
* layout where the text button is replaced by a replay icon sitting next to the
|
||||
* pin toggle (#1616). Matches PinToggle's subtle gray icon-button styling.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe("MessageDirectionFilter", () => {
|
|||
screen.getByRole("button", { name: "client → server" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "client ← server" }),
|
||||
screen.getByRole("button", { name: "server → client" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ describe("MessageDirectionFilter", () => {
|
|||
onToggleDirection={onToggleDirection}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "client ← server" }));
|
||||
await user.click(screen.getByRole("button", { name: "server → client" }));
|
||||
expect(onToggleDirection).toHaveBeenCalledWith("server", false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ const SubtleButton = Button.withProps({
|
|||
|
||||
// The two message directions, in display order. Label + color mirror the
|
||||
// MessageDirectionBadge: outgoing (client → server) is green, incoming
|
||||
// (client ← server) is violet.
|
||||
// (server → client) is violet.
|
||||
const MESSAGE_DIRECTIONS: {
|
||||
origin: MessageOrigin;
|
||||
label: string;
|
||||
color: string;
|
||||
}[] = [
|
||||
{ origin: "client", label: "client → server", color: "green" },
|
||||
{ origin: "server", label: "client ← server", color: "violet" },
|
||||
{ origin: "server", label: "server → client", color: "violet" },
|
||||
];
|
||||
|
||||
export interface MessageDirectionFilterProps {
|
||||
|
|
@ -27,8 +27,8 @@ export interface MessageDirectionFilterProps {
|
|||
|
||||
/**
|
||||
* "Filter by Message Direction" section — a Select/Deselect All control plus a
|
||||
* FilterToggleButton per direction (client → server / client ← server). Used by
|
||||
* the History controls. (Kept as its own component so the section is testable in
|
||||
* FilterToggleButton per direction (client → server / server → client). Used by
|
||||
* the Protocol controls. (Kept as its own component so the section is testable in
|
||||
* isolation and reusable if another screen ever needs a direction filter.)
|
||||
*/
|
||||
export function MessageDirectionFilter({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const meta: Meta<typeof MonitoringControls> = {
|
|||
title: "Groups/MonitoringControls",
|
||||
component: MonitoringControls,
|
||||
args: {
|
||||
tabs: ["Logs", "History", "Network"],
|
||||
tabs: ["Logs", "Protocol", "Network"],
|
||||
value: "Logs",
|
||||
onChange: fn(),
|
||||
searchValue: "",
|
||||
|
|
@ -29,7 +29,7 @@ export const AllTabs: Story = {
|
|||
};
|
||||
|
||||
export const StdioServer: Story = {
|
||||
args: { tabs: ["Logs", "History"], value: "History" },
|
||||
args: { tabs: ["Logs", "Protocol"], value: "Protocol" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.queryByRole("radio", { name: "Network" })).toBeNull();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type MonitoringControlsProps,
|
||||
} from "./MonitoringControls";
|
||||
|
||||
const TABS = ["Logs", "History", "Network"];
|
||||
const TABS = ["Logs", "Protocol", "Network"];
|
||||
|
||||
function renderControls(overrides: Partial<MonitoringControlsProps> = {}) {
|
||||
const props: MonitoringControlsProps = {
|
||||
|
|
@ -31,7 +31,7 @@ describe("MonitoringControls", () => {
|
|||
});
|
||||
|
||||
it("renders only the tabs it is given", () => {
|
||||
renderControls({ tabs: ["Logs", "History"] });
|
||||
renderControls({ tabs: ["Logs", "Protocol"] });
|
||||
expect(screen.getAllByRole("radio")).toHaveLength(2);
|
||||
expect(screen.queryByRole("radio", { name: "Network" })).toBeNull();
|
||||
});
|
||||
|
|
@ -53,8 +53,8 @@ describe("MonitoringControls", () => {
|
|||
});
|
||||
|
||||
it("marks the active tab as selected", () => {
|
||||
renderControls({ value: "History" });
|
||||
expect(screen.getByRole("radio", { name: "History" })).toBeChecked();
|
||||
renderControls({ value: "Protocol" });
|
||||
expect(screen.getByRole("radio", { name: "Protocol" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("calls onChange when a different tab is chosen", async () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { TbLayoutSidebarRightCollapse } from "react-icons/tb";
|
|||
import { ClearButton } from "../../elements/ClearButton/ClearButton";
|
||||
|
||||
export interface MonitoringControlsProps {
|
||||
/** The monitor screens currently available to pin (e.g. Logs/History/Network). */
|
||||
/** The monitor screens currently available to pin (e.g. Logs/Protocol/Network). */
|
||||
tabs: string[];
|
||||
/** The active monitor tab shown in the column below. */
|
||||
value: string;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const demoScreens = {
|
|||
<Text>Log stream</Text>
|
||||
</Box>
|
||||
),
|
||||
History: (
|
||||
Protocol: (
|
||||
<Box p="md">
|
||||
<Text>Request history</Text>
|
||||
</Box>
|
||||
|
|
@ -32,7 +32,7 @@ const meta: Meta<typeof MonitoringScreen> = {
|
|||
),
|
||||
],
|
||||
args: {
|
||||
tabs: ["Logs", "History", "Network"],
|
||||
tabs: ["Logs", "Protocol", "Network"],
|
||||
value: "Logs",
|
||||
onChange: fn(),
|
||||
searchValue: "",
|
||||
|
|
@ -52,8 +52,8 @@ export const Logs: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const History: Story = {
|
||||
args: { value: "History" },
|
||||
export const Protocol: Story = {
|
||||
args: { value: "Protocol" },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByText("Request history")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import {
|
|||
type MonitoringScreenProps,
|
||||
} from "./MonitoringScreen";
|
||||
|
||||
const TABS = ["Logs", "History", "Network"];
|
||||
const TABS = ["Logs", "Protocol", "Network"];
|
||||
|
||||
function screens() {
|
||||
return {
|
||||
Logs: <div>logs-body</div>,
|
||||
History: <div>history-body</div>,
|
||||
Protocol: <div>history-body</div>,
|
||||
Network: <div>network-body</div>,
|
||||
};
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ function renderScreen(overrides: Partial<MonitoringScreenProps> = {}) {
|
|||
|
||||
describe("MonitoringScreen", () => {
|
||||
it("renders the screen for the active tab", () => {
|
||||
renderScreen({ value: "History" });
|
||||
renderScreen({ value: "Protocol" });
|
||||
expect(screen.getByText("history-body")).toBeInTheDocument();
|
||||
expect(screen.queryByText("logs-body")).toBeNull();
|
||||
expect(screen.queryByText("network-body")).toBeNull();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Divider, Stack } from "@mantine/core";
|
|||
import { MonitoringControls } from "../MonitoringControls/MonitoringControls";
|
||||
|
||||
export interface MonitoringScreenProps {
|
||||
/** Available monitor tabs (Logs/History/Network, filtered by capability). */
|
||||
/** Available monitor tabs (Logs/Protocol/Network, filtered by capability). */
|
||||
tabs: string[];
|
||||
/** Active monitor tab; keys into `screens` to pick what renders below. */
|
||||
value: string;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const UrlScroll = Text.withProps({
|
|||
variant: "nowrap",
|
||||
});
|
||||
|
||||
// Left / right clusters for a compact header line (mirrors HistoryEntry).
|
||||
// Left / right clusters for a compact header line (mirrors ProtocolEntry).
|
||||
const HeaderCluster = Group.withProps({
|
||||
gap: "sm",
|
||||
wrap: "nowrap",
|
||||
|
|
@ -240,7 +240,7 @@ export function NetworkEntry({
|
|||
// The list-level Expand/Collapse toggle is authoritative: each time the
|
||||
// parent changes `isListExpanded`, every entry snaps to that state and
|
||||
// any per-entry override is intentionally discarded. Mirrors
|
||||
// HistoryEntry; do not change without aligning both. This re-runs on
|
||||
// ProtocolEntry; do not change without aligning both. This re-runs on
|
||||
// re-render when `isListExpanded` keeps its reference, but the setter
|
||||
// is a no-op when the next value equals the current one.
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue