diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts index b397c5ecc..e1c235605 100644 --- a/apps/fabro-web/app/data/runs.ts +++ b/apps/fabro-web/app/data/runs.ts @@ -48,6 +48,7 @@ export const columnStatuses = [ BoardColumn.BLOCKED, BoardColumn.SUCCEEDED, BoardColumn.FAILED, + BoardColumn.ARCHIVED, ] as const satisfies readonly ColumnStatus[]; export const columnStatusDisplay: Record = { @@ -57,6 +58,7 @@ export const columnStatusDisplay: Record(queryKeys.boards.runs(), apiPaginatedFetcher); +export function useBoardsRuns(includeArchived: boolean = false) { + return useSWR( + queryKeys.boards.runs(includeArchived), + apiPaginatedFetcher, + ); } export function useRun(id: string | undefined) { diff --git a/apps/fabro-web/app/lib/query-keys.ts b/apps/fabro-web/app/lib/query-keys.ts index 402739897..1b1ffabf0 100644 --- a/apps/fabro-web/app/lib/query-keys.ts +++ b/apps/fabro-web/app/lib/query-keys.ts @@ -24,7 +24,10 @@ export const queryKeys = { attach: () => "/api/v1/attach", }, boards: { - runs: () => "/api/v1/boards/runs", + runs: (includeArchived = false) => + withQuery("/api/v1/boards/runs", { + include_archived: includeArchived ? "true" : null, + }), }, runs: { detail: (id: string) => `/api/v1/runs/${pathSegment(id)}`, diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx index 3cd9dec0a..140f9820e 100644 --- a/apps/fabro-web/app/routes/runs.test.tsx +++ b/apps/fabro-web/app/routes/runs.test.tsx @@ -44,6 +44,58 @@ describe("runs route board mapping", () => { expect(columns.find((column) => column.id === "blocked")?.items[0]?.question).toBe("Older unresolved question?"); }); + test("renders an archived column when the response includes one", () => { + const columns = buildBoardColumns({ + columns: [ + { id: "queued", name: "Queued" }, + { id: "initializing", name: "Initializing" }, + { id: "running", name: "Running" }, + { id: "blocked", name: "Blocked" }, + { id: "succeeded", name: "Succeeded" }, + { id: "failed", name: "Failed" }, + { id: "archived", name: "Archived" }, + ], + data: [ + boardRun("succeeded-run", "succeeded"), + boardRun("archived-run", "archived"), + ], + meta: { has_more: false }, + }); + + expect(columns.map((column) => column.id)).toEqual([ + "queued", + "initializing", + "running", + "blocked", + "succeeded", + "failed", + "archived", + ]); + expect( + columns.find((column) => column.id === "archived")?.items.map((item) => item.id), + ).toEqual(["archived-run"]); + expect( + columns.find((column) => column.id === "succeeded")?.items.map((item) => item.id), + ).toEqual(["succeeded-run"]); + }); + + test("omits the archived column when the response does not include it", () => { + const columns = buildBoardColumns({ + columns: [ + { id: "queued", name: "Queued" }, + { id: "initializing", name: "Initializing" }, + { id: "running", name: "Running" }, + { id: "blocked", name: "Blocked" }, + { id: "succeeded", name: "Succeeded" }, + { id: "failed", name: "Failed" }, + ], + data: [boardRun("succeeded-run", "succeeded")], + meta: { has_more: false }, + }); + + expect(columns.some((column) => column.id === "archived")).toBe(false); + }); + test("refreshes for blocked status and interview events", () => { expect(shouldRefreshBoardForEvent("run.queued")).toBe(true); expect(shouldRefreshBoardForEvent("run.blocked")).toBe(true); diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 95801988a..2f3c30347 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Link } from "react-router"; -import { ChevronDownIcon, CommandLineIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { ArchiveBoxIcon, ChevronDownIcon, CommandLineIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { useSWRConfig } from "swr"; @@ -50,6 +50,7 @@ const columnStyles: Record = { blocked: { iconType: "branch", actions: ["Answer Question"] }, succeeded: { iconType: "pr", actions: [] }, failed: { iconType: "branch", actions: [] }, + archived: { iconType: "branch", actions: [] }, }; const defaultColumnStyle: ColumnStyle = { iconType: "branch", actions: [] }; @@ -71,18 +72,20 @@ type Column = { items: RunItem[]; }; -function buildSkeletonColumns(): Column[] { - return columnStatuses.map((id) => { - const colors = columnStatusDisplay[id]; - return { - id, - name: colors.label, - dot: colors.dot, - text: colors.text, - ...(columnStyles[id] ?? defaultColumnStyle), - items: [], - }; - }); +function buildSkeletonColumns(includeArchived: boolean): Column[] { + return columnStatuses + .filter((id) => includeArchived || id !== "archived") + .map((id) => { + const colors = columnStatusDisplay[id]; + return { + id, + name: colors.label, + dot: colors.dot, + text: colors.text, + ...(columnStyles[id] ?? defaultColumnStyle), + items: [], + }; + }); } export function buildBoardColumns(response: BoardRunsResponse): Column[] { @@ -750,7 +753,8 @@ function RunsLandingEmpty({ } export default function Runs() { - const boardRuns = useBoardsRuns(); + const [includeArchived, setIncludeArchived] = useState(false); + const boardRuns = useBoardsRuns(includeArchived); const authConfig = useAuthConfig(); const systemInfo = useSystemInfo(); const isLandingReady = @@ -758,8 +762,11 @@ export default function Runs() { authConfig.data !== undefined && systemInfo.data !== undefined; const initialColumns = useMemo( - () => boardRuns.data ? buildBoardColumns(boardRuns.data) : buildSkeletonColumns(), - [boardRuns.data], + () => + boardRuns.data + ? buildBoardColumns(boardRuns.data) + : buildSkeletonColumns(includeArchived), + [boardRuns.data, includeArchived], ); const hasGitHubAuth = authConfig.data?.methods.includes("github") === true; const serverUrl = systemInfo.data?.server_url; @@ -851,6 +858,16 @@ export default function Runs() { +