import { useEffect, useMemo, useState } from "react"; import { Clock4, Plus, Trash2, Save } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { managerPost } from "../../host-metrics-api"; import { useManagerData, extractError } from "./useManagerData"; import { ManagerCardShell } from "./ManagerCardShell"; import { ManagerSearch } from "./ManagerToolbar"; interface CronEntry { raw: string; enabled: boolean; schedule: string; command: string; } export function CronManagerCard({ hostId }: { hostId: number | null }) { const { t } = useTranslation(); const { data, loading, error, refresh } = useManagerData<{ entries: CronEntry[]; }>(hostId, "cron"); const [entries, setEntries] = useState([]); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [query, setQuery] = useState(""); // Keep the real array index so edits/removes target the right entry even when // the list is filtered by the search box. const visible = useMemo(() => { const q = query.toLowerCase(); return entries .map((entry, index) => ({ entry, index })) .filter( ({ entry }) => !q || entry.schedule.toLowerCase().includes(q) || entry.command.toLowerCase().includes(q), ); }, [entries, query]); useEffect(() => { if (data?.entries) { setEntries(data.entries); setDirty(false); } }, [data]); const update = (i: number, patch: Partial) => { setEntries((prev) => prev.map((e, idx) => (idx === i ? { ...e, ...patch } : e)), ); setDirty(true); }; const remove = (i: number) => { setEntries((prev) => prev.filter((_, idx) => idx !== i)); setDirty(true); }; const add = () => { setEntries((prev) => [ ...prev, { raw: "", enabled: true, schedule: "0 * * * *", command: "" }, ]); setDirty(true); }; const save = async () => { if (hostId == null) return; setSaving(true); try { const res = await managerPost<{ success: boolean; output: string }>( hostId, "cron", { entries }, ); if (res.success) { toast.success(t("hostMetrics.managers.cronSaved")); setDirty(false); refresh(); } else { toast.error(res.output || t("hostMetrics.managers.actionFailed")); } } catch (e) { toast.error(extractError(e).message); } finally { setSaving(false); } }; return ( } loading={loading} error={error} onRefresh={refresh} headerExtra={ <> {dirty && ( )} } empty={!loading && entries.length === 0} > {entries.length > 5 && ( )}
{visible.map(({ entry: e, index: i }) => (
update(i, { enabled: ev.target.checked })} className="accent-accent-brand" title={t("hostMetrics.managers.enabled")} /> update(i, { schedule: ev.target.value })} placeholder="0 * * * *" className="h-6 w-28 border border-border bg-background px-1.5 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring" />
update(i, { command: ev.target.value })} placeholder={t("hostMetrics.managers.command")} className="h-6 w-full border border-border bg-background px-1.5 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring" />
))}
); }