diff --git a/web/src/lib/__tests__/pipelineRunSelection.test.ts b/web/src/lib/__tests__/pipelineRunSelection.test.ts new file mode 100644 index 00000000..c69f8dd8 --- /dev/null +++ b/web/src/lib/__tests__/pipelineRunSelection.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { selectRunIdToLoad } from "../pipelineRunSelection"; + +describe("selectRunIdToLoad", () => { + it("keeps the user's selected run across polls", () => { + expect(selectRunIdToLoad("older-run", ["newest-run", "older-run"])).toBe("older-run"); + }); + + it("auto-selects the newest run when no run is selected", () => { + expect(selectRunIdToLoad(null, ["newest-run", "older-run"])).toBe("newest-run"); + }); + + it("selects the newest run when the previous selection belongs to another pipeline", () => { + expect(selectRunIdToLoad("other-pipeline-run", ["newest-run", "older-run"])).toBe("newest-run"); + }); + + it("clears the selection when there are no runs", () => { + expect(selectRunIdToLoad("older-run", [])).toBeNull(); + }); +}); diff --git a/web/src/lib/pipelineRunSelection.ts b/web/src/lib/pipelineRunSelection.ts new file mode 100644 index 00000000..4b847889 --- /dev/null +++ b/web/src/lib/pipelineRunSelection.ts @@ -0,0 +1,8 @@ +export function selectRunIdToLoad( + selectedRunId: string | null, + runIds: readonly string[] +): string | null { + if (runIds.length === 0) return null; + if (selectedRunId && runIds.includes(selectedRunId)) return selectedRunId; + return runIds[0] ?? null; +} diff --git a/web/src/pages/Pipelines.tsx b/web/src/pages/Pipelines.tsx index dbb09985..d37c4322 100644 --- a/web/src/pages/Pipelines.tsx +++ b/web/src/pages/Pipelines.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { api, type PipelineData, @@ -8,6 +8,7 @@ import { type PipelineStepData, } from "../lib/api"; import { useTranslation } from "react-i18next"; +import { selectRunIdToLoad } from "../lib/pipelineRunSelection"; interface StepDraft { id: string; @@ -438,6 +439,7 @@ export function Pipelines() { const [saving, setSaving] = useState(false); const [running, setRunning] = useState(false); const [error, setError] = useState(null); + const selectedRunIdRef = useRef(null); const selected = useMemo( () => pipelines.find((pipeline) => pipeline.id === selectedId) ?? pipelines[0] ?? null, @@ -462,7 +464,9 @@ export function Pipelines() { const loadRunDetail = useCallback(async (pipelineId: string, runId: string) => { try { const res = await api.pipelineRunDetail(pipelineId, runId); - setSelectedRun(res.data ?? null); + const detail = res.data ?? null; + selectedRunIdRef.current = detail?.run.id ?? null; + setSelectedRun(detail); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } @@ -475,9 +479,14 @@ export function Pipelines() { const res = await api.pipelineRunsList(pipelineId); const next = res.data ?? []; setRuns(next); - if (next[0]) { - await loadRunDetail(pipelineId, next[0].id); + const runId = selectRunIdToLoad( + selectedRunIdRef.current, + next.map((run) => run.id) + ); + if (runId) { + await loadRunDetail(pipelineId, runId); } else { + selectedRunIdRef.current = null; setSelectedRun(null); } } catch (err) {