mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
feat(cron): edit command payloads in UI (#1287)
This commit is contained in:
1 parent
99e2b7f851
commit
7a9210b765
6 files changed
+67
-15
No files matched your search
@@ -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.",
|
||||
|
||||
@@ -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의 토큰을 절약합니다.",
|
||||
|
||||
@@ -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ũ.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface CronJobPatch {
|
||||
enabled?: boolean;
|
||||
schedule?: CronSchedule;
|
||||
message?: string;
|
||||
command?: CronCommandSpec;
|
||||
deliver?: boolean;
|
||||
deliverChannel?: string;
|
||||
deliverTo?: string;
|
||||
|
||||
Reference in new issue
Block a user