refactor: simplify after stage status unification

Share ACTIVE_STAGE_STATES/SUCCEEDED_STAGE_STATES across stage-sidebar and
run-overview, collapse the nested match in active_stage_state_from_events,
and drop a few WHAT-comments that narrated the recent rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-30 08:02:08 -04:00
parent 29eca2921d
commit 118ce83966
No known key found for this signature in database
6 changed files with 32 additions and 38 deletions

View file

@ -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<StageState>(["running", "retrying"]);
export const statusConfig: Record<StageState, { icon: ComponentType<{ className?: string }>; 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<string>(
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"
}`}
>
<Icon className={`size-4 shrink-0 ${config.color} ${activeStageStates.has(stage.status) ? "animate-spin" : ""}`} />
<Icon className={`size-4 shrink-0 ${config.color} ${ACTIVE_STAGE_STATES.has(stage.status) ? "animate-spin" : ""}`} />
<span className="flex-1 truncate">{stage.name}</span>
<span className="font-mono text-xs tabular-nums text-fg-muted">{stageDuration(stage)}</span>
</Link>

View file

@ -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<StageState> = new Set(["running", "retrying"]);
export const SUCCEEDED_STAGE_STATES: ReadonlySet<StageState> = new Set([
"succeeded",
"partially_succeeded",
]);
export function mapRunStagesToSidebarStages(
stagesResult: PaginatedRunStageList | null | undefined,
): Stage[] {

View file

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

View file

@ -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<string>(
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<string>(
stages.filter((s: Stage) => s.status === "failed").map((s: Stage) => s.dotId ?? s.id),
);
const completedDotIds = new Set<string>(
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<string, string>(
stages.map((s: Stage) => [s.dotId ?? s.id, s.id]),

View file

@ -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<string, WorkflowEntry> = {
fix_build: {
name: "Fix Build",

View file

@ -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(),