Files
jerapah-flow/packages/web/src/pages/EventsPage.jsx
T
nsrbandCursor db884808f2 refactor: split oversized modules and start server src/stores layout
Consolidate format helpers under lib/format, split React Query hooks by domain, extract AuthPicker/ConfigRefHint and HTTP trigger routing, demote file-private exports, and move KV/secrets/variables/profiles/http-auths stores under src/stores with root re-exports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 06:16:38 +07:00

246 lines
7.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Link, useSearchParams } from "react-router-dom";
import { useOwners, useRuns } from "../api/hooks.js";
import { formatTime, StatusBadge } from "../lib/format";
const PAGE_SIZES = [25, 50, 100];
const DEFAULT_LIMIT = 50;
const TRIGGER_TYPES = ["HTTP", "cron", "manual"];
const SORT_COLUMNS = [
{ key: "status", label: "Status" },
{ key: "workflow", label: "Workflow" },
{ key: "revision", label: "Revision" },
{ key: "trigger", label: "Trigger" },
{ key: "started_at", label: "Started" },
{ key: "duration", label: "Duration" },
];
function dateToAfterIso(date) {
if (!date) return undefined;
return `${date}T00:00:00.000Z`;
}
function dateToBeforeIso(date) {
if (!date) return undefined;
const d = new Date(`${date}T00:00:00.000Z`);
d.setUTCDate(d.getUTCDate() + 1);
return d.toISOString();
}
function SortHeader({ column, label, sort, order, onSort }) {
const active = sort === column;
return (
<th>
<button
type="button"
className={`font-semibold hover:underline ${active ? "" : "opacity-80"}`}
onClick={() => onSort(column)}
>
{label}
{active ? (order === "asc" ? " ↑" : " ↓") : ""}
</button>
</th>
);
}
export function EventsPage() {
const [params, setParams] = useSearchParams();
const workflow = params.get("workflow") || "";
const status = params.get("status") || "";
const owner = params.get("owner") || "";
const trigger = params.get("trigger") || "";
const afterDate = params.get("after") || "";
const beforeDate = params.get("before") || "";
const offset = Math.max(Number(params.get("offset")) || 0, 0);
const limitParam = Number(params.get("limit"));
const limit = PAGE_SIZES.includes(limitParam) ? limitParam : DEFAULT_LIMIT;
const sort = params.get("sort") || "started_at";
const order = params.get("order") === "asc" ? "asc" : "desc";
const { data: owners = [] } = useOwners();
const { data, isLoading } = useRuns({
workflow: workflow || undefined,
status: status || undefined,
owner: owner || undefined,
trigger: trigger || undefined,
after: dateToAfterIso(afterDate),
before: dateToBeforeIso(beforeDate),
limit,
offset,
sort,
order,
});
const runs = data?.runs ?? [];
const total = data?.total ?? 0;
function update(key, value, resetOffset = true) {
const next = new URLSearchParams(params);
if (value) next.set(key, value);
else next.delete(key);
if (resetOffset && key !== "offset") next.delete("offset");
setParams(next);
}
function setSort(column) {
const next = new URLSearchParams(params);
if (sort === column) {
next.set("order", order === "asc" ? "desc" : "asc");
} else {
next.set("sort", column);
next.set("order", "desc");
}
next.delete("offset");
setParams(next);
}
return (
<div className="space-y-4">
<h1 className="text-xl font-semibold">{status === "failed" ? "Failed events" : "Events"}</h1>
<div className="flex flex-col sm:flex-row flex-wrap gap-2">
<input
className="input input-sm w-full sm:max-w-sm"
placeholder="workflow key (* wildcard)"
value={workflow}
onChange={(e) => update("workflow", e.target.value)}
/>
<select
className="select select-sm w-full sm:max-w-xs"
value={status}
onChange={(e) => update("status", e.target.value)}
>
<option value="">all statuses</option>
<option value="queued">queued</option>
<option value="running">running</option>
<option value="success">success</option>
<option value="failed">failed</option>
<option value="skipped">skipped</option>
</select>
<select
className="select select-sm w-full sm:max-w-xs"
value={owner}
onChange={(e) => update("owner", e.target.value)}
>
<option value="">all owners</option>
{owners.map((o) => (
<option key={o} value={o}>
{o}
</option>
))}
</select>
<select
className="select select-sm w-full sm:max-w-xs"
value={trigger}
onChange={(e) => update("trigger", e.target.value)}
>
<option value="">all triggers</option>
{TRIGGER_TYPES.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<label className="flex items-center gap-1 text-sm">
<span className="opacity-70 whitespace-nowrap">from</span>
<input
type="date"
className="input input-sm"
value={afterDate}
onChange={(e) => update("after", e.target.value)}
/>
</label>
<label className="flex items-center gap-1 text-sm">
<span className="opacity-70 whitespace-nowrap">to</span>
<input
type="date"
className="input input-sm"
value={beforeDate}
onChange={(e) => update("before", e.target.value)}
/>
</label>
<select
className="select select-sm w-full sm:max-w-[8rem]"
value={String(limit)}
onChange={(e) => update("limit", e.target.value)}
>
{PAGE_SIZES.map((size) => (
<option key={size} value={size}>
{size} / page
</option>
))}
</select>
</div>
{isLoading ? (
<span className="loading loading-spinner" />
) : runs.length === 0 ? (
<p className="text-sm opacity-60">No events match these filters.</p>
) : (
<>
<div className="overflow-x-auto">
<table className="table table-sm">
<thead>
<tr>
{SORT_COLUMNS.map(({ key, label }) => (
<SortHeader
key={key}
column={key}
label={label}
sort={sort}
order={order}
onSort={setSort}
/>
))}
</tr>
</thead>
<tbody>
{runs.map((r) => (
<tr key={r.id} className="hover">
<td>
<StatusBadge status={r.status} />
</td>
<td>
<Link className="link" to={`/events/${r.id}`}>
{r.workflow_name || r.workflow}
</Link>
</td>
<td className="text-xs opacity-70">
{r.workflow_revision != null ? `#${r.workflow_revision}` : "unknown"}
</td>
<td className="text-xs">
{r.trigger_type}
{r.trigger_detail ? ` · ${r.trigger_detail}` : ""}
</td>
<td className="whitespace-nowrap">{formatTime(r.started_at)}</td>
<td>{r.duration_ms != null ? `${r.duration_ms}ms` : "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center gap-2 text-sm">
<button
type="button"
className="btn btn-sm"
disabled={offset <= 0}
onClick={() => update("offset", String(Math.max(offset - limit, 0)), false)}
>
Prev
</button>
<button
type="button"
className="btn btn-sm"
disabled={offset + runs.length >= total}
onClick={() => update("offset", String(offset + limit), false)}
>
Next
</button>
<span className="opacity-60">
{total === 0 ? "0" : `${offset + 1}–${offset + runs.length}`} of {total}
</span>
</div>
</>
)}
</div>
);
}