diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index 944d1f90f..b72ee77bd 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -11,6 +11,7 @@ import { } from "@heroicons/react/24/solid"; import { Bars3BottomLeftIcon, DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline"; import { formatDurationSecs } from "../lib/format"; +import { ACTIVE_STAGE_STATES } from "../lib/stage-sidebar"; export interface Stage { id: string; @@ -20,8 +21,6 @@ export interface Stage { dotId?: string; } -const activeStageStates = new Set(["running", "retrying"]); - export const statusConfig: Record; color: string }> = { pending: { icon: PauseCircleIcon, color: "text-fg-muted" }, running: { icon: ArrowPathIcon, color: "text-teal-500" }, @@ -48,7 +47,7 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta // Track start times for running stages useEffect(() => { const running = new Set( - stages.filter((s) => activeStageStates.has(s.status)).map((s) => s.id), + stages.filter((s) => ACTIVE_STAGE_STATES.has(s.status)).map((s) => s.id), ); for (const stageId of running) { if (!runningStartRef.current.has(stageId)) { @@ -64,13 +63,13 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta // Tick every second while any stage is running useEffect(() => { - if (!stages.some((s) => activeStageStates.has(s.status))) return; + if (!stages.some((s) => ACTIVE_STAGE_STATES.has(s.status))) return; const interval = setInterval(() => setTick((t) => t + 1), 1000); return () => clearInterval(interval); }, [stages]); function stageDuration(stage: Stage): string { - if (activeStageStates.has(stage.status)) { + if (ACTIVE_STAGE_STATES.has(stage.status)) { const start = runningStartRef.current.get(stage.id); if (start) return formatDurationSecs(Math.floor((Date.now() - start) / 1000)); return "0s"; @@ -100,7 +99,7 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta : "text-fg-3 hover:bg-overlay hover:text-fg" }`} > - + {stage.name} {stageDuration(stage)} diff --git a/apps/fabro-web/app/lib/stage-sidebar.ts b/apps/fabro-web/app/lib/stage-sidebar.ts index e007c56ae..747c6a70e 100644 --- a/apps/fabro-web/app/lib/stage-sidebar.ts +++ b/apps/fabro-web/app/lib/stage-sidebar.ts @@ -1,9 +1,15 @@ -import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; +import type { PaginatedRunStageList, StageState } from "@qltysh/fabro-api-client"; import type { Stage } from "../components/stage-sidebar"; import { isVisibleStage } from "../data/runs"; import { formatDurationSecs } from "./format"; +export const ACTIVE_STAGE_STATES: ReadonlySet = new Set(["running", "retrying"]); +export const SUCCEEDED_STAGE_STATES: ReadonlySet = new Set([ + "succeeded", + "partially_succeeded", +]); + export function mapRunStagesToSidebarStages( stagesResult: PaginatedRunStageList | null | undefined, ): Stage[] { diff --git a/apps/fabro-web/app/routes/run-files/states.tsx b/apps/fabro-web/app/routes/run-files/states.tsx index 026694269..9f85f91ab 100644 --- a/apps/fabro-web/app/routes/run-files/states.tsx +++ b/apps/fabro-web/app/routes/run-files/states.tsx @@ -43,15 +43,9 @@ export function emptyStateCopy(kind: EmptyKind): string { } } -/// Derive the empty-state variant from the full loader context. `runStatus` -/// comes from the parent run loader (`run.lifecycleStatus`); its absence -/// collapses to the "unknown" catchall so the empty state never displays -/// misleading copy. -/// -/// The full RunStatus enum (per fabro-types/src/status.rs) is: -/// submitted, queued, starting, running, blocked, paused, removing, -/// succeeded, failed, dead, archived -/// partially_succeeded is a stage outcome, not a run status. +/// `runStatus` comes from the parent run loader (`run.lifecycleStatus`); its +/// absence collapses to the "unknown" catchall so the empty state never +/// displays misleading copy. export function deriveEmptyKind(args: { runStatus: string | undefined; totalChanged: number; diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx index c60d1baf8..ec6841b27 100644 --- a/apps/fabro-web/app/routes/run-overview.tsx +++ b/apps/fabro-web/app/routes/run-overview.tsx @@ -10,7 +10,11 @@ import { GraphToolbar, } from "../components/graph-toolbar"; import { EmptyState } from "../components/state"; -import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar"; +import { + ACTIVE_STAGE_STATES, + SUCCEEDED_STAGE_STATES, + mapRunStagesToSidebarStages, +} from "../lib/stage-sidebar"; export const handle = { wide: true }; @@ -52,13 +56,13 @@ export default function RunOverview() { const gt = graphTheme; const runningDotIds = new Set( - stages.filter((s: Stage) => s.status === "running" || s.status === "retrying").map((s: Stage) => s.dotId ?? s.id), + stages.filter((s: Stage) => ACTIVE_STAGE_STATES.has(s.status)).map((s: Stage) => s.dotId ?? s.id), ); const failedDotIds = new Set( stages.filter((s: Stage) => s.status === "failed").map((s: Stage) => s.dotId ?? s.id), ); const completedDotIds = new Set( - stages.filter((s: Stage) => s.status === "succeeded" || s.status === "partially_succeeded").map((s: Stage) => s.dotId ?? s.id), + stages.filter((s: Stage) => SUCCEEDED_STAGE_STATES.has(s.status)).map((s: Stage) => s.dotId ?? s.id), ); const dotIdToStageId = new Map( stages.map((s: Stage) => [s.dotId ?? s.id, s.id]), diff --git a/apps/fabro-web/app/routes/workflow-detail.tsx b/apps/fabro-web/app/routes/workflow-detail.tsx index 68c5dbdb6..8036c17e7 100644 --- a/apps/fabro-web/app/routes/workflow-detail.tsx +++ b/apps/fabro-web/app/routes/workflow-detail.tsx @@ -95,9 +95,6 @@ function sampleSettings({ }; } -// Static sample data used by the `workflow-definition` index route for the -// hardcoded showcase workflows. Shape mirrors the persisted -// `WorkflowSettings` snapshot returned by `/api/v1/runs/:id/settings`. export const workflowData: Record = { fix_build: { name: "Fix Build", diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index f947973ee..0b4c7a875 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -2150,23 +2150,18 @@ async fn openapi_spec() -> Response { } fn active_stage_state_from_events(events: &[EventEnvelope], node_id: &str) -> StageState { - let latest_stage_event = events.iter().rev().find_map(|envelope| { - let event = &envelope.event; - if event.node_id.as_deref() == Some(node_id) { - match event.event_name() { - "stage.retrying" | "stage.started" | "stage.completed" | "stage.failed" => { - Some(event.event_name()) - } - _ => None, - } - } else { - None - } + let latest = events.iter().rev().find(|envelope| { + envelope.event.node_id.as_deref() == Some(node_id) + && matches!( + envelope.event.event_name(), + "stage.retrying" | "stage.started" | "stage.completed" | "stage.failed" + ) }); - match latest_stage_event { - Some("stage.retrying") => StageState::Retrying, - _ => StageState::Running, + if latest.is_some_and(|e| e.event.event_name() == "stage.retrying") { + StageState::Retrying + } else { + StageState::Running } } @@ -2270,7 +2265,6 @@ async fn list_run_stages( .into_response(); }; - // Get durations and active retrying state from events. let events = match state.store.open_run_reader(&id).await { Ok(run_store) => run_store.list_events().await.unwrap_or_default(), Err(_) => Vec::new(),