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} + pr.id)} strategy={verticalListSortingStrategy}> @@ -497,9 +582,15 @@ type ViewMode = "columns" | "list"; function RunRow({ run }: { run: RunWithStatus }) { const lifecycleLabel = listLifecycleStatusLabel(run); + const statusDisplay = columnStatusDisplay[run.status]; return ( + + + {run.elapsed} @@ -540,39 +631,6 @@ function RunRow({ run }: { run: RunWithStatus }) { ); } -function SortableRunRow({ run }: { run: RunWithStatus }) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: run.id }); - const wasDragging = useRef(false); - if (isDragging) wasDragging.current = true; - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : undefined, - position: "relative" as const, - zIndex: isDragging ? 10 : undefined, - gridColumn: "1 / -1", - display: "grid", - gridTemplateColumns: "subgrid", - }; - return ( -
{ - if (wasDragging.current) { - e.preventDefault(); - e.stopPropagation(); - wasDragging.current = false; - } - }} - > - -
- ); -} - function TerminalLine({ prompt, command }: { prompt: string; command: string }) { return (
@@ -713,7 +771,6 @@ export default function Runs() { const [query, setQuery] = useState(""); const [repoFilter, setRepoFilter] = useState("all"); const [view, setView] = useState("columns"); - const [collapsed, setCollapsed] = useState>(new Set()); const [columns, setColumns] = useState(initialColumns); const lowerQuery = query.toLowerCase(); useBoardEvents(); @@ -845,45 +902,15 @@ export default function Runs() { ) : ( <> -
- {visibleColumns.map((col) => { - const isCollapsed = collapsed.has(col.id); - return ( -
- - {!isCollapsed && (col.items.length > 0 ? ( - item.id)} strategy={verticalListSortingStrategy}> -
- {col.items.map((item) => ( - - ))} -
-
- ) : ( -

No runs

- ))} -
- ); - })} -
+ {filteredRuns > 0 && ( +
+ {visibleColumns.flatMap((col) => + col.items.map((item) => ( + + )), + )} +
+ )} {isLandingReady && totalRuns === 0 ? (