Simplify routine configuration UI
- Add "Every minute" schedule preset as finest granularity - Remove status and priority from advanced delivery settings - Auto-generate trigger labels from kind instead of manual input Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -4,9 +4,10 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
type SchedulePreset = "every_hour" | "every_day" | "weekdays" | "weekly" | "monthly" | "custom";
|
type SchedulePreset = "every_minute" | "every_hour" | "every_day" | "weekdays" | "weekly" | "monthly" | "custom";
|
||||||
|
|
||||||
const PRESETS: { value: SchedulePreset; label: string }[] = [
|
const PRESETS: { value: SchedulePreset; label: string }[] = [
|
||||||
|
{ value: "every_minute", label: "Every minute" },
|
||||||
{ value: "every_hour", label: "Every hour" },
|
{ value: "every_hour", label: "Every hour" },
|
||||||
{ value: "every_day", label: "Every day" },
|
{ value: "every_day", label: "Every day" },
|
||||||
{ value: "weekdays", label: "Weekdays" },
|
{ value: "weekdays", label: "Weekdays" },
|
||||||
@@ -60,6 +61,11 @@ function parseCronToPreset(cron: string): {
|
|||||||
|
|
||||||
const [min, hr, dom, , dow] = parts;
|
const [min, hr, dom, , dow] = parts;
|
||||||
|
|
||||||
|
// Every minute: "* * * * *"
|
||||||
|
if (min === "*" && hr === "*" && dom === "*" && dow === "*") {
|
||||||
|
return { preset: "every_minute", ...defaults };
|
||||||
|
}
|
||||||
|
|
||||||
// Every hour: "0 * * * *"
|
// Every hour: "0 * * * *"
|
||||||
if (hr === "*" && dom === "*" && dow === "*") {
|
if (hr === "*" && dom === "*" && dow === "*") {
|
||||||
return { preset: "every_hour", ...defaults, minute: min === "*" ? "0" : min };
|
return { preset: "every_hour", ...defaults, minute: min === "*" ? "0" : min };
|
||||||
@@ -90,6 +96,8 @@ function parseCronToPreset(cron: string): {
|
|||||||
|
|
||||||
function buildCron(preset: SchedulePreset, hour: string, minute: string, dayOfWeek: string, dayOfMonth: string): string {
|
function buildCron(preset: SchedulePreset, hour: string, minute: string, dayOfWeek: string, dayOfMonth: string): string {
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
|
case "every_minute":
|
||||||
|
return "* * * * *";
|
||||||
case "every_hour":
|
case "every_hour":
|
||||||
return `${minute} * * * *`;
|
return `${minute} * * * *`;
|
||||||
case "every_day":
|
case "every_day":
|
||||||
@@ -110,6 +118,8 @@ function describeSchedule(cron: string): string {
|
|||||||
const timeStr = HOURS.find((h) => h.value === hour)?.label?.replace(":00", `:${minute.padStart(2, "0")}`) ?? `${hour}:${minute.padStart(2, "0")}`;
|
const timeStr = HOURS.find((h) => h.value === hour)?.label?.replace(":00", `:${minute.padStart(2, "0")}`) ?? `${hour}:${minute.padStart(2, "0")}`;
|
||||||
|
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
|
case "every_minute":
|
||||||
|
return "Every minute";
|
||||||
case "every_hour":
|
case "every_hour":
|
||||||
return `Every hour at :${minute.padStart(2, "0")}`;
|
return `Every hour at :${minute.padStart(2, "0")}`;
|
||||||
case "every_day":
|
case "every_day":
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import type { RoutineTrigger } from "@paperclipai/shared";
|
import type { RoutineTrigger } from "@paperclipai/shared";
|
||||||
|
|
||||||
const priorities = ["critical", "high", "medium", "low"];
|
|
||||||
const routineStatuses = ["active", "paused", "archived"];
|
|
||||||
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
||||||
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
||||||
const triggerKinds = ["schedule", "webhook", "api"];
|
const triggerKinds = ["schedule", "webhook", "api"];
|
||||||
@@ -266,7 +264,6 @@ export function RoutineDetail() {
|
|||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||||
const [newTrigger, setNewTrigger] = useState({
|
const [newTrigger, setNewTrigger] = useState({
|
||||||
kind: "schedule",
|
kind: "schedule",
|
||||||
label: "",
|
|
||||||
cronExpression: "0 10 * * *",
|
cronExpression: "0 10 * * *",
|
||||||
signingMode: "bearer",
|
signingMode: "bearer",
|
||||||
replayWindowSec: "300",
|
replayWindowSec: "300",
|
||||||
@@ -466,10 +463,12 @@ export function RoutineDetail() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createTrigger = useMutation({
|
const createTrigger = useMutation({
|
||||||
mutationFn: async (): Promise<RoutineTriggerResponse> =>
|
mutationFn: async (): Promise<RoutineTriggerResponse> => {
|
||||||
routinesApi.createTrigger(routineId!, {
|
const existingOfKind = (routine?.triggers ?? []).filter((t) => t.kind === newTrigger.kind).length;
|
||||||
|
const autoLabel = existingOfKind > 0 ? `${newTrigger.kind}-${existingOfKind + 1}` : newTrigger.kind;
|
||||||
|
return routinesApi.createTrigger(routineId!, {
|
||||||
kind: newTrigger.kind,
|
kind: newTrigger.kind,
|
||||||
label: newTrigger.label.trim() || null,
|
label: autoLabel,
|
||||||
...(newTrigger.kind === "schedule"
|
...(newTrigger.kind === "schedule"
|
||||||
? { cronExpression: newTrigger.cronExpression.trim(), timezone: getLocalTimezone() }
|
? { cronExpression: newTrigger.cronExpression.trim(), timezone: getLocalTimezone() }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -479,7 +478,8 @@ export function RoutineDetail() {
|
|||||||
replayWindowSec: Number(newTrigger.replayWindowSec || "300"),
|
replayWindowSec: Number(newTrigger.replayWindowSec || "300"),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
onSuccess: async (result) => {
|
onSuccess: async (result) => {
|
||||||
if (result.secretMaterial) {
|
if (result.secretMaterial) {
|
||||||
setSecretMessage({
|
setSecretMessage({
|
||||||
@@ -787,33 +787,7 @@ export function RoutineDetail() {
|
|||||||
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="pt-3">
|
<CollapsibleContent className="pt-3">
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Status</p>
|
|
||||||
<Select value={editDraft.status} onValueChange={(status) => setEditDraft((current) => ({ ...current, status }))}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{routineStatuses.map((status) => (
|
|
||||||
<SelectItem key={status} value={status}>{status.replaceAll("_", " ")}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Priority</p>
|
|
||||||
<Select value={editDraft.priority} onValueChange={(priority) => setEditDraft((current) => ({ ...current, priority }))}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{priorities.map((priority) => (
|
|
||||||
<SelectItem key={priority} value={priority}>{priority.replaceAll("_", " ")}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
||||||
<Select
|
<Select
|
||||||
@@ -909,10 +883,6 @@ export function RoutineDetail() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label className="text-xs">Label</Label>
|
|
||||||
<Input value={newTrigger.label} onChange={(event) => setNewTrigger((current) => ({ ...current, label: event.target.value }))} />
|
|
||||||
</div>
|
|
||||||
{newTrigger.kind === "schedule" && (
|
{newTrigger.kind === "schedule" && (
|
||||||
<div className="md:col-span-2 space-y-1.5">
|
<div className="md:col-span-2 space-y-1.5">
|
||||||
<Label className="text-xs">Schedule</Label>
|
<Label className="text-xs">Schedule</Label>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const priorities = ["critical", "high", "medium", "low"];
|
|
||||||
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
||||||
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
||||||
const concurrencyPolicyDescriptions: Record<string, string> = {
|
const concurrencyPolicyDescriptions: Record<string, string> = {
|
||||||
@@ -420,20 +419,7 @@ export function Routines() {
|
|||||||
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="pt-3">
|
<CollapsibleContent className="pt-3">
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Priority</p>
|
|
||||||
<Select value={draft.priority} onValueChange={(priority) => setDraft((current) => ({ ...current, priority }))}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{priorities.map((priority) => (
|
|
||||||
<SelectItem key={priority} value={priority}>{priority.replace("_", " ")}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
Reference in New Issue
Block a user