Merge branch 'main' into cursor/bullmq-queue-phase-a-3038
This commit is contained in:
@@ -21,7 +21,7 @@ export function EventsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-semibold">Events</h1>
|
||||
<h1 className="text-xl font-semibold">{status === "failed" ? "Failed events" : "Events"}</h1>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
className="input input-sm w-full sm:max-w-sm"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useConsecutiveFailures } from "../api/hooks.js";
|
||||
import { formatTime } from "../lib/format.jsx";
|
||||
|
||||
export function FailuresPage() {
|
||||
const { data, isLoading, error } = useConsecutiveFailures();
|
||||
const items = data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-semibold">Failures</h1>
|
||||
<p className="opacity-70 text-sm">
|
||||
Workflows that have failed 4 or more times in a row for the same trigger.
|
||||
</p>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner" />
|
||||
) : error ? (
|
||||
<p className="text-error">Failed to load consecutive failures</p>
|
||||
) : items.length === 0 ? (
|
||||
<p className="opacity-60 text-sm">None</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Streak</th>
|
||||
<th>Workflow</th>
|
||||
<th>Trigger</th>
|
||||
<th>Last error</th>
|
||||
<th>Last failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((s) => (
|
||||
<tr
|
||||
key={`${s.workflow}\0${s.trigger_type}\0${s.trigger_detail ?? ""}`}
|
||||
className="hover"
|
||||
>
|
||||
<td>
|
||||
<span className="badge badge-error badge-sm">{s.consecutiveFailures}</span>
|
||||
</td>
|
||||
<td>
|
||||
<Link className="link" to={`/events/${s.lastRun.id}`}>
|
||||
{s.workflow_name || s.workflow}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="text-xs">
|
||||
{s.trigger_type}
|
||||
{s.trigger_detail ? ` · ${s.trigger_detail}` : ""}
|
||||
</td>
|
||||
<td className="max-w-md truncate text-xs" title={s.lastRun.error ?? ""}>
|
||||
{s.lastRun.error || "—"}
|
||||
</td>
|
||||
<td className="whitespace-nowrap">{formatTime(s.lastRun.started_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,8 +18,10 @@ export function HomePage() {
|
||||
return <p className="text-error">Failed to load dashboard</p>;
|
||||
}
|
||||
|
||||
const failed = data.needsAttention?.failed ?? [];
|
||||
const streaks = data.needsAttention?.consecutiveFailures ?? [];
|
||||
const streakCount = data.needsAttention?.consecutiveFailureCount ?? streaks.length;
|
||||
const broken = data.needsAttention?.brokenWorkflows ?? [];
|
||||
const failedEvents = data.failedEvents ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -51,9 +53,7 @@ export function HomePage() {
|
||||
<LuTriangleAlert className="size-7" />
|
||||
</div>
|
||||
<div className="stat-title">Attention</div>
|
||||
<div className="stat-value text-2xl">
|
||||
{failed.length + broken.length}
|
||||
</div>
|
||||
<div className="stat-value text-2xl">{streakCount + broken.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,29 +62,80 @@ export function HomePage() {
|
||||
<RunList runs={data.running} empty="None" />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-2">Needs attention</h2>
|
||||
{broken.length > 0 ? (
|
||||
<ul className="mb-3 space-y-1">
|
||||
{broken.map((w) => (
|
||||
<li key={w.key}>
|
||||
<Link
|
||||
className="link link-error"
|
||||
to={`/workflows/${encodeURIComponent(w.owner)}/${encodeURIComponent(w.file)}/edit`}
|
||||
>
|
||||
{w.key}: {w.loadError}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<RunList runs={failed} empty={broken.length ? "" : "None"} />
|
||||
</section>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<section className="min-w-0">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-2">
|
||||
<h2 className="text-lg font-semibold">Needs attention</h2>
|
||||
<Link className="link link-hover text-sm" to="/failures">
|
||||
Show all
|
||||
</Link>
|
||||
</div>
|
||||
{broken.length > 0 ? (
|
||||
<ul className="mb-3 space-y-1">
|
||||
{broken.map((w) => (
|
||||
<li key={w.key}>
|
||||
<Link
|
||||
className="link link-error"
|
||||
to={`/workflows/${encodeURIComponent(w.owner)}/${encodeURIComponent(w.file)}/edit`}
|
||||
>
|
||||
{w.key}: {w.loadError}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<StreakList streaks={streaks} empty={broken.length ? "" : "None"} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-2">Recent</h2>
|
||||
<RunList runs={data.recent} empty="None" />
|
||||
</section>
|
||||
<section className="min-w-0">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-2">
|
||||
<h2 className="text-lg font-semibold">Failed events</h2>
|
||||
<Link className="link link-hover text-sm" to="/events?status=failed">
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
<RunList runs={failedEvents} empty="None" />
|
||||
</section>
|
||||
|
||||
<section className="min-w-0">
|
||||
<h2 className="text-lg font-semibold mb-2">Recent</h2>
|
||||
<RunList runs={data.recent} empty="None" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StreakList({ streaks, empty }) {
|
||||
if (!streaks?.length) {
|
||||
return empty ? <p className="opacity-60 text-sm">{empty}</p> : null;
|
||||
}
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Streak</th>
|
||||
<th>Workflow</th>
|
||||
<th>Last failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{streaks.map((s) => (
|
||||
<tr key={`${s.workflow}\0${s.trigger_type}\0${s.trigger_detail ?? ""}`} className="hover">
|
||||
<td>
|
||||
<span className="badge badge-error badge-sm">{s.consecutiveFailures}</span>
|
||||
</td>
|
||||
<td>
|
||||
<Link className="link" to={`/events/${s.lastRun.id}`}>
|
||||
{s.workflow_name || s.workflow}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="whitespace-nowrap">{formatTime(s.lastRun.started_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,11 +180,11 @@ export function SecretsPage() {
|
||||
value={form.value}
|
||||
onChange={(e) => setForm({ ...form, value: e.target.value })}
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<p className="text-xs opacity-60 mt-1">
|
||||
Values are encrypted at rest and never shown again after save.
|
||||
Values shorter than 8 characters are not redacted from logs.
|
||||
</p>
|
||||
{upsert.isError ? (
|
||||
<p className="text-error text-sm">{errorMessage(upsert.error)}</p>
|
||||
|
||||
Reference in New Issue
Block a user