import { useState } from "react"; import { Link, Navigate, useNavigate, useSearchParams } from "react-router-dom"; import { LuActivity, LuCopy, LuPencil, LuPlay, LuPlus, LuRefreshCw, LuTrash2, LuTriangleAlert } from "react-icons/lu"; import { errorMessage } from "../api/client.js"; import { useDeleteWorkflow, useReregisterWorkflows, useRunWorkflow, useSetWorkflowEnabled, useWorkflows, } from "../api/hooks.js"; import { DuplicateWorkflowDialog } from "../components/DuplicateWorkflowDialog.jsx"; import { ConfirmDialog } from "../components/ConfirmDialog.jsx"; import { WorkflowFileIcon } from "../components/WorkflowFileIcon.jsx"; import { formatTime, WorkflowStatusBadge } from "../lib/format"; const SORT_COLUMNS = [ { key: "name", label: "Name", defaultOrder: "asc" }, { key: "owner", label: "Owner", defaultOrder: "asc" }, { key: "status", label: "Status", defaultOrder: "asc" }, { key: "lastModifiedAt", label: "Last modified", defaultOrder: "desc" }, { key: "lastInvokedAt", label: "Last run", defaultOrder: "desc" }, { key: "invocationCount", label: "Runs", defaultOrder: "desc" }, ]; function SortHeader({ column, label, sort, order, onSort }) { const active = sort === column; return ( ); } function statusSortKey(w) { if (w.loadError) return "0-broken"; if (!w.enabled) return "1-disabled"; if (w.lastStatus === "failed") return "2-failed"; if (w.lastStatus === "queued") return "3-queued"; if (w.lastStatus === "running") return "4-running"; if (w.lastStatus === "success") return "5-working"; return "6-never"; } function compareName(a, b) { const byName = String(a.name ?? "").localeCompare(String(b.name ?? ""), undefined, { sensitivity: "base", }); if (byName !== 0) return byName; return String(a.key ?? "").localeCompare(String(b.key ?? "")); } function compareWorkflows(a, b, sort) { if (sort === "name") return compareName(a, b); if (sort === "owner") { const byOwner = String(a.owner ?? "").localeCompare(String(b.owner ?? ""), undefined, { sensitivity: "base", }); return byOwner !== 0 ? byOwner : compareName(a, b); } if (sort === "status") { const byStatus = statusSortKey(a).localeCompare(statusSortKey(b)); return byStatus !== 0 ? byStatus : compareName(a, b); } if (sort === "invocationCount") { const byCount = (Number(a.invocationCount) || 0) - (Number(b.invocationCount) || 0); return byCount !== 0 ? byCount : compareName(a, b); } const aTime = a[sort] ?? ""; const bTime = b[sort] ?? ""; const byTime = String(aTime).localeCompare(String(bTime)); return byTime !== 0 ? byTime : compareName(a, b); } export function WorkflowsPage() { const navigate = useNavigate(); const [params] = useSearchParams(); const editParam = params.get("edit"); const { data: workflows = [], isLoading } = useWorkflows(); const [sort, setSortColumn] = useState("name"); const [order, setOrder] = useState("asc"); const [confirmDelete, setConfirmDelete] = useState(null); const [duplicateSource, setDuplicateSource] = useState(null); const [runError, setRunError] = useState(null); const del = useDeleteWorkflow(); const run = useRunWorkflow(); const setEnabled = useSetWorkflowEnabled(); const reregister = useReregisterWorkflows(); if (editParam) { const slash = editParam.indexOf("/"); if (slash !== -1) { const owner = editParam.slice(0, slash); const file = editParam.slice(slash + 1); return ( ); } } function onRun(w) { setRunError(null); run.mutate( { owner: w.owner, file: w.file }, { onSuccess: (data) => { if (data?.runId) navigate(`/events/${data.runId}`); }, onError: (err) => { const runId = err?.response?.data?.runId; if (runId) { navigate(`/events/${runId}`); return; } setRunError(`${w.key}: ${errorMessage(err)}`); }, }, ); } const runningKey = run.isPending && run.variables ? `${run.variables.owner}/${run.variables.file}` : null; const togglingKey = setEnabled.isPending && setEnabled.variables ? `${setEnabled.variables.owner}/${setEnabled.variables.file}` : null; const dir = order === "desc" ? -1 : 1; const sortedWorkflows = [...workflows].sort((a, b) => dir * compareWorkflows(a, b, sort)); function setSort(column) { const spec = SORT_COLUMNS.find((c) => c.key === column); if (sort === column) { setOrder((prev) => (prev === "asc" ? "desc" : "asc")); return; } setSortColumn(column); setOrder(spec?.defaultOrder ?? "asc"); } return (

Workflows

Trash Add
{reregister.isError ? (

{errorMessage(reregister.error)}

) : null} {runError ?

{runError}

: null} {setEnabled.isError ? (

{errorMessage(setEnabled.error)}

) : null} {isLoading ? ( ) : (
{sortedWorkflows.map((w) => ( ))}
Triggers
{w.name} {w.file} {!w.registered ? ( ) : null} {w.owner} {formatTime(w.lastModifiedAt)} {formatTime(w.lastInvokedAt)} {w.invocationCount}
)} {duplicateSource ? ( setDuplicateSource(null)} onDuplicated={(data) => { setDuplicateSource(null); navigate( `/workflows/${encodeURIComponent(data.owner)}/${encodeURIComponent(data.file)}/edit`, ); }} /> ) : null} setConfirmDelete(null)} onConfirm={() => del.mutate( { owner: confirmDelete.owner, file: confirmDelete.file }, { onSuccess: () => setConfirmDelete(null) }, ) } />
); } function triggerLabel(t) { const type = String(t?.type ?? "").toLowerCase(); if (type === "cron") return t.schedule || "cron"; if (type === "http") return t.path || "/"; if (type === "workflow") return ""; return t?.type ?? "—"; } function triggerKind(t) { const type = String(t?.type ?? "").toLowerCase(); if (type === "http") return t.method || "POST"; if (type === "workflow") return "workflow"; return t?.type ?? "—"; } function TriggerList({ triggers }) { if (!triggers?.length) return —; return ( ); }