diff --git a/apps/fabro-web/app/components/run-waterfall.tsx b/apps/fabro-web/app/components/run-waterfall.tsx index 9bf76c148..b9b06fb08 100644 --- a/apps/fabro-web/app/components/run-waterfall.tsx +++ b/apps/fabro-web/app/components/run-waterfall.tsx @@ -1,10 +1,15 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; import { Link } from "react-router"; import { StageState, type RunStage } from "@qltysh/fabro-api-client"; +import { HoverCard, PopoverHeader, PopoverRow, PopoverRows } from "./ui"; import { isVisibleStage } from "../data/runs"; import { formatAbsoluteTs, formatDurationMs } from "../lib/format"; -import { formatStageLabel } from "../lib/stage-sidebar"; +import { + formatStageLabel, + stageStatusLabel, + stageStatusTone, +} from "../lib/stage-sidebar"; import { deriveRunPhases, type RunPhase } from "../lib/run-phases"; import type { EventEnvelope } from "@qltysh/fabro-api-client"; @@ -25,7 +30,7 @@ interface Row { durationMs: number | null; barClass: string; href: string | null; - tooltip: string; + popover: ReactNode; } const MIN_BAR_WIDTH_PCT = 0.4; @@ -71,23 +76,72 @@ function chooseTickIntervalMs(rangeMs: number): number { return 60 * 60_000; } +function phasePopover(phase: RunPhase, durationMs: number | null, inFlight: boolean): ReactNode { + return ( + <> + {phase.label} + + + {formatAbsoluteTs(new Date(phase.startMs).toISOString())} + + + + {durationMs != null ? formatDurationMs(durationMs) : "--"} + + + + + ); +} + function phaseRow(phase: RunPhase, nowMs: number): Row { const endMs = phase.endMs; + const inFlight = endMs == null; const closedEnd = endMs ?? nowMs; - const durationMs = closedEnd - phase.startMs; + const rawDuration = closedEnd - phase.startMs; + const durationMs = rawDuration >= 0 ? rawDuration : null; return { key: `phase:${phase.kind}`, kind: "phase", label: phase.label, startMs: phase.startMs, endMs, - durationMs: durationMs >= 0 ? durationMs : null, - barClass: endMs == null ? "bg-fg-3/40 animate-pulse" : "bg-fg-3/40", + durationMs, + barClass: inFlight ? "bg-fg-3/40 animate-pulse" : "bg-fg-3/40", href: null, - tooltip: `${phase.label} • started ${formatAbsoluteTs(new Date(phase.startMs).toISOString())}`, + popover: phasePopover(phase, durationMs, inFlight), }; } +function stagePopover( + stage: RunStage, + durationMs: number | null, + inFlight: boolean, +): ReactNode { + return ( + <> + {formatStageLabel(stage)} + + + + {stageStatusLabel(stage.status)} + + + {stage.started_at && ( + {formatAbsoluteTs(stage.started_at)} + )} + + + {durationMs != null ? formatDurationMs(durationMs) : "--"} + + + + + ); +} + function stageRow(runId: string, stage: RunStage, nowMs: number): Row | null { if (!stage.started_at) return null; const startMs = Date.parse(stage.started_at); @@ -105,7 +159,7 @@ function stageRow(runId: string, stage: RunStage, nowMs: number): Row | null { durationMs, barClass: stageBarClass(stage.status), href: `/runs/${runId}/stages/${encodeURIComponent(stage.id)}`, - tooltip: `${formatStageLabel(stage)} • ${stage.status} • started ${formatAbsoluteTs(stage.started_at)}`, + popover: stagePopover(stage, durationMs, inFlight), }; } @@ -242,11 +296,8 @@ function WaterfallRow({ row.kind === "phase" ? "text-fg-muted" : "text-fg-2"; - const content = ( -
+ const inner = ( +
{row.label}
@@ -261,15 +312,19 @@ function WaterfallRow({
); - if (row.href) { - return ( - - {content} - - ); - } - return content; + const trigger = row.href ? ( + + {inner} + + ) : ( + inner + ); + return ( + + {trigger} + + ); } diff --git a/apps/fabro-web/app/components/ui.tsx b/apps/fabro-web/app/components/ui.tsx index 3b7aebe00..5f460d4e8 100644 --- a/apps/fabro-web/app/components/ui.tsx +++ b/apps/fabro-web/app/components/ui.tsx @@ -226,9 +226,11 @@ function hoverCardStyle(rect: DOMRect): CSSProperties { export function HoverCard({ content, children, + className = "inline-flex", }: { content: ReactNode; children: ReactNode; + className?: string; }) { const { open, rect, triggerProps } = useHoverAnchor(); const id = useId(); @@ -236,7 +238,7 @@ export function HoverCard({ return ( <> - + {children} {rect && portalTarget @@ -255,3 +257,24 @@ export function HoverCard({ ); } + +export function PopoverHeader({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function PopoverRows({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function PopoverRow({ label, children }: { label: string; children: ReactNode }) { + return ( + <> +
{label}
+
{children}
+ + ); +} diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 4ba358ee1..86cb6f7e4 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -38,7 +38,15 @@ import { InterviewDock } from "../components/interview-dock"; import { SteerBar, type SteerBarHandle } from "../components/steer-bar"; import { ErrorState } from "../components/state"; import { useToast } from "../components/toast"; -import { ConfirmDialog, HoverCard, SECONDARY_BUTTON_CLASS, Tooltip } from "../components/ui"; +import { + ConfirmDialog, + HoverCard, + PopoverHeader, + PopoverRow, + PopoverRows, + SECONDARY_BUTTON_CLASS, + Tooltip, +} from "../components/ui"; import { isRunStatus, mapRunToRunItem, @@ -193,27 +201,6 @@ export function meta({ data }: any) { // ---- Header hover-card popovers ---- -function PopoverHeader({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} - -function PopoverRows({ children }: { children: ReactNode }) { - return
{children}
; -} - -function PopoverRow({ label, children }: { label: string; children: ReactNode }) { - return ( - <> -
{label}
-
{children}
- - ); -} - function humanizeFailureReason(reason: string): string { const spaced = reason.replace(/_/g, " "); return spaced.charAt(0).toUpperCase() + spaced.slice(1);