Files
model-platform/frontend/app/features/schedules/ScheduleInspector.tsx
T
2026-08-28 12:00:14 +08:00

202 lines
8.4 KiB
TypeScript

import { type Schedule, type CronPreview } from "../../services/api";
import { Settings } from "lucide-react";
import { formatTime } from "./utils";
type ScheduleForm = {
scheduleName: string;
description: string;
triggerType: "manual" | "cron" | "api";
cronExpression: string;
timezone: string;
enabled: boolean;
maxConcurrency: string;
failurePolicy: "stop" | "continue";
};
export function ScheduleInspector({
schedule,
form,
cronResult,
busy,
onChange,
onPreview,
onSave,
}: {
schedule: Schedule | null;
form: ScheduleForm;
cronResult: CronPreview | null;
busy: boolean;
onChange: (form: ScheduleForm) => void;
onPreview: () => void;
onSave: () => void;
}) {
if (!schedule) {
return (
<div className="flex min-h-full flex-col items-center justify-center p-[30px] text-center text-[#9aa8b6] [&>svg]:mb-[10px] [&>svg]:text-[#93b4d6]">
<Settings size={28} />
<strong className="text-[14px] text-[#516a82]">调度属性</strong>
<p className="text-[11px] leading-[1.6]">
选中调度方案后可配置触发方式、Cron 和执行策略。
</p>
</div>
);
}
return (
<div className="flex min-h-full flex-col">
<section className="flex flex-col gap-[10px] border-b border-line p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]">基本信息</h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">调度名称</span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.scheduleName}
onChange={(event) => onChange({
...form,
scheduleName: event.target.value,
})}
/>
</label>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">说明</span>
<textarea
className="w-full resize-y rounded-[5px] border border-line bg-white p-[7px_8px] text-[11px] leading-[1.5] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
rows={2}
value={form.description}
onChange={(event) => onChange({
...form,
description: event.target.value,
})}
/>
</label>
<label className="flex flex-row items-center justify-between rounded-[6px] bg-[#f5f8fb] p-[8px_9px]">
<span className="flex flex-col gap-[2px]">
<b className="text-[11px] text-[#45596f]">启用调度</b>
<small className="text-[9px] font-normal text-[#94a1ae]">DAG 有效后才能启用</small>
</span>
<input
type="checkbox"
className="h-[18px] w-[34px] accent-brand"
checked={form.enabled}
onChange={(event) => onChange({
...form,
enabled: event.target.checked,
})}
/>
</label>
</section>
<section className="flex flex-col gap-[10px] border-b border-line p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]">触发器</h3>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">触发方式</span>
<select
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.triggerType}
onChange={(event) => onChange({
...form,
triggerType: event.target.value as typeof form.triggerType,
enabled: event.target.value === "cron" ? form.enabled : false,
})}
>
<option value="manual">手动触发</option>
<option value="cron">Cron 定时</option>
<option value="api">API 触发</option>
</select>
</label>
{form.triggerType === "cron" && (
<>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">Cron(分 周)</span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.cronExpression}
onChange={(event) => onChange({
...form,
cronExpression: event.target.value,
})}
/>
</label>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">时区</span>
<input
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.timezone}
onChange={(event) => onChange({
...form,
timezone: event.target.value,
})}
/>
</label>
<button
className="flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-[#bcd2e9] bg-[#f7fbff] text-[11px] text-[#2871bb] [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onPreview}
>
预览未来 5
</button>
{cronResult && (
<ol className="m-0 list-decimal rounded-[5px] bg-[#f5f8fb] p-[8px_8px_8px_28px] font-mono text-[9px] leading-[1.7] text-ink-muted">
{cronResult.occurrences.map((item) => (
<li key={item.utc_time}>
{new Date(item.local_time).toLocaleString("zh-CN", {
hour12: false,
})}
</li>
))}
</ol>
)}
</>
)}
</section>
<section className="flex flex-col gap-[10px] border-b border-line p-[14px]">
<h3 className="mb-[2px] text-[13px] text-[#273b50]">执行策略</h3>
<div className="grid grid-cols-2 gap-2">
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">最大并发</span>
<input
type="number"
min="1"
max="64"
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.maxConcurrency}
onChange={(event) => onChange({
...form,
maxConcurrency: event.target.value,
})}
/>
</label>
<label className="flex flex-col gap-[5px]">
<span className="text-[10px] font-semibold text-ink-caption">失败策略</span>
<select
className="h-[33px] w-full rounded-[5px] border border-line bg-white px-2 text-[11px] text-[#32475d] outline-none focus:border-brand/70 focus:ring-3 focus:ring-brand/10"
value={form.failurePolicy}
onChange={(event) => onChange({
...form,
failurePolicy: event.target.value as "stop" | "continue",
})}
>
<option value="stop">停止后续</option>
<option value="continue">继续执行</option>
</select>
</label>
</div>
<div className="flex items-center justify-between rounded-[5px] bg-[#f5f8fb] p-[8px_9px] text-[10px] text-ink-subtle">
<span>下一次执行</span>
<strong className="text-[10px] text-[#425970]">{formatTime(schedule.next_run_at)}</strong>
</div>
</section>
<button
className="m-[14px] flex min-h-[34px] cursor-pointer items-center justify-center rounded-[6px] border border-brand bg-brand text-[11px] text-white [font-weight:650] disabled:cursor-not-allowed disabled:opacity-[0.55]"
type="button"
disabled={busy}
onClick={onSave}
>
保存调度属性
</button>
</div>
);
}