diff --git a/apps/yaak-proxy/components/FlowListHeader.tsx b/apps/yaak-proxy/components/FlowListHeader.tsx index 7b4af4aa7..4b5c89819 100644 --- a/apps/yaak-proxy/components/FlowListHeader.tsx +++ b/apps/yaak-proxy/components/FlowListHeader.tsx @@ -46,6 +46,7 @@ import { resetWidth, resetWidths, resizeColumn, + resizeDistance, toggleColumn, unavailableColumns, } from "../lib/columns"; @@ -262,9 +263,12 @@ export const FlowListHeader = memo(function FlowListHeader({ sort={sort} onSort={onSort} onResizeStart={() => { - startWidth.current = shownWidth(gridRef.current, c); + startWidth.current = c.current; + }} + onResizeMove={(dx) => { + const by = resizeDistance(c, dx, rootFontSize()); + setPrefs((p) => resizeColumn(p, c.id, startWidth.current + by)); }} - onResizeMove={(dx) => setPrefs((p) => resizeColumn(p, c.id, startWidth.current + dx))} onResetWidth={() => setPrefs((p) => resetWidth(p, c.id))} /> ))} @@ -279,18 +283,9 @@ export const FlowListHeader = memo(function FlowListHeader({ ); }); -/** - * A column's content width as laid out, so a resize starts from what shows: a numeric column's - * can be wider than its width in pixels, at its default or its least width, at a larger - * interface size - */ -function shownWidth(grid: HTMLElement | null, c: Column): number { - if (c.fitChars == null) return c.current; - const cell = grid?.querySelector(`[data-column="${c.id}"]`); - if (cell == null) return c.current; - const style = getComputedStyle(cell); - const padding = Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight); - return cell.getBoundingClientRect().width - padding; +/** The interface font size now, which numeric columns' widths grow with */ +function rootFontSize(): number { + return Number.parseFloat(getComputedStyle(document.documentElement).fontSize); } interface HeaderCellProps { diff --git a/apps/yaak-proxy/lib/columns.test.ts b/apps/yaak-proxy/lib/columns.test.ts index a49e22401..b5ab8b0fb 100644 --- a/apps/yaak-proxy/lib/columns.test.ts +++ b/apps/yaak-proxy/lib/columns.test.ts @@ -26,6 +26,7 @@ import { hideOtherColumns, moveColumn, moveColumnBy, + NUMERIC_FIT_CHARS, nextSort, operationOf, parseColumnPrefs, @@ -34,6 +35,7 @@ import { resetWidth, resetWidths, resizeColumn, + resizeDistance, showOperationFor, shownColumns, sortFlows, @@ -116,19 +118,16 @@ describe("shownColumns", () => { prefs = resizeColumn(prefs, "path", 500); const columns = shownColumns(prefs); const pad = (w: number) => `calc(${w}px + 0.75rem)`; - // Size and Time at their defaults, which grow with the interface size, and never narrower - // than their widest values - const size = "calc(max(4.8rem, 4rem) + 0.75rem)"; - const time = `calc(max(${64 / 15}rem, 4rem) + 0.75rem)`; + // Size and Time, in pixels at the default interface size of 15, grow with it + const size = "calc(4.8rem + 0.75rem)"; + const time = `calc(${64 / 15}rem + 0.75rem)`; expect(gridTemplate(columns)).toBe( ["24px", ...[46, 20, 50, 240, 500, 120, 46, 50].map(pad), size, time].join(" "), ); - expect(gridWidth(columns)).toBe( - `calc(1096px + max(4.8rem, 4rem) + max(${64 / 15}rem, 4rem) + 10 * 0.75rem)`, - ); - // Resized, a numeric column keeps the pixels it was given - const resized = shownColumns(resizeColumn(prefs, "size", 90)); - expect(gridTemplate(resized)).toContain("calc(max(90px, 4rem) + 0.75rem)"); + expect(gridWidth(columns)).toBe(`calc(1096px + 4.8rem + ${64 / 15}rem + 10 * 0.75rem)`); + // Resized, too, and at its least width it holds its widest value: eight characters of 0.5rem + const resized = shownColumns(resizeColumn(prefs, "size", 10)); + expect(gridTemplate(resized)).toContain(" calc(4rem + 0.75rem)"); }); }); @@ -148,9 +147,10 @@ describe("numeric column widths", () => { for (const id of numeric) { const def = columnDef(id); const chars = id === "size" || id === "requestSize" ? sizeChars : timeChars; - expect(def.fitChars).toBeGreaterThanOrEqual(chars); + expect(NUMERIC_FIT_CHARS).toBeGreaterThanOrEqual(chars); + expect(def.scales).toBe(true); // In pixels at the default interface size, as the row's font sets them - const fit = (def.fitChars ?? 0) * CHAR_REM * BASE_FONT_SIZE; + const fit = NUMERIC_FIT_CHARS * CHAR_REM * BASE_FONT_SIZE; expect(def.minWidth).toBeGreaterThanOrEqual(fit); expect(def.width).toBeGreaterThanOrEqual(fit); } @@ -168,6 +168,14 @@ describe("numeric column widths", () => { for (const id of numeric) expect(columnDef(id).width).toBeGreaterThanOrEqual(header[id]); }); + test("a resize moves a numeric column's edge with the pointer at any interface size", () => { + const size = columnDef("size"); + // At 30, twice the default, a 20px drag is 10 of the pixels its width is kept in + expect(resizeDistance(size, 20, 30)).toBe(10); + expect(resizeDistance(size, -12, 12)).toBe(-15); + expect(resizeDistance(columnDef("host"), 20, 30)).toBe(20); + }); + test("a stored width narrower than the least is raised, and a wider one kept", () => { const parsed = parseColumnPrefs({ widths: { size: 44, time: 50, requestSize: 120, host: 20 } }); expect(parsed.widths).toEqual({ size: 60, time: 60, requestSize: 120, host: 60 }); @@ -175,7 +183,7 @@ describe("numeric column widths", () => { expect(resizeColumn(defaultColumnPrefs, "latency", 10).widths).toEqual({ latency: 60 }); // Reset, the defaults are back const reset = shownColumns(resetWidths(parsed)).find((c) => c.id === "size"); - expect(reset).toMatchObject({ current: 72, resized: false }); + expect(reset?.current).toBe(72); expect(resetWidth(parsed, "time").widths).toEqual({ size: 60, requestSize: 120, host: 60 }); }); }); diff --git a/apps/yaak-proxy/lib/columns.ts b/apps/yaak-proxy/lib/columns.ts index 8f1f50f6b..6b6678d48 100644 --- a/apps/yaak-proxy/lib/columns.ts +++ b/apps/yaak-proxy/lib/columns.ts @@ -24,10 +24,10 @@ export interface ColumnDef { /** The header or query parameter a custom column shows: `header.x-request-id` */ path?: string; /** - * A numeric column's widest value, in characters of the row's monospace font: it is never - * narrower than that at any interface size, and its default width grows with the interface size + * A numeric column's: its width, resized or not, grows with the interface size, as its values + * do, so the least width always holds the widest value */ - fitChars?: number; + scales?: boolean; } const builtin = ( @@ -37,8 +37,8 @@ const builtin = ( minWidth: number, align: "left" | "right", defaultVisible: boolean, - fitChars?: number, -): ColumnDef => ({ id, label, width, minWidth, align, defaultVisible, fitChars }); + scales?: boolean, +): ColumnDef => ({ id, label, width, minWidth, align, defaultVisible, scales }); /** * The widest size and duration a numeric cell shows, in characters: `999.9 KB` and `999m 59s`, @@ -55,7 +55,10 @@ export const CHAR_REM = 0.5; /** The default interface font size, `DEFAULT_SETTINGS_VALUES.interfaceFontSize`, widths are at */ export const BASE_FONT_SIZE = 15; -/** A numeric column's least width, in pixels at the default interface size: its widest value */ +/** + * A numeric column's least width, in pixels at the default interface size, which grow with it: + * its widest value + */ const NUMERIC_MIN = NUMERIC_FIT_CHARS * CHAR_REM * BASE_FONT_SIZE; export const COLUMNS: ColumnDef[] = [ @@ -68,10 +71,10 @@ export const COLUMNS: ColumnDef[] = [ builtin("status", "Status", 46, 34, "left", true), builtin("type", "Type", 50, 34, "left", true), // Each default fits its label sorted, with the sort's arrow and place, and its widest value - builtin("requestSize", "Request Size", 96, NUMERIC_MIN, "right", false, NUMERIC_FIT_CHARS), - builtin("size", "Size", 72, NUMERIC_MIN, "right", true, NUMERIC_FIT_CHARS), - builtin("time", "Time", 64, NUMERIC_MIN, "right", true, NUMERIC_FIT_CHARS), - builtin("latency", "Latency", 68, NUMERIC_MIN, "right", false, NUMERIC_FIT_CHARS), + builtin("requestSize", "Request Size", 96, NUMERIC_MIN, "right", false, true), + builtin("size", "Size", 72, NUMERIC_MIN, "right", true, true), + builtin("time", "Time", 64, NUMERIC_MIN, "right", true, true), + builtin("latency", "Latency", 68, NUMERIC_MIN, "right", false, true), builtin("started", "Started", 92, 60, "left", false), builtin("remote", "Remote Address", 150, 60, "left", false), builtin("version", "HTTP Version", 70, 44, "left", false), @@ -197,8 +200,6 @@ export const defaultColumnPrefs: ColumnPrefs = { export interface Column extends ColumnDef { /** Its width now, in pixels of content */ current: number; - /** Whether the person set its width, rather than leaving it at its default */ - resized: boolean; } /** @@ -225,11 +226,7 @@ export function shownColumns(prefs: ColumnPrefs, unavailable: ColumnId[] = []): const all = allColumns(prefs).filter((c) => !unavailable.includes(c.id)); const shown = all.filter((c) => isVisible(prefs, c)); const columns = shown.length > 0 ? shown : all.filter((c) => c.id === "path"); - return columns.map((c) => ({ - ...c, - current: columnWidth(prefs, c.id), - resized: prefs.widths[c.id] != null, - })); + return columns.map((c) => ({ ...c, current: columnWidth(prefs, c.id) })); } /** Columns the platform has nothing for: App, where it cannot tell which app sent a flow */ @@ -242,14 +239,17 @@ export function columnWidth(prefs: ColumnPrefs, id: ColumnId): number { return Math.max(def.minWidth, Math.round(prefs.widths[id] ?? def.width)); } -/** - * A column's content width, as CSS. A numeric column's default grows with the interface size, - * and at any width it holds its widest value, which grows with the interface size too. - */ +/** A column's content width, as CSS: a numeric column's grows with the interface size */ function columnTrack(c: Column): string { - if (c.fitChars == null) return `${c.current}px`; - const width = c.resized ? `${c.current}px` : `${c.current / BASE_FONT_SIZE}rem`; - return `max(${width}, ${c.fitChars * CHAR_REM}rem)`; + return c.scales ? `${c.current / BASE_FONT_SIZE}rem` : `${c.current}px`; +} + +/** + * How far a resize moved, in the pixels a column's width is kept in: a numeric column's are at + * the default interface size, `rootFontSize` being the size now + */ +export function resizeDistance(c: ColumnDef, dx: number, rootFontSize: number): number { + return c.scales && rootFontSize > 0 ? (dx * BASE_FONT_SIZE) / rootFontSize : dx; } /** The grid template for the header and every row: the mark, then each column and its padding */ @@ -260,10 +260,9 @@ export function gridTemplate(columns: Column[]): string { /** The width of a row's grid, as CSS: the list scrolls sideways when it is wider than the list */ export function gridWidth(columns: Column[]): string { - const fixed = columns.filter((c) => c.fitChars == null); - const px = fixed.reduce((sum, c) => sum + c.current, MARK_WIDTH); - const numeric = columns.filter((c) => c.fitChars != null).map((c) => ` + ${columnTrack(c)}`); - return `calc(${px}px${numeric.join("")} + ${columns.length} * ${CELL_PADDING})`; + const px = columns.filter((c) => !c.scales).reduce((sum, c) => sum + c.current, MARK_WIDTH); + const scaled = columns.filter((c) => c.scales).map((c) => ` + ${columnTrack(c)}`); + return `calc(${px}px${scaled.join("")} + ${columns.length} * ${CELL_PADDING})`; } /** Whether the picker may uncheck `id`: any column but the last one shown */