diff --git a/apps/arc-web/app/routes/pipelines.tsx b/apps/arc-web/app/routes/pipelines.tsx index fe75c5649..e9bbd35f3 100644 --- a/apps/arc-web/app/routes/pipelines.tsx +++ b/apps/arc-web/app/routes/pipelines.tsx @@ -1,7 +1,24 @@ -import { useState } from "react"; +import { useState, useCallback, useRef } from "react"; import { Link } from "react-router"; import { ChevronDownIcon, ChevronRightIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import { columns, ciConfig, statusColors } from "../data/runs"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import type { DragEndEvent } from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, + arrayMove, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { columns as staticColumns, ciConfig, statusColors } from "../data/runs"; import type { CiStatus, RunItem, RunWithStatus } from "../data/runs"; import type { Route } from "./+types/pipelines"; @@ -51,8 +68,8 @@ function CiBadge({ status }: { status: CiStatus }) { ); } -const totalCards = columns.reduce((sum, col) => sum + col.items.length, 0); -const totalPrs = columns.reduce( +const totalCards = staticColumns.reduce((sum, col) => sum + col.items.length, 0); +const totalPrs = staticColumns.reduce( (sum, col) => sum + col.items.filter((item) => item.number != null).length, 0, ); @@ -179,7 +196,47 @@ function PrCard({ ); } -function BoardColumn({ column }: { column: (typeof columns)[number] }) { +function SortablePrCard({ + pr, + icon, + iconColor, + actions, +}: { + pr: RunItem; + icon: React.ComponentType<{ className?: string }>; + iconColor: string; + actions?: string[]; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: pr.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, + }; + return ( +
{ + if (wasDragging.current) { + e.preventDefault(); + e.stopPropagation(); + wasDragging.current = false; + } + }} + > + +
+ ); +} + +function BoardColumn({ column }: { column: (typeof staticColumns)[number] }) { const Icon = iconMap[column.iconType]; return (
@@ -193,17 +250,19 @@ function BoardColumn({ column }: { column: (typeof columns)[number] }) {
-
- {column.items.map((pr) => ( - - ))} -
+ pr.id)} strategy={verticalListSortingStrategy}> +
+ {column.items.map((pr) => ( + + ))} +
+
); } @@ -248,15 +307,68 @@ function RunRow({ run }: { run: RunWithStatus }) { ); } -const allRepos = [...new Set(columns.flatMap((col) => col.items.map((item) => item.repo)))].sort(); +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; + } + }} + > + +
+ ); +} + +const allRepos = [...new Set(staticColumns.flatMap((col) => col.items.map((item) => item.repo)))].sort(); export default function Pipelines() { const [query, setQuery] = useState(""); const [repoFilter, setRepoFilter] = useState("all"); const [view, setView] = useState("columns"); const [collapsed, setCollapsed] = useState>(new Set()); + const [columns, setColumns] = useState(staticColumns); const lowerQuery = query.toLowerCase(); + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), + ); + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + setColumns((prev) => + prev.map((col) => { + const oldIndex = col.items.findIndex((item) => item.id === active.id); + const newIndex = col.items.findIndex((item) => item.id === over.id); + if (oldIndex === -1 || newIndex === -1) return col; + return { ...col, items: arrayMove(col.items, oldIndex, newIndex) }; + }), + ); + }, []); + const filteredColumns = columns.map((col) => ({ ...col, items: col.items.filter( @@ -270,100 +382,104 @@ export default function Pipelines() { })); return ( -
-
-
- - setQuery(e.target.value)} - className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0" - /> + +
+
+
+ + setQuery(e.target.value)} + className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0" + /> +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- {view === "columns" ? ( -
- {filteredColumns.map((col) => ( - - ))} -
- ) : ( -
- {filteredColumns.map((col) => { - const isCollapsed = collapsed.has(col.id); - return ( -
- - {!isCollapsed && (col.items.length > 0 ? ( -
- {col.items.map((item) => ( - - ))} -
- ) : ( -

No runs

- ))} -
- ); - })} -
- )} -
+ {view === "columns" ? ( +
+ {filteredColumns.map((col) => ( + + ))} +
+ ) : ( +
+ {filteredColumns.map((col) => { + const isCollapsed = collapsed.has(col.id); + return ( +
+ + {!isCollapsed && (col.items.length > 0 ? ( + item.id)} strategy={verticalListSortingStrategy}> +
+ {col.items.map((item) => ( + + ))} +
+
+ ) : ( +

No runs

+ ))} +
+ ); + })} +
+ )} +
+ ); } diff --git a/apps/arc-web/bun.lock b/apps/arc-web/bun.lock index 07e08a353..84e0f5dc7 100644 --- a/apps/arc-web/bun.lock +++ b/apps/arc-web/bun.lock @@ -5,6 +5,9 @@ "": { "name": "arc-web", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@pierre/diffs": "^1.0.11", @@ -87,6 +90,14 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], diff --git a/apps/arc-web/package.json b/apps/arc-web/package.json index 1fb7c6cf1..e19f77b4a 100644 --- a/apps/arc-web/package.json +++ b/apps/arc-web/package.json @@ -9,6 +9,9 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@pierre/diffs": "^1.0.11",