diff --git a/apps/web/src/page/traces/detail/waterfall/row.ts b/apps/web/src/page/traces/detail/waterfall/row.ts index a99ec8a17d..01b7377502 100644 --- a/apps/web/src/page/traces/detail/waterfall/row.ts +++ b/apps/web/src/page/traces/detail/waterfall/row.ts @@ -2,15 +2,16 @@ import { cacheResultStyles, getCacheInfo } from "@maple/ui/lib/cache" import { getServiceColor } from "@maple/ui/lib/colors" import { formatDuration } from "@maple/ui/lib/format" import { getHttpInfo, HTTP_METHOD_COLORS, httpStatusTone } from "@maple/ui/lib/http" -import { getSpanKindLabel, getSpanStatusBadgeClass } from "@maple/ui/lib/span-kind" +import { getSpanKindLabel } from "@maple/ui/lib/span-kind" import { spanStartMs } from "@maple/ui/lib/span-tree" -import { TONE_FILL, TONE_TEXT } from "@maple/ui/lib/tone" +import { severityTone, TONE_COLOR, TONE_FILL, TONE_SOFT } from "@maple/ui/lib/tone" import { cn } from "@maple/ui/lib/utils" import { Option } from "effect" import type { Html, HtmlBuilder } from "foldkit/html" import { badge } from "../../../../ui/display" import { icon } from "../../../../ui/icon" import { iconButton } from "../../../../ui/icon-button" +import { traceStatusBadge } from "../../../../ui/trace-badges" import { truncatedId } from "../../../../ui/truncated-id" import { truncatedText } from "../../../../ui/truncated-text" import { serviceDot } from "../../../../ui/values" @@ -51,7 +52,7 @@ const treeGuides = (h: HtmlBuilder, row: TreeRow, isExpanded: boolean): h.div( [ h.AriaHidden(true), - h.Class("relative -my-1.5 shrink-0 self-stretch"), + h.Class("relative shrink-0 self-stretch"), h.Style({ width: `${shown * COLUMN_PX}px`, "margin-right": shown === 0 ? "-8px" : "0px" }), ], [ @@ -107,8 +108,9 @@ const treeGuides = (h: HtmlBuilder, row: TreeRow, isExpanded: boolean): ] } +// The status column carries Ok; the tree only flags errors. const statusFill = (statusCode: string): string => - statusCode === "Error" ? TONE_FILL.crit : statusCode === "Ok" ? TONE_FILL.ok : TONE_FILL.neutral + statusCode === "Error" ? TONE_FILL.crit : TONE_FILL.neutral /** A parent's chevron, or a leaf's status square where the elbow lands. */ const toggle = (h: HtmlBuilder, span: SpanNodeData, isExpanded: boolean): Html => @@ -126,7 +128,7 @@ const toggle = (h: HtmlBuilder, span: SpanNodeData, isExpanded: boolean [icon(h, isExpanded ? "ChevronDownIcon" : "ChevronRightIcon")], ) : h.div( - [h.Class("flex h-7 w-6 shrink-0 items-center justify-center")], + [h.Class("flex size-6 shrink-0 items-center justify-center")], [ h.span( [ @@ -155,8 +157,9 @@ const pixelBar = ( [ h.Attribute( "class", - `w-[5px] h-2.5 rounded-2xs shrink-0 ${isActive ? color : "bg-muted"}`, + `w-[5px] h-2.5 rounded-2xs shrink-0 ${isActive ? "" : "bg-muted"}`, ), + ...(isActive ? [h.Style({ "background-color": color })] : []), ], [], ) @@ -170,14 +173,14 @@ const missingRow = (h: HtmlBuilder, row: TreeRow, ctx: RowContext): Htm [ h.Class( cn( - "@container/row group flex items-stretch px-2 bg-surface-sunken", + "@container/row group flex h-7 items-stretch px-2 bg-surface-sunken", ctx.isSelected && "bg-primary-a1 shadow-accent-start", ), ), ], [ h.div( - [h.Class("flex items-center gap-2 flex-1 min-w-0 overflow-hidden py-1.5")], + [h.Class("flex items-center gap-2 flex-1 min-w-0 overflow-hidden")], [ ...treeGuides(h, row, ctx.isExpanded), toggle(h, span, ctx.isExpanded), @@ -201,7 +204,7 @@ const missingRow = (h: HtmlBuilder, row: TreeRow, ctx: RowContext): Htm ], ), h.div( - [h.Class("flex items-center gap-2 shrink-0 ml-2 py-1.5")], + [h.Class("flex items-center gap-2 shrink-0 ml-2")], [ h.div([h.Class("hidden w-48 @min-[560px]/row:block")], []), truncatedId(h, { @@ -346,49 +349,31 @@ const locationLabel = (h: HtmlBuilder, row: TreeRow): ReadonlyArray, span: SpanNodeData, httpInfo: HttpInfo | null): Html => { const cacheInfo = getCacheInfo({ ...span.spanAttributes }) if (cacheInfo?.result === "hit" || cacheInfo?.result === "miss") { return badge( h, - { - variant: "outline", - className: cn( - "text-3xs w-14 justify-center font-medium", - cacheResultStyles[cacheInfo.result], - ), - }, + { variant: "outline", className: cn(STATUS_CELL, cacheResultStyles[cacheInfo.result]) }, [cacheInfo.result === "hit" ? "HIT" : "MISS"], ) } const code = httpInfo?.statusCode ?? null if (code !== null && code !== 0) { - const tone = httpStatusTone(code) - return h.span( - [ - h.Class( - cn( - "w-14 text-center font-mono text-xs font-medium", - tone !== "neutral" - ? TONE_TEXT[tone] - : code >= 300 - ? "text-chart-p50" - : "text-severity-info", - ), - ), - ], - [String(code)], - ) + return traceStatusBadge(h, { + hasError: span.statusCode === "Error", + httpStatusCode: code, + className: STATUS_CELL, + }) } return badge( h, { - variant: "outline", - className: cn( - "text-3xs w-14 justify-center font-medium", - getSpanStatusBadgeClass(span.statusCode), - ), + variant: "secondary", + className: cn(STATUS_CELL, TONE_SOFT[severityTone(span.statusCode)]), }, [span.statusCode === "" ? "Unset" : span.statusCode], ) @@ -405,18 +390,17 @@ const rightSection = ( ctx.totalDurationMs > 0 ? ((spanStartMs(span) - traceStartMs) / ctx.totalDurationMs) * 100 : 0 const widthPercent = ctx.totalDurationMs > 0 ? (span.durationMs / ctx.totalDurationMs) * 100 : 0 const httpTone = httpStatusTone(httpInfo?.statusCode) + // Errors and HTTP warnings keep their tone; everything else takes the service colour, as on the Timeline tab. const barColor = - httpTone === "crit" - ? TONE_FILL.crit + httpTone === "crit" || span.statusCode === "Error" + ? TONE_COLOR.crit : httpTone === "warn" - ? TONE_FILL.warn - : span.statusCode === "Error" - ? TONE_FILL.crit - : "bg-primary" + ? TONE_COLOR.warn + : getServiceColor(span.serviceName) const platform = rowPlatform({ ...span.spanAttributes }) const outcome = Option.fromNullishOr(platform?.outcome).pipe(Option.filter((o) => o.bad)) return h.div( - [h.Class("flex items-center gap-2 shrink-0 ml-2 py-1.5")], + [h.Class("flex items-center gap-2 shrink-0 ml-2")], [ pixelBar(h, leftPercent, Math.max(widthPercent, 1), barColor), h.span( @@ -461,7 +445,7 @@ export const spanRow = (h: HtmlBuilder, row: TreeRow, ctx: RowContext): [ h.Class( cn( - "@container/row group flex items-stretch hover:bg-surface-sunken active:bg-surface-sunken cursor-pointer px-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "@container/row group flex h-7 items-stretch hover:bg-surface-sunken active:bg-surface-sunken cursor-pointer px-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", span.statusCode === "Error" && "bg-severity-error-a1", ctx.isSelected && "bg-primary-a1 shadow-accent-start", ), @@ -473,7 +457,7 @@ export const spanRow = (h: HtmlBuilder, row: TreeRow, ctx: RowContext): ], [ h.div( - [h.Class("@container flex items-center gap-2 flex-1 min-w-0 overflow-hidden py-1.5")], + [h.Class("@container flex items-center gap-2 flex-1 min-w-0 overflow-hidden")], [ ...treeGuides(h, row, ctx.isExpanded), toggle(h, span, ctx.isExpanded), diff --git a/apps/web/src/page/traces/detail/waterfall/view.ts b/apps/web/src/page/traces/detail/waterfall/view.ts index 397b154f43..e0f19675f4 100644 --- a/apps/web/src/page/traces/detail/waterfall/view.ts +++ b/apps/web/src/page/traces/detail/waterfall/view.ts @@ -1,6 +1,7 @@ import { Submodel } from "foldkit" import type { Html, HtmlBuilder } from "foldkit/html" import { emptyMessage } from "../../../../ui/empty" +import { eyebrow } from "../../../../ui/eyebrow" import { depthControls } from "../depth-controls" import { Message, type Model } from "./model" import { spanRow } from "./row" @@ -53,9 +54,9 @@ const header = (h: HtmlBuilder, shown: number, total: number): Html => h.div( [h.Class("flex items-center gap-2 shrink-0 ml-2")], [ - h.span([h.Class("hidden w-48 text-center @min-[560px]/row:block")], ["Duration"]), - h.span([h.Class("w-16 text-right")], ["Time"]), - h.span([h.Class("w-14 text-center")], ["Status"]), + eyebrow(h, { className: "hidden w-48 @min-[560px]/row:block" }, ["Timeline"]), + eyebrow(h, { className: "w-16 text-right" }, ["Duration"]), + eyebrow(h, { className: "w-14 text-center" }, ["Status"]), ], ), ], diff --git a/apps/web/src/page/traces/detail/waterfall/virtualize.ts b/apps/web/src/page/traces/detail/waterfall/virtualize.ts index 40f299c0d7..a8d844e911 100644 --- a/apps/web/src/page/traces/detail/waterfall/virtualize.ts +++ b/apps/web/src/page/traces/detail/waterfall/virtualize.ts @@ -8,7 +8,7 @@ export const KEYS_ATTRIBUTE = "waterfall-keys" export const SELECTED_ATTRIBUTE = "waterfall-selected" /** Estimated row height; measured rows replace it. */ -export const ROW_HEIGHT = 33 +export const ROW_HEIGHT = 28 const OVERSCAN = 12 const MAX_SCROLL_ATTEMPTS = 15 diff --git a/apps/web/src/ui/trace-badges.ts b/apps/web/src/ui/trace-badges.ts index 64afe85889..348b2b5b5f 100644 --- a/apps/web/src/ui/trace-badges.ts +++ b/apps/web/src/ui/trace-badges.ts @@ -15,25 +15,32 @@ import { serviceDot } from "./values" /** The root's HTTP status toned by class (3xx keeps its own blue), else OK / Error. */ export const traceStatusBadge = ( h: HtmlBuilder, - options: Readonly<{ hasError: boolean; httpStatusCode?: number | null }>, + options: Readonly<{ hasError: boolean; httpStatusCode?: number | null; className?: string }>, ): Html => { const code = options.httpStatusCode if (code === undefined || code === null) { - return badge(h, { variant: "secondary", className: TONE_SOFT[options.hasError ? "crit" : "ok"] }, [ - options.hasError ? "Error" : "OK", - ]) + return badge( + h, + { + variant: "secondary", + className: cn(TONE_SOFT[options.hasError ? "crit" : "ok"], options.className), + }, + [options.hasError ? "Error" : "OK"], + ) } const tone = httpStatusTone(code) return badge( h, { variant: "secondary", - className: + className: cn( tone !== "neutral" ? TONE_SOFT[tone] : code >= 300 ? "bg-chart-p50/15 text-chart-p50" : TONE_SOFT.ok, + options.className, + ), }, [String(code)], )