diff --git a/apps/fabro-web/app/routes/runs.tsx b/apps/fabro-web/app/routes/runs.tsx index 9c32daace..95801988a 100644 --- a/apps/fabro-web/app/routes/runs.tsx +++ b/apps/fabro-web/app/routes/runs.tsx @@ -1,6 +1,9 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Link } from "react-router"; -import { ChevronDownIcon, ChevronRightIcon, CommandLineIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { 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"; import { DndContext, closestCenter, @@ -22,8 +25,11 @@ import { ciConfig, columnStatusDisplay, columnStatuses, deriveCiStatus, mapRunLi import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs"; import { EmptyState } from "../components/state"; import { SteerComposer } from "../components/steer-composer"; +import { useToast } from "../components/toast"; import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events"; import { useAuthConfig, useBoardsRuns, useSystemInfo } from "../lib/queries"; +import { queryKeys } from "../lib/query-keys"; +import { archiveRun, canArchive } from "../lib/run-actions"; import type { PaginatedBoardRunList } from "@qltysh/fabro-api-client"; export { shouldRefreshBoardForEvent }; @@ -461,6 +467,84 @@ function SortablePrCard({ ); } +function archivableItems(items: RunItem[]): RunItem[] { + return items.filter((item) => canArchive(item.lifecycleStatus)); +} + +function ColumnActionsMenu({ column }: { column: Column }) { + const archivable = archivableItems(column.items); + const [pending, setPending] = useState(false); + const { mutate } = useSWRConfig(); + const { push } = useToast(); + + if (archivable.length === 0) return null; + + async function handleArchiveAll() { + if (pending) return; + setPending(true); + const total = archivable.length; + try { + const results = await Promise.allSettled( + archivable.map((item) => archiveRun(item.id)), + ); + const succeeded = results.filter((r) => r.status === "fulfilled").length; + const failed = total - succeeded; + const runWord = (n: number) => (n === 1 ? "run" : "runs"); + if (failed === 0) { + push({ message: `Archived ${total} ${runWord(total)}.` }); + } else if (succeeded === 0) { + push({ + message: `Couldn't archive ${total} ${runWord(total)}. Try again.`, + tone: "error", + }); + } else { + push({ + message: `Archived ${succeeded} of ${total} runs. ${failed} failed.`, + tone: "error", + }); + } + } finally { + setPending(false); + void mutate(queryKeys.boards.runs()); + } + } + + const label = pending + ? `Archiving ${archivable.length}…` + : `Archive all`; + + return ( +
+ ); +} + function BoardColumn({ column }: { column: Column }) { const Icon = iconMap[column.iconType]; const actions = column.actions; @@ -474,6 +558,7 @@ function BoardColumn({ column }: { column: Column }) { {column.items.length} +No runs
- ))} -