feat(cron): edit command payloads in UI (#1287)

This commit is contained in:
Zezae Oh authored and GitHub committed 2026-06-27 12:04:13 +07:00
1 parent 99e2b7f851
commit 7a9210b765
6 files changed
+67 -15

No files matched your search

+4 -1
View File
@@ -143,7 +143,10 @@
"bytes": "{{count}} bytes",
"defaultValue": "Default",
"none": "None",
"envVars": "{{count}} vars"
"envVars": "{{count}} vars",
"commandJson": "Command JSON",
"commandJsonHelp": "Edit the command payload as JSON. argv is required and must be a non-empty string array.",
"invalidCommandJson": "Invalid command JSON: argv must be a non-empty array."
},
"stateless": "Stateless",
"statelessHelp": "Skip session persistence. Each run starts fresh without loading previous messages. Saves tokens for crons that don't need past context.",
+4 -1
View File
@@ -143,7 +143,10 @@
"bytes": "{{count}} bytes",
"defaultValue": "기본값",
"none": "없음",
"envVars": "{{count}}개"
"envVars": "{{count}}개",
"commandJson": "명령 JSON",
"commandJsonHelp": "명령 payload를 JSON으로 수정합니다. argv는 필수이며 비어 있지 않은 문자열 배열이어야 합니다.",
"invalidCommandJson": "명령 JSON이 올바르지 않습니다: argv는 비어 있지 않은 배열이어야 합니다."
},
"stateless": "상태 없음",
"statelessHelp": "세션 지속성 건너뜁니다. 각 실행은 이전 메시지 없이 새로 시작됩니다. 이전 컨텍스트가 필요 없는 cron의 토큰을 절약합니다.",
+4 -1
View File
@@ -143,7 +143,10 @@
"bytes": "{{count}} bytes",
"defaultValue": "Mặc định",
"none": "Không có",
"envVars": "{{count}} biến"
"envVars": "{{count}} biến",
"commandJson": "JSON lệnh",
"commandJsonHelp": "Chỉnh sửa payload lệnh dưới dạng JSON. argv là bắt buộc và phải là mảng chuỗi không rỗng.",
"invalidCommandJson": "JSON lệnh không hợp lệ: argv phải là mảng không rỗng."
},
"stateless": "Không trạng thái",
"statelessHelp": "Bỏ qua lưu session. Mỗi lần chạy bắt đầu mới. Tiết kiệm token cho các cron không cần context cũ.",
+4 -1
View File
@@ -143,7 +143,10 @@
"bytes": "{{count}} bytes",
"defaultValue": "默认值",
"none": "无",
"envVars": "{{count}}个变量"
"envVars": "{{count}}个变量",
"commandJson": "命令 JSON",
"commandJsonHelp": "以 JSON 编辑命令 payload。argv 必填,且必须是非空字符串数组。",
"invalidCommandJson": "命令 JSON 无效:argv 必须是非空数组。"
},
"stateless": "无状态",
"statelessHelp": "跳过会话持久化。每次运行从零开始。为不需要历史上下文的定时任务节省令牌。",
@@ -53,6 +53,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
const [agentId, setAgentId] = useState(job.agentId ?? "");
const [enabled, setEnabled] = useState(job.enabled);
const [editingMessage, setEditingMessage] = useState(false);
const [editingCommand, setEditingCommand] = useState(false);
// Delivery fields
const [deliver, setDeliver] = useState(job.deliver ?? false);
@@ -67,6 +68,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
const [saving, setSaving] = useState(false);
const command = job.payload?.command;
const [commandJson, setCommandJson] = useState(() => JSON.stringify(command ?? { argv: [] }, null, 2));
const commandEnvCount = command?.env ? Object.keys(command.env).length : 0;
// Fetch delivery targets on mount
@@ -88,6 +90,23 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
toast.error(t("detail.invalidTimezone", "Invalid timezone"));
return;
}
const commandChanged = isCommandCron(job)
&& commandJson.trim() !== JSON.stringify(command ?? { argv: [] }, null, 2).trim();
let parsedCommand: import("../hooks/use-cron").CronCommandSpec | undefined;
if (commandChanged) {
try {
parsedCommand = JSON.parse(commandJson);
if (!Array.isArray(parsedCommand?.argv) || parsedCommand.argv.length === 0 || !parsedCommand.argv[0]) {
toast.error(t("detail.invalidCommandJson"));
return;
}
} catch {
toast.error(t("detail.invalidCommandJson"));
return;
}
}
setSaving(true);
try {
let schedule;
@@ -101,6 +120,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
const patch: import("../hooks/use-cron").CronJobPatch = {
schedule,
message: isCommandCron(job) ? undefined : message.trim(),
command: commandChanged ? parsedCommand : undefined,
agentId: agentId.trim() || "",
deliver,
deliverChannel: deliver ? channel.trim() || undefined : undefined,
@@ -113,6 +133,7 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
if (enabled !== job.enabled) patch.enabled = enabled;
await onUpdate(job.id, patch);
setEditingMessage(false);
setEditingCommand(false);
} catch {
// toast shown by hook
} finally {
@@ -143,19 +164,37 @@ export function CronOverviewTab({ job, onUpdate }: CronOverviewTabProps) {
<Terminal className="h-4 w-4 text-amber-600 dark:text-amber-400" />
<h3 className="text-sm font-medium">{t("detail.commandSection")}</h3>
</div>
<span className="rounded-full border border-amber-300 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-700 dark:border-amber-800 dark:text-amber-300">
{t("payload.command")}
</span>
</div>
<div className="space-y-3 rounded-md border bg-background/80 p-3 sm:p-4">
<div>
<div className="mb-1 text-xs font-medium text-muted-foreground">{t("detail.commandArgv")}</div>
{formatCommand(job) ? (
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 text-xs"><code>{formatCommand(job)}</code></pre>
) : (
<p className="text-sm italic text-muted-foreground">{t("detail.noCommand")}</p>
<div className="flex items-center gap-2">
<span className="rounded-full border border-amber-300 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-700 dark:border-amber-800 dark:text-amber-300">
{t("payload.command")}
</span>
{!readonly && (
<Button variant="ghost" size="sm" className="h-7 gap-1 text-xs text-muted-foreground"
onClick={() => setEditingCommand(!editingCommand)}>
<Pencil className="h-3 w-3" />
{editingCommand ? t("detail.preview") : t("detail.edit")}
</Button>
)}
</div>
</div>
<div className="space-y-3 rounded-md border bg-background/80 p-3 sm:p-4">
{editingCommand ? (
<div>
<div className="mb-1 text-xs font-medium text-muted-foreground">{t("detail.commandJson")}</div>
<Textarea value={commandJson} onChange={(e) => setCommandJson(e.target.value)}
rows={10} className="font-mono text-base md:text-sm" />
<p className="mt-1 text-xs text-muted-foreground">{t("detail.commandJsonHelp")}</p>
</div>
) : (
<div>
<div className="mb-1 text-xs font-medium text-muted-foreground">{t("detail.commandArgv")}</div>
{formatCommand(job) ? (
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 text-xs"><code>{formatCommand(job)}</code></pre>
) : (
<p className="text-sm italic text-muted-foreground">{t("detail.noCommand")}</p>
)}
</div>
)}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">{t("detail.commandCwd")}</div>
+1
View File
@@ -37,6 +37,7 @@ export interface CronJobPatch {
enabled?: boolean;
schedule?: CronSchedule;
message?: string;
command?: CronCommandSpec;
deliver?: boolean;
deliverChannel?: string;
deliverTo?: string;