feat: APScheduler持久化定时任务 + SQL检测按钮 + 定时时间醒目红色
1. ticket_scheduler.py: 纯asyncio→APScheduler+SQLite持久化 - 服务重启后定时任务不丢失 - 启动时自动恢复scheduled状态工单 - shutdown时优雅关闭scheduler 2. TicketCreatePage.tsx: - 提交按钮旁新增SQL检测按钮(Shield图标) - 验证通过前提交按钮置灰+提示'请先通过SQL检测' - SQL内容变化自动重置验证状态 - 定时时间改为醒目红色字体(text-red-500 font-semibold) 3. main.py: lifespan中启动APScheduler+恢复定时工单
This commit is contained in:
1 parent
ffb3d24e2a
commit
a8c92fe7ae
3 files changed
+208
-73
No files matched your search
@@ -111,6 +111,8 @@ export default function TicketCreatePage() {
|
||||
|
||||
// -- Inline audit result --
|
||||
const [auditResult, setAuditResult] = useState<AuditResult | null>(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validated, setValidated] = useState(false);
|
||||
|
||||
// -- Schema Browser State --
|
||||
const [tables, setTables] = useState<TableMeta[]>([]);
|
||||
@@ -249,37 +251,53 @@ export default function TicketCreatePage() {
|
||||
const handleSqlChange = useCallback((val: string) => {
|
||||
setSqlContent(val);
|
||||
if (auditResult) setAuditResult(null);
|
||||
}, [auditResult]);
|
||||
if (validated) setValidated(false);
|
||||
}, [auditResult, validated]);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Submit: audit + create in one click */
|
||||
/* Validate: SQL 检测(不提交) */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
const handleValidate = useCallback(async () => {
|
||||
if (!sqlContent.trim()) { setError('请填写 SQL 语句'); return; }
|
||||
if (!instanceId) { setError('请选择目标实例'); return; }
|
||||
if (!database) { setError('请选择数据库'); return; }
|
||||
|
||||
setError(''); setAuditResult(null); setValidating(true);
|
||||
try {
|
||||
const result = await ticketApi.audit({
|
||||
sql_content: sqlContent, instance_id: Number(instanceId), database,
|
||||
}) as unknown as AuditResult;
|
||||
setAuditResult(result);
|
||||
|
||||
const hasErrors = result.findings?.some((f) => f.level === 'error');
|
||||
if (hasErrors) {
|
||||
setError('SQL 审核未通过,请修改后重新检测');
|
||||
setValidated(false);
|
||||
} else {
|
||||
setValidated(true);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { detail?: string } } };
|
||||
setError(err?.response?.data?.detail || 'SQL 检测失败');
|
||||
setValidated(false);
|
||||
} finally { setValidating(false); }
|
||||
}, [sqlContent, instanceId, database]);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Submit: 验证通过后直接提交工单 */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
// 1. 表单校验
|
||||
if (!title.trim()) { setError('请填写工单标题'); return; }
|
||||
if (!instanceId) { setError('请选择目标实例'); return; }
|
||||
if (!database) { setError('请选择数据库'); return; }
|
||||
if (!sqlContent.trim()) { setError('请填写 SQL 语句'); return; }
|
||||
if (!reason.trim()) { setError('请填写变更原因'); return; }
|
||||
|
||||
setError(''); setAuditResult(null); setSubmitting(true);
|
||||
setError(''); setSubmitting(true);
|
||||
|
||||
try {
|
||||
// 2. 先审核
|
||||
const result = await ticketApi.audit({
|
||||
sql_content: sqlContent, instance_id: Number(instanceId), database,
|
||||
}) as unknown as AuditResult;
|
||||
setAuditResult(result);
|
||||
|
||||
// 3. 有 error 级别违规则阻止提交
|
||||
const hasErrors = result.findings?.some((f) => f.level === 'error');
|
||||
if (hasErrors) {
|
||||
setError('SQL 审核未通过,请修改后重新提交');
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 提交工单
|
||||
const inst = instances.find((i) => i.id === Number(instanceId));
|
||||
await ticketApi.create({
|
||||
title: title.trim(),
|
||||
@@ -298,7 +316,7 @@ export default function TicketCreatePage() {
|
||||
const msg = Array.isArray(raw) ? raw.map((v) => v.msg).join('; ') : (raw || '提交失败,请稍后重试');
|
||||
setError(msg);
|
||||
} finally { setSubmitting(false); }
|
||||
}, [title, sqlContent, instanceId, database, reason, instances, navigate]);
|
||||
}, [title, sqlContent, instanceId, database, reason, instances, navigate, enableSchedule, scheduledAt]);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Derived values */
|
||||
@@ -718,7 +736,7 @@ export default function TicketCreatePage() {
|
||||
min={new Date().toISOString().slice(0, 16)} />
|
||||
)}
|
||||
{enableSchedule && scheduledAt && (
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
||||
<span className="text-sm font-semibold text-red-500">
|
||||
将于 {new Date(scheduledAt).toLocaleString('zh-CN')} 执行
|
||||
</span>
|
||||
)}
|
||||
@@ -795,13 +813,23 @@ export default function TicketCreatePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* === Submit Button === */}
|
||||
<div className="flex justify-end">
|
||||
<button onClick={handleSubmit} disabled={submitting}
|
||||
className="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{/* === Bottom Actions === */}
|
||||
<div className="flex justify-end items-center gap-3">
|
||||
<button onClick={handleValidate}
|
||||
disabled={validating || !sqlContent.trim() || !instanceId || !database}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] rounded-lg text-sm font-medium transition-all hover:bg-[var(--dg-bg-hover)] disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
{validating
|
||||
? <><Loader2 className="w-4 h-4 animate-spin" /> 检测中...</>
|
||||
: <><Shield className="w-4 h-4" /> SQL 检测</>}
|
||||
</button>
|
||||
<button onClick={handleSubmit}
|
||||
disabled={submitting || !validated}
|
||||
className="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-all disabled:opacity-40 disabled:cursor-not-allowed">
|
||||
{submitting
|
||||
? <><Loader2 className="w-4 h-4 animate-spin" /> 审核提交中...</>
|
||||
: <><Shield className="w-4 h-4" /> 提交工单</>}
|
||||
? <><Loader2 className="w-4 h-4 animate-spin" /> 提交中...</>
|
||||
: validated
|
||||
? <><Check className="w-4 h-4" /> 提交工单</>
|
||||
: <><Check className="w-4 h-4" /> 请先通过 SQL 检测</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user