From 77c28fe2078a770ffea2ddfddae6158652a097f9 Mon Sep 17 00:00:00 2001 From: Nishant <261417038+webtools-dotcom@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:36:09 +0530 Subject: [PATCH] fix(ui): network and mocks panel polish - Network: the URL column leads with path + query and dims the host after it, so truncation cuts the repeated host instead of the part that tells requests apart. - Network: Method/Status/Type columns sized for their header labels; "STATUS" no longer runs into "TYPE". - Network: JSON response bodies are pretty-printed, with room for 4 KB instead of 500 characters. - Mocks: the add form folds behind a "New mock" button once rules exist, so the rules stay at the top of the panel. --- src/components/panels/MocksPanel.tsx | 34 ++++++++++++++++++++++---- src/components/panels/NetworkPanel.tsx | 20 +++++++++------ src/lib/format.test.ts | 13 +++++++++- src/lib/format.ts | 15 ++++++++++++ src/lib/url.test.ts | 14 +++++++++++ src/lib/url.ts | 14 +++++++++++ 6 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/components/panels/MocksPanel.tsx b/src/components/panels/MocksPanel.tsx index ab2385e..8b01584 100644 --- a/src/components/panels/MocksPanel.tsx +++ b/src/components/panels/MocksPanel.tsx @@ -250,6 +250,10 @@ export function MocksPanel() { const updateRule = useMocksStore((s) => s.updateRule); const removeRule = useMocksStore((s) => s.removeRule); const active = rules.filter((r) => r.enabled).length; + // The add form is the whole panel when there are no rules; once there are, + // it folds behind a button so the rules themselves stay at the top. + const [adding, setAdding] = useState(false); + const showForm = adding || rules.length === 0; return (
+{bodyPreview(entry.body)}{entry.bodyTruncated && ( @@ -283,6 +287,7 @@ function EntryRow({ e.stopPropagation(); copyToClipboard(entryToCurl(entry, true)); }; + const { path, host } = splitUrl(entry.url); return (- {entry.url} + {path} + {host && {host}} {hostOf(entry.referrer, entry.referrer || "—")} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 36025da..bed3920 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { formatBytes } from "./format"; +import { formatBytes, prettyBody } from "./format"; describe("formatBytes", () => { it("formats bytes under 1KB as B", () => { @@ -21,3 +21,14 @@ describe("formatBytes", () => { expect(formatBytes(-1)).toBe("—"); }); }); + +describe("prettyBody", () => { + it("re-indents JSON", () => { + expect(prettyBody('{"a":1,"b":[2]}')).toBe('{\n "a": 1,\n "b": [\n 2\n ]\n}'); + }); + + it("leaves non-JSON and broken JSON alone", () => { + expect(prettyBody("")).toBe(""); + expect(prettyBody('{"a":')).toBe('{"a":'); + }); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts index 5a868bb..ffb853f 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -13,3 +13,18 @@ export function statusColor(code: number): string { if (code >= 500) return "text-status-5xx"; return "text-[var(--color-text-disabled)]"; } + +/** + * A response body as the network log shows it: JSON re-indented when it + * parses, anything else untouched. Keyed off the body itself rather than the + * Content-Type header, which APIs get wrong often enough (text/plain JSON). + */ +export function prettyBody(body: string): string { + const t = body.trimStart(); + if (!t.startsWith("{") && !t.startsWith("[")) return body; + try { + return JSON.stringify(JSON.parse(body), null, 2); + } catch { + return body; + } +} diff --git a/src/lib/url.test.ts b/src/lib/url.test.ts index 40d3342..a1bf7f9 100644 --- a/src/lib/url.test.ts +++ b/src/lib/url.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_SEARCH_ENGINE, SEARCH_ENGINES, hostOf, + splitUrl, originOf, resolveInput, searchUrl, @@ -127,3 +128,16 @@ describe("titleFromUrl", () => { expect(titleFromUrl("not a url at all")).toBe("not a url at all"); }); }); + +describe("splitUrl", () => { + it("puts the path and query first, host second", () => { + expect(splitUrl("http://localhost:8000/api/orders?page=2")).toEqual({ + path: "/api/orders?page=2", + host: "localhost:8000", + }); + }); + + it("returns an unparseable URL whole", () => { + expect(splitUrl("not a url")).toEqual({ path: "not a url", host: "" }); + }); +}); diff --git a/src/lib/url.ts b/src/lib/url.ts index e816eb4..4cc41dc 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -7,6 +7,20 @@ export function originOf(url: string, fallback = ""): string { } } +/** + * Splits a URL for the network log: path + query first (what tells requests + * apart), host second (repeated on most rows, so it is the part to truncate). + * An unparseable URL comes back whole as `path`. + */ +export function splitUrl(url: string): { path: string; host: string } { + try { + const u = new URL(url); + return { path: u.pathname + u.search, host: u.host }; + } catch { + return { path: url, host: "" }; + } +} + /** Hostname only (no port), or `fallback` if `url` doesn't parse. */ export function hostOf(url: string, fallback = ""): string { try {