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);