import { t } from "@lingui/core/macro"; import { Trans, useLingui } from "@lingui/react/macro"; import type { Routine } from "@rakazo/contracts"; import { type CronFreq, type CronPreset, cronFromPreset, defaultCronPreset, formatCron, isOneShotRoutineCrons, presetFromCron, } from "@rakazo/core"; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Switch, Textarea, } from "@rakazo/ui-web"; import { ChevronLeft, Clock, GitBranch, Globe, MessageSquare, Pause, Plus, X } from "lucide-react"; import { useId } from "react"; import { routineName } from "../lib/routine-name"; import { RoutineSchedule } from "./RoutineSchedule"; function toDatetimeLocalValue(date: Date): string { const pad = (n: number) => String(n).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; } function defaultArmRunAtLocal(): string { return toDatetimeLocalValue(new Date(Date.now() + 60 * 60 * 1000)); } export function routineNeedsOneShotArm( routine: Pick, crons: string[], ) { return isOneShotRoutineCrons(crons) && !routine.nextRunAt && !routine.lastRunAt; } const SCHEDULE_PRESETS: CronFreq[] = [ "Every hour", "Every day", "Weekdays", "Every week", "Every month", "Interval", "Advanced", ]; const COMING_SOON = [ { id: "teams", label: () => t`Teams message` }, { id: "linear", label: () => t`Linear issue` }, { id: "sentry", label: () => t`Sentry alert` }, { id: "pagerduty", label: () => t`PagerDuty incident` }, ] as const; export type RoutineDraftState = { name: string; prompt: string; schedules: CronPreset[]; webhookEnabled: boolean; githubEnabled: boolean; messageProvider: string | null; active: boolean; runAtLocal: string; }; export function emptyRoutineDraft(): RoutineDraftState { return { name: "", prompt: "", schedules: [], webhookEnabled: false, githubEnabled: false, messageProvider: null, active: true, runAtLocal: "", }; } export function draftFromRoutine(routine: Routine): RoutineDraftState { return { name: routineName(routine), prompt: routine.prompt, schedules: routine.crons.map(presetFromCron), webhookEnabled: routine.webhookEnabled, githubEnabled: routine.githubEnabled, messageProvider: routine.messageProvider, active: routine.active, runAtLocal: routineNeedsOneShotArm(routine, routine.crons) ? defaultArmRunAtLocal() : "", }; } export function routineTriggerSummary(routine: Routine): string { if (!routine.active) return t`Paused`; const parts: string[] = []; if (routine.webhookEnabled) parts.push(t`When a webhook fires`); if (routine.githubEnabled) parts.push(t`Git event`); if (routine.messageProvider === "slack") parts.push(t`Slack message`); else if (routine.messageProvider === "teams") parts.push(t`Teams message`); else if (routine.messageProvider) parts.push(t`Message event`); for (const cron of routine.crons) parts.push(formatCron(cron)); return parts.length > 0 ? parts.join(" · ") : t`No trigger`; } export function RoutineListHeader({ onCreate }: { onCreate: () => void }) { const { t } = useLingui(); return (
Routines
); } export function RoutineListRow({ routine, running, onOpen, onStop, }: { routine: Routine; running: boolean; onOpen: () => void; onStop: () => void; }) { return (
{running ? ( ) : null}
); } export function RoutineEditor({ draft, onChange, editing, timezone, webhook, githubPath, messageProviders, saving, running, error, onBack, onClose, onSave, onTestRun, onDelete, onEnsureWebhook, }: { draft: RoutineDraftState; onChange: (next: RoutineDraftState) => void; editing: Routine | null; timezone: string; webhook: { path: string; secret: string | null; configured: boolean }; githubPath: string; messageProviders: string[]; saving: boolean; running: boolean; error: string | null; onBack: () => void; onClose: () => void; onSave: () => void; onTestRun: () => void; onDelete: () => void; onEnsureWebhook: () => Promise; }) { const { t } = useLingui(); const fieldId = useId(); const activeLabelId = useId(); const slackAvailable = messageProviders.includes("slack"); const slackDisabledReasonId = `${fieldId}-slack-disabled-reason`; const hasTriggers = draft.schedules.length > 0 || draft.webhookEnabled || draft.githubEnabled || Boolean(draft.messageProvider); const canTest = Boolean(editing) && !saving && !running; const needsOneShotArm = editing != null && routineNeedsOneShotArm(editing, draft.schedules.map(cronFromPreset)); function addSchedule(freq: CronFreq) { const base = defaultCronPreset(); const next: CronPreset = freq === "Advanced" ? { ...base, freq, cron: cronFromPreset(base) } : { ...base, freq }; onChange({ ...draft, schedules: [...draft.schedules, next] }); } async function addWebhook() { onChange({ ...draft, webhookEnabled: true }); if (!webhook.configured) { await onEnsureWebhook().catch(() => undefined); } } async function addGithub() { onChange({ ...draft, githubEnabled: true }); if (!webhook.configured) { await onEnsureWebhook().catch(() => undefined); } } function addMessageProvider(provider: string) { onChange({ ...draft, messageProvider: provider }); } return (
Routine
onChange({ ...draft, active })} /> Active