import { useState } from "react"; import { CalendarClock, CircleAlert, ListTodo, RefreshCcw, } from "lucide-react"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useSessionAutomationJobs } from "@/hooks/useSessionAutomationJobs"; import { currentLocale } from "@/i18n"; import { fmtDateTime } from "@/lib/format"; import type { SessionAutomationJob } from "@/lib/types"; import { cn } from "@/lib/utils"; const RELATIVE_THRESHOLDS: [number, Intl.RelativeTimeFormatUnit][] = [ [60, "second"], [60, "minute"], [24, "hour"], [7, "day"], [4.345, "week"], [12, "month"], [Number.POSITIVE_INFINITY, "year"], ]; interface SessionInfoPopoverProps { sessionKey: string; token: string; title: string; } export function SessionInfoPopover({ sessionKey, token, title }: SessionInfoPopoverProps) { const { t } = useTranslation("common"); const [open, setOpen] = useState(false); const { jobs, loading, loadFailed, now } = useSessionAutomationJobs(open, token, sessionKey); const automationContent = loading ? (
{t("thread.sessionInfo.loading")}
) : loadFailed ? (
{t("thread.sessionInfo.loadFailed")}
) : jobs.length ? (
{jobs.map((job) => ( ))}
) : (
{t("thread.sessionInfo.empty")}
); return (
{t("thread.sessionInfo.title")}
{title || t("thread.sessionInfo.untitled")}
{t("thread.sessionInfo.automations")}
{t("thread.sessionInfo.count", { count: jobs.length })}
{automationContent}
); } function AutomationRow({ job, now }: { job: SessionAutomationJob; now: number }) { const { t } = useTranslation("common"); const schedule = formatSchedule(job, t); const nextRun = formatNextRun(job, t, now); const statusClass = job.enabled ? job.state.last_status === "error" ? "bg-destructive" : "bg-emerald-500" : "bg-muted-foreground/35"; return (
{job.name} {!job.enabled ? ( {t("thread.sessionInfo.disabled")} ) : null}
{job.payload.message}
{schedule} ยท {nextRun.label}
); } function formatSchedule(job: SessionAutomationJob, t: TFunction) { const locale = currentLocale(); if (isLocalTriggerAutomation(job)) { return t("thread.sessionInfo.schedule.local"); } if (job.schedule.kind === "at" && job.schedule.at_ms) { return t("thread.sessionInfo.schedule.at", { time: fmtDateTime(job.schedule.at_ms, locale) }); } if (job.schedule.kind === "every" && job.schedule.every_ms) { return t("thread.sessionInfo.schedule.every", { duration: formatDuration(job.schedule.every_ms, locale), }); } if (job.schedule.kind === "cron" && job.schedule.expr) { return job.schedule.tz ? t("thread.sessionInfo.schedule.cronWithTz", { expr: job.schedule.expr, tz: job.schedule.tz, }) : t("thread.sessionInfo.schedule.cron", { expr: job.schedule.expr }); } return t("thread.sessionInfo.schedule.unknown"); } function formatNextRun(job: SessionAutomationJob, t: TFunction, now: number) { const locale = currentLocale(); if (!job.enabled) { return { label: t("thread.sessionInfo.next.disabled"), title: "" }; } if (job.state.pending) { return { label: t("thread.sessionInfo.next.pending"), title: "" }; } if (isLocalTriggerAutomation(job)) { return { label: t("thread.sessionInfo.next.local"), title: "" }; } const next = job.state.next_run_at_ms; if (!next) { return { label: t("thread.sessionInfo.next.none"), title: "" }; } return { label: t("thread.sessionInfo.next.label", { time: relativeTimeFrom(next, now, locale) }), title: fmtDateTime(next, locale), }; } function isLocalTriggerAutomation(job: SessionAutomationJob): boolean { return job.kind === "local_trigger" || job.payload.kind === "local_trigger" || job.schedule.kind === "local"; } function relativeTimeFrom(value: number, now: number, locale: string): string { let delta = (value - now) / 1000; const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); for (const [step, unit] of RELATIVE_THRESHOLDS) { if (Math.abs(delta) < step) { return formatter.format(Math.round(delta), unit); } delta /= step; } return formatter.format(Math.round(delta), "year"); } function formatDuration(ms: number, locale: string): string { const units: Array<[Intl.NumberFormatOptions["unit"], number]> = [ ["day", 86_400_000], ["hour", 3_600_000], ["minute", 60_000], ["second", 1000], ]; for (const [unit, size] of units) { if (ms >= size && ms % size === 0) { return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "long", maximumFractionDigits: 0, }).format(ms / size); } } return new Intl.NumberFormat(locale, { style: "unit", unit: "minute", unitDisplay: "long", maximumFractionDigits: 1, }).format(ms / 60_000); }