mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 10:18:42 +00:00
feat(cron-ui): move advanced settings inline to overview tab
Refactor cron detail page to consolidate settings in overview tab. Remove separate advanced dialog, simplifying navigation and reducing layout complexity.
This commit is contained in:
1 parent
ab0e2051a6
commit
7291f31364
4 files changed
+188
-89
No files matched your search
@@ -157,7 +157,7 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
</div>
|
||||
|
||||
{deliver && (
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[140px_1fr]">
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>{t("detail.channelLabel")}</Label>
|
||||
{channelNames.length > 0 ? (
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { CronJob, CronJobPatch, CronRunLogEntry } from "../hooks/use-cron";
|
||||
import { CronHeader } from "./cron-header";
|
||||
import { CronOverviewTab } from "./cron-overview-tab";
|
||||
import { CronRunHistoryTab } from "./cron-run-history-tab";
|
||||
import { CronAdvancedDialog } from "./cron-advanced-dialog";
|
||||
|
||||
interface CronDetailPageProps {
|
||||
job: CronJob;
|
||||
@@ -31,7 +30,6 @@ export function CronDetailPage({
|
||||
}: CronDetailPageProps) {
|
||||
const { t } = useTranslation("cron");
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [confirmToggle, setConfirmToggle] = useState(false);
|
||||
const [running, setRunning] = useState(false);
|
||||
@@ -64,7 +62,6 @@ export function CronDetailPage({
|
||||
isRunning={isRunning}
|
||||
onBack={onBack}
|
||||
onRun={handleRun}
|
||||
onAdvanced={() => setAdvancedOpen(true)}
|
||||
onToggle={() => setConfirmToggle(true)}
|
||||
onDelete={() => setConfirmDelete(true)}
|
||||
/>
|
||||
@@ -88,14 +85,6 @@ export function CronDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CronAdvancedDialog
|
||||
key={job.id}
|
||||
open={advancedOpen}
|
||||
onOpenChange={setAdvancedOpen}
|
||||
job={job}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmDelete}
|
||||
onOpenChange={setConfirmDelete}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowLeft, Clock, Loader2, Play, Power, Settings, Trash2 } from "lucide-react";
|
||||
import { ArrowLeft, Clock, Loader2, Play, Power, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
@@ -13,13 +13,12 @@ interface CronHeaderProps {
|
||||
isRunning: boolean;
|
||||
onBack: () => void;
|
||||
onRun: () => void;
|
||||
onAdvanced: () => void;
|
||||
onToggle: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
|
||||
export function CronHeader({ job, isRunning, onBack, onRun, onAdvanced, onToggle, onDelete }: CronHeaderProps) {
|
||||
export function CronHeader({ job, isRunning, onBack, onRun, onToggle, onDelete }: CronHeaderProps) {
|
||||
const { t } = useTranslation("cron");
|
||||
const { agents } = useAgents();
|
||||
const agent = job.agentId ? agents.find((a) => a.id === job.agentId) : null;
|
||||
@@ -100,17 +99,6 @@ export function CronHeader({ job, isRunning, onBack, onRun, onAdvanced, onToggle
|
||||
<Power className="h-4 w-4 sm:hidden" />
|
||||
</Button>
|
||||
|
||||
{/* Advanced */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onAdvanced}
|
||||
className="shrink-0 gap-1.5 size-9 sm:w-auto sm:px-3"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t("detail.advanced")}</span>
|
||||
</Button>
|
||||
|
||||
{/* Delete */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Calendar, Clock, AlertTriangle, Globe, Pencil } from "lucide-react";
|
||||
import { Calendar, Clock, AlertTriangle, Pencil, Send, Settings } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -9,11 +9,24 @@ import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { StickySaveBar } from "@/components/shared/sticky-save-bar";
|
||||
import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { getAllIanaTimezones, isValidIanaTimezone } from "@/lib/constants";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { toast } from "@/stores/use-toast-store";
|
||||
import { useChannels } from "@/pages/channels/hooks/use-channels";
|
||||
import { useWs } from "@/hooks/use-ws";
|
||||
import { Methods } from "@/api/protocol";
|
||||
import type { CronJob, CronJobPatch } from "../hooks/use-cron";
|
||||
import { CronStatusBadge } from "../cron-utils";
|
||||
import { useAgents } from "@/pages/agents/hooks/use-agents";
|
||||
|
||||
interface DeliveryTarget {
|
||||
channel: string;
|
||||
chatId: string;
|
||||
title?: string;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface CronOverviewTabProps {
|
||||
job: CronJob;
|
||||
onUpdate?: (id: string, params: CronJobPatch) => Promise<void>;
|
||||
@@ -31,34 +44,74 @@ function getEverySeconds(job: CronJob): string {
|
||||
export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
const { t } = useTranslation("cron");
|
||||
const { agents } = useAgents();
|
||||
const ws = useWs();
|
||||
const { channels: availableChannels } = useChannels();
|
||||
const channelNames = Object.keys(availableChannels);
|
||||
const readonly = !onUpdate;
|
||||
|
||||
// Schedule fields
|
||||
const [scheduleKind, setScheduleKind] = useState<ScheduleKind>(job.schedule.kind as ScheduleKind);
|
||||
const [everySeconds, setEverySeconds] = useState(getEverySeconds(job));
|
||||
const [cronExpr, setCronExpr] = useState(job.schedule.expr ?? "0 * * * *");
|
||||
const [timezone, setTimezone] = useState(job.schedule.tz ?? "UTC");
|
||||
const [message, setMessage] = useState(job.payload?.message ?? "");
|
||||
const [agentId, setAgentId] = useState(job.agentId ?? "");
|
||||
const [enabled, setEnabled] = useState(job.enabled);
|
||||
const [editingMessage, setEditingMessage] = useState(false);
|
||||
|
||||
// Delivery fields
|
||||
const [deliver, setDeliver] = useState(job.deliver ?? false);
|
||||
const [channel, setChannel] = useState(job.deliverChannel ?? "");
|
||||
const [to, setTo] = useState(job.deliverTo ?? "");
|
||||
const [wakeHeartbeat, setWakeHeartbeat] = useState(job.wakeHeartbeat ?? false);
|
||||
const [targets, setTargets] = useState<DeliveryTarget[]>([]);
|
||||
|
||||
// Lifecycle fields
|
||||
const [deleteAfterRun, setDeleteAfterRun] = useState(job.deleteAfterRun ?? false);
|
||||
const [stateless, setStateless] = useState(job.stateless ?? false);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Fetch delivery targets on mount
|
||||
const fetchTargets = useCallback(async () => {
|
||||
if (!ws.isConnected) return;
|
||||
try {
|
||||
const res = await ws.call<{ targets: DeliveryTarget[] }>(
|
||||
Methods.HEARTBEAT_TARGETS, { agentId: job.agentId || "" },
|
||||
);
|
||||
setTargets(res.targets ?? []);
|
||||
} catch { /* fallback to Input */ }
|
||||
}, [ws, job.agentId]);
|
||||
|
||||
useEffect(() => { fetchTargets(); }, [fetchTargets]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!onUpdate) return;
|
||||
if (timezone && timezone !== "UTC" && !isValidIanaTimezone(timezone)) {
|
||||
toast.error(t("detail.invalidTimezone", "Invalid timezone"));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
let schedule;
|
||||
if (scheduleKind === "every") {
|
||||
schedule = { kind: "every" as const, everyMs: Number(everySeconds) * 1000 };
|
||||
schedule = { kind: "every" as const, everyMs: Number(everySeconds) * 1000, tz: timezone !== "UTC" ? timezone : "" };
|
||||
} else if (scheduleKind === "cron") {
|
||||
schedule = { kind: "cron" as const, expr: cronExpr };
|
||||
schedule = { kind: "cron" as const, expr: cronExpr, tz: timezone !== "UTC" ? timezone : "" };
|
||||
} else {
|
||||
schedule = { kind: "at" as const, atMs: job.schedule.atMs ?? Date.now() + 60000 };
|
||||
schedule = { kind: "at" as const, atMs: job.schedule.atMs ?? Date.now() + 60000, tz: timezone !== "UTC" ? timezone : "" };
|
||||
}
|
||||
await onUpdate(job.id, {
|
||||
schedule,
|
||||
message: message.trim(),
|
||||
agentId: agentId.trim() || "",
|
||||
enabled,
|
||||
deliver,
|
||||
deliverChannel: deliver ? channel.trim() || undefined : undefined,
|
||||
deliverTo: deliver ? to.trim() || undefined : undefined,
|
||||
wakeHeartbeat,
|
||||
deleteAfterRun,
|
||||
stateless,
|
||||
});
|
||||
setEditingMessage(false);
|
||||
} catch {
|
||||
@@ -76,11 +129,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("create.name")}</Label>
|
||||
<Input
|
||||
value={job.name}
|
||||
disabled
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<Input value={job.name} disabled className="text-base md:text-sm" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -105,12 +154,9 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
<div className="space-y-2">
|
||||
<Label>{t("create.intervalSeconds")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={everySeconds}
|
||||
type="number" min={1} value={everySeconds}
|
||||
onChange={(e) => setEverySeconds(e.target.value)}
|
||||
disabled={readonly}
|
||||
className="text-base md:text-sm"
|
||||
disabled={readonly} className="text-base md:text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -119,11 +165,8 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
<div className="space-y-2">
|
||||
<Label>{t("create.cronExpression")}</Label>
|
||||
<Input
|
||||
value={cronExpr}
|
||||
onChange={(e) => setCronExpr(e.target.value)}
|
||||
disabled={readonly}
|
||||
placeholder="0 * * * *"
|
||||
className="text-base md:text-sm"
|
||||
value={cronExpr} onChange={(e) => setCronExpr(e.target.value)}
|
||||
disabled={readonly} placeholder="0 * * * *" className="text-base md:text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("create.cronHint")}</p>
|
||||
</div>
|
||||
@@ -132,33 +175,35 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
{scheduleKind === "at" && (
|
||||
<p className="text-sm text-muted-foreground">{t("create.onceDesc")}</p>
|
||||
)}
|
||||
|
||||
{/* Timezone */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("detail.timezone")}</Label>
|
||||
<Combobox
|
||||
value={timezone} onChange={setTimezone}
|
||||
options={getAllIanaTimezones()}
|
||||
placeholder={t("detail.timezone")}
|
||||
className="text-base md:text-sm"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("detail.timezoneDesc")}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Message section — markdown preview or textarea */}
|
||||
{/* Message section */}
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">{t("detail.messageSection")}</h3>
|
||||
{!readonly && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1 text-xs text-muted-foreground"
|
||||
onClick={() => setEditingMessage(!editingMessage)}
|
||||
>
|
||||
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs text-muted-foreground"
|
||||
onClick={() => setEditingMessage(!editingMessage)}>
|
||||
<Pencil className="h-3 w-3" />
|
||||
{editingMessage ? t("detail.preview") : t("detail.edit")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editingMessage ? (
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
rows={6}
|
||||
placeholder={t("create.messagePlaceholder")}
|
||||
className="text-base md:text-sm resize-none"
|
||||
/>
|
||||
<Textarea value={message} onChange={(e) => setMessage(e.target.value)}
|
||||
rows={6} placeholder={t("create.messagePlaceholder")} className="text-base md:text-sm resize-none" />
|
||||
) : (
|
||||
<div className="rounded-md border bg-muted/30 p-3 sm:p-4">
|
||||
{message ? (
|
||||
@@ -170,13 +215,90 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Delivery section */}
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Send className="h-4 w-4 text-blue-500" />
|
||||
<h3 className="text-sm font-medium">{t("detail.delivery")}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("detail.deliveryDesc")}</p>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
<p className="text-sm font-medium">{t("detail.deliverToChannel")}</p>
|
||||
<Switch checked={deliver} onCheckedChange={setDeliver} disabled={readonly} />
|
||||
</div>
|
||||
|
||||
{deliver && (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[140px_1fr]">
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>{t("detail.channelLabel")}</Label>
|
||||
{channelNames.length > 0 ? (
|
||||
<Select value={channel || "__none__"}
|
||||
onValueChange={(v) => { setChannel(v === "__none__" ? "" : v); setTo(""); }}>
|
||||
<SelectTrigger className="text-base md:text-sm">
|
||||
<SelectValue placeholder={t("detail.channelPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">{t("detail.channelPlaceholder")}</SelectItem>
|
||||
{channelNames.map((ch) => (
|
||||
<SelectItem key={ch} value={ch}>{ch}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input value={channel} onChange={(e) => setChannel(e.target.value)}
|
||||
placeholder={t("detail.channelPlaceholder")} className="text-base md:text-sm" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2 min-w-0">
|
||||
<Label>{t("detail.toLabel")}</Label>
|
||||
{(() => {
|
||||
if (!channel) {
|
||||
return <Input placeholder={t("detail.channelPlaceholder")} disabled className="text-base md:text-sm" />;
|
||||
}
|
||||
const filtered = targets.filter((tgt) => tgt.channel === channel);
|
||||
if (filtered.length > 0) {
|
||||
return (
|
||||
<Select value={to || "__none__"} onValueChange={(v) => setTo(v === "__none__" ? "" : v)}>
|
||||
<SelectTrigger className="text-base md:text-sm">
|
||||
<SelectValue placeholder={t("detail.toPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">{t("detail.toPlaceholder")}</SelectItem>
|
||||
{filtered.map((tgt) => (
|
||||
<SelectItem key={tgt.chatId} value={tgt.chatId}
|
||||
title={tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}>
|
||||
<span className="truncate">{tgt.title ? `${tgt.title} (${tgt.chatId})` : tgt.chatId}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return <Input value={to} onChange={(e) => setTo(e.target.value)}
|
||||
placeholder={t("detail.toPlaceholder")} className="text-base md:text-sm" />;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t("detail.wakeHeartbeat")}</p>
|
||||
<p className="text-xs text-muted-foreground">{t("detail.wakeHeartbeatDesc")}</p>
|
||||
</div>
|
||||
<Switch checked={wakeHeartbeat} onCheckedChange={setWakeHeartbeat} disabled={readonly} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Agent & Status section */}
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<h3 className="text-sm font-medium">{t("detail.agentStatus")}</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("create.agentId")}</Label>
|
||||
<Select name="agentId" value={agentId || "__default__"} onValueChange={(v) => setAgentId(v === "__default__" ? "" : v)} disabled={readonly}>
|
||||
<Select name="agentId" value={agentId || "__default__"}
|
||||
onValueChange={(v) => setAgentId(v === "__default__" ? "" : v)} disabled={readonly}>
|
||||
<SelectTrigger className="text-base md:text-sm">
|
||||
<SelectValue placeholder={t("create.agentIdPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
@@ -194,11 +316,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
<Label>{t("columns.enabled")}</Label>
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
<span className="text-sm">{enabled ? t("detail.enabled") : t("detail.disabled")}</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} disabled={readonly} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,8 +326,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
{job.state?.nextRunAtMs && (
|
||||
<div className="rounded-md bg-muted/50 p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{t("detail.infoRows.nextRun")}
|
||||
<Calendar className="h-3 w-3" />{t("detail.infoRows.nextRun")}
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium">{formatDate(new Date(job.state.nextRunAtMs))}</div>
|
||||
</div>
|
||||
@@ -217,8 +334,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
{job.state?.lastRunAtMs && (
|
||||
<div className="rounded-md bg-muted/50 p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
{t("detail.infoRows.lastRun")}
|
||||
<Clock className="h-3 w-3" />{t("detail.infoRows.lastRun")}
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium">{formatDate(new Date(job.state.lastRunAtMs))}</div>
|
||||
</div>
|
||||
@@ -233,19 +349,30 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
<div className="text-xs text-muted-foreground">{t("detail.infoRows.created")}</div>
|
||||
<div className="mt-1 text-sm font-medium">{formatDate(new Date(job.createdAtMs))}</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted/50 p-3">
|
||||
<div className="text-xs text-muted-foreground">{t("detail.infoRows.updated")}</div>
|
||||
<div className="mt-1 text-sm font-medium">{formatDate(new Date(job.updatedAtMs))}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Lifecycle section */}
|
||||
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">{t("detail.lifecycle")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t("detail.deleteAfterRun")}</p>
|
||||
<p className="text-xs text-muted-foreground">{t("detail.deleteAfterRunDesc")}</p>
|
||||
</div>
|
||||
{job.schedule.tz && (
|
||||
<div className="rounded-md bg-muted/50 p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Globe className="h-3 w-3" />
|
||||
{t("detail.infoRows.timezone")}
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium">{job.schedule.tz}</div>
|
||||
</div>
|
||||
)}
|
||||
<Switch checked={deleteAfterRun} onCheckedChange={setDeleteAfterRun} disabled={readonly} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border px-3 py-2.5">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t("stateless")}</p>
|
||||
<p className="text-xs text-muted-foreground">{t("statelessHelp")}</p>
|
||||
</div>
|
||||
<Switch checked={stateless} onCheckedChange={setStateless} disabled={readonly} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -262,12 +389,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{!readonly && (
|
||||
<StickySaveBar
|
||||
onSave={handleSave}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
{!readonly && <StickySaveBar onSave={handleSave} saving={saving} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user