loops wizard: expose 'until' repeat policy
Third radio option on the repeat step. Two inputs: event name (defaults
to 'ok') and a within_iters cap (defaults to 20). Ships the full
{kind: 'until', event, within_iters} payload the schema already accepts.
LoopsCanvas repeat summary now formats iters/until/infinite via one
helper — the previous inline expression was rendering 'until' as a bare
label with no event context.
This commit is contained in:
@@ -14,9 +14,20 @@ import {
|
|||||||
listLoopRuns,
|
listLoopRuns,
|
||||||
runLoopNow,
|
runLoopNow,
|
||||||
type Loop,
|
type Loop,
|
||||||
|
type LoopRepeatPolicy,
|
||||||
type RunSummary,
|
type RunSummary,
|
||||||
} from "@/lib/api/loops";
|
} from "@/lib/api/loops";
|
||||||
|
|
||||||
|
function formatRepeatPolicy(policy: LoopRepeatPolicy | undefined): string {
|
||||||
|
if (!policy) return "infinite";
|
||||||
|
if (policy.kind === "iters") return `${policy.n} iterations`;
|
||||||
|
if (policy.kind === "until") {
|
||||||
|
const cap = policy.within_iters ? ` · cap ${policy.within_iters}` : "";
|
||||||
|
return `until "${policy.event}"${cap}`;
|
||||||
|
}
|
||||||
|
return "infinite";
|
||||||
|
}
|
||||||
|
|
||||||
const mono =
|
const mono =
|
||||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
|
|
||||||
@@ -199,11 +210,7 @@ export function LoopsCanvas({
|
|||||||
</Section>
|
</Section>
|
||||||
<Section header="Repeat">
|
<Section header="Repeat">
|
||||||
<div style={{ fontFamily: mono, fontSize: 12.5, color: "#eaeaee" }}>
|
<div style={{ fontFamily: mono, fontSize: 12.5, color: "#eaeaee" }}>
|
||||||
{loop.repeat_policy?.kind === "iters"
|
{formatRepeatPolicy(loop.repeat_policy)}
|
||||||
? `${(loop.repeat_policy as { kind: "iters"; n: number }).n} iterations`
|
|
||||||
: loop.repeat_policy?.kind === "until"
|
|
||||||
? "until"
|
|
||||||
: "infinite"}
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,8 +36,12 @@ export function LoopsWizard({
|
|||||||
const [cron, setCron] = useState("0 */6 * * *");
|
const [cron, setCron] = useState("0 */6 * * *");
|
||||||
const [onCompletion, setOnCompletion] = useState(false);
|
const [onCompletion, setOnCompletion] = useState(false);
|
||||||
const [webhookEnabled, setWebhookEnabled] = useState(false);
|
const [webhookEnabled, setWebhookEnabled] = useState(false);
|
||||||
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters">("infinite");
|
const [repeatKind, setRepeatKind] = useState<"infinite" | "iters" | "until">(
|
||||||
|
"infinite",
|
||||||
|
);
|
||||||
const [iters, setIters] = useState(10);
|
const [iters, setIters] = useState(10);
|
||||||
|
const [untilEvent, setUntilEvent] = useState("ok");
|
||||||
|
const [untilWithin, setUntilWithin] = useState(20);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
const [created, setCreated] = useState<LoopCreated | null>(null);
|
const [created, setCreated] = useState<LoopCreated | null>(null);
|
||||||
@@ -60,10 +64,24 @@ export function LoopsWizard({
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const repeat_policy: LoopRepeatPolicy =
|
let repeat_policy: LoopRepeatPolicy;
|
||||||
repeatKind === "infinite"
|
if (repeatKind === "infinite") {
|
||||||
? { kind: "infinite" }
|
repeat_policy = { kind: "infinite" };
|
||||||
: { kind: "iters", n: Math.max(1, Math.floor(iters)) };
|
} else if (repeatKind === "iters") {
|
||||||
|
repeat_policy = { kind: "iters", n: Math.max(1, Math.floor(iters)) };
|
||||||
|
} else {
|
||||||
|
const event = untilEvent.trim();
|
||||||
|
if (!event) {
|
||||||
|
setSubmitError("Until: event name is required");
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repeat_policy = {
|
||||||
|
kind: "until",
|
||||||
|
event,
|
||||||
|
within_iters: Math.max(1, Math.floor(untilWithin)),
|
||||||
|
};
|
||||||
|
}
|
||||||
const out = await createLoop({
|
const out = await createLoop({
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
@@ -260,6 +278,69 @@ export function LoopsWizard({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
<label style={radioRowStyle(repeatKind === "until")}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={repeatKind === "until"}
|
||||||
|
onChange={() => setRepeatKind("until")}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontWeight: 600, color: "#f3f3f5" }}>Until event</div>
|
||||||
|
<div style={hintStyle}>
|
||||||
|
Stop the first time an iteration emits the named run event.
|
||||||
|
Give up after the iteration cap regardless.
|
||||||
|
</div>
|
||||||
|
{repeatKind === "until" && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 120px",
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ ...hintStyle, fontSize: 10, letterSpacing: ".1em", textTransform: "uppercase" }}>
|
||||||
|
Event
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={untilEvent}
|
||||||
|
placeholder="ok"
|
||||||
|
onChange={(e) => setUntilEvent(e.target.value)}
|
||||||
|
style={fieldStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ ...hintStyle, fontSize: 10, letterSpacing: ".1em", textTransform: "uppercase" }}>
|
||||||
|
Cap
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={untilWithin}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
setUntilWithin(parseInt(e.target.value, 10) || 1)
|
||||||
|
}
|
||||||
|
style={fieldStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
{submitError && (
|
{submitError && (
|
||||||
<p style={{ fontFamily: mono, fontSize: 12, color: "#ff8a7a" }}>
|
<p style={{ fontFamily: mono, fontSize: 12, color: "#ff8a7a" }}>
|
||||||
{submitError}
|
{submitError}
|
||||||
|
|||||||
Reference in New Issue
Block a user