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: "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 === "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 ? (
) : (
)}
{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 (
{triggers.map((t, i) => (
-
{triggerKind(t)} {triggerLabel(t)}
))}
);
}