From 7a9210b76590ada558799bc2644462265299b283 Mon Sep 17 00:00:00 2001 From: Zezae Oh Date: Sat, 27 Jun 2026 14:04:13 +0900 Subject: [PATCH] feat(cron): edit command payloads in UI (#1287) --- ui/web/src/i18n/locales/en/cron.json | 5 +- ui/web/src/i18n/locales/ko/cron.json | 5 +- ui/web/src/i18n/locales/vi/cron.json | 5 +- ui/web/src/i18n/locales/zh/cron.json | 5 +- .../cron/cron-detail/cron-overview-tab.tsx | 61 +++++++++++++++---- ui/web/src/pages/cron/hooks/use-cron.ts | 1 + 6 files changed, 67 insertions(+), 15 deletions(-) diff --git a/ui/web/src/i18n/locales/en/cron.json b/ui/web/src/i18n/locales/en/cron.json index e169a897..62f30a73 100644 --- a/ui/web/src/i18n/locales/en/cron.json +++ b/ui/web/src/i18n/locales/en/cron.json @@ -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.", diff --git a/ui/web/src/i18n/locales/ko/cron.json b/ui/web/src/i18n/locales/ko/cron.json index 8ebad7ce..3fe81362 100644 --- a/ui/web/src/i18n/locales/ko/cron.json +++ b/ui/web/src/i18n/locales/ko/cron.json @@ -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의 토큰을 절약합니다.", diff --git a/ui/web/src/i18n/locales/vi/cron.json b/ui/web/src/i18n/locales/vi/cron.json index 480e3cf4..44aadbac 100644 --- a/ui/web/src/i18n/locales/vi/cron.json +++ b/ui/web/src/i18n/locales/vi/cron.json @@ -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ũ.", diff --git a/ui/web/src/i18n/locales/zh/cron.json b/ui/web/src/i18n/locales/zh/cron.json index b1263a84..f00d3fb0 100644 --- a/ui/web/src/i18n/locales/zh/cron.json +++ b/ui/web/src/i18n/locales/zh/cron.json @@ -143,7 +143,10 @@ "bytes": "{{count}} bytes", "defaultValue": "默认值", "none": "无", - "envVars": "{{count}}个变量" + "envVars": "{{count}}个变量", + "commandJson": "命令 JSON", + "commandJsonHelp": "以 JSON 编辑命令 payload。argv 必填,且必须是非空字符串数组。", + "invalidCommandJson": "命令 JSON 无效:argv 必须是非空数组。" }, "stateless": "无状态", "statelessHelp": "跳过会话持久化。每次运行从零开始。为不需要历史上下文的定时任务节省令牌。", diff --git a/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx b/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx index 8e40b6b5..4c4eb6bb 100644 --- a/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx +++ b/ui/web/src/pages/cron/cron-detail/cron-overview-tab.tsx @@ -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) {

{t("detail.commandSection")}

- - {t("payload.command")} - - -
-
-
{t("detail.commandArgv")}
- {formatCommand(job) ? ( -
{formatCommand(job)}
- ) : ( -

{t("detail.noCommand")}

+
+ + {t("payload.command")} + + {!readonly && ( + )}
+
+
+ {editingCommand ? ( +
+
{t("detail.commandJson")}
+