mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 21:03:57 +08:00
Improve biz-compare UX with guided create, sheet toggles, and per-run delete.
Also sticky template field headers and biz-state pause/start endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9b3e98ede5
commit
3d2489041a
12 changed files with 669 additions and 155 deletions
|
|
@ -1248,11 +1248,22 @@ def _job_out(j: BizCompareJob) -> dict[str, Any]:
|
||||||
"after_batch_id": j.after_batch_id,
|
"after_batch_id": j.after_batch_id,
|
||||||
"mode": j.mode,
|
"mode": j.mode,
|
||||||
"status": j.status,
|
"status": j.status,
|
||||||
|
"enabled_sheet_ids": _str_list(getattr(j, "enabled_sheet_ids", None)),
|
||||||
"note": j.note,
|
"note": j.note,
|
||||||
"updated_at": j.updated_at.isoformat() + "Z" if j.updated_at else None,
|
"updated_at": j.updated_at.isoformat() + "Z" if j.updated_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_enabled_sheets(
|
||||||
|
sheets_cfg: list[dict[str, Any]], enabled_sheet_ids: list[str] | None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Empty enabled list → all sheets; else keep matching sheet_id only."""
|
||||||
|
allowed = set(_str_list(enabled_sheet_ids))
|
||||||
|
if not allowed:
|
||||||
|
return sheets_cfg
|
||||||
|
return [s for s in sheets_cfg if sheet_key(s) in allowed]
|
||||||
|
|
||||||
|
|
||||||
def list_jobs(db: Session) -> list[dict[str, Any]]:
|
def list_jobs(db: Session) -> list[dict[str, Any]]:
|
||||||
rows = db.query(BizCompareJob).order_by(BizCompareJob.updated_at.desc()).all()
|
rows = db.query(BizCompareJob).order_by(BizCompareJob.updated_at.desc()).all()
|
||||||
return [_job_out(j) for j in rows]
|
return [_job_out(j) for j in rows]
|
||||||
|
|
@ -1267,6 +1278,7 @@ def create_job(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
else:
|
else:
|
||||||
if not db.get(BizCompareTemplate, template_id):
|
if not db.get(BizCompareTemplate, template_id):
|
||||||
raise HTTPException(status_code=404, detail="template_not_found")
|
raise HTTPException(status_code=404, detail="template_not_found")
|
||||||
|
enabled = _str_list(body.get("enabled_sheet_ids"))
|
||||||
j = BizCompareJob(
|
j = BizCompareJob(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
name=str(body.get("name") or "compare")[:256],
|
name=str(body.get("name") or "compare")[:256],
|
||||||
|
|
@ -1278,6 +1290,7 @@ def create_job(db: Session, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
after_batch_id=str(body.get("after_batch_id") or ""),
|
after_batch_id=str(body.get("after_batch_id") or ""),
|
||||||
mode=str(body.get("mode") or "manual")[:16],
|
mode=str(body.get("mode") or "manual")[:16],
|
||||||
status="ready",
|
status="ready",
|
||||||
|
enabled_sheet_ids=enabled,
|
||||||
note=str(body.get("note") or "")[:512],
|
note=str(body.get("note") or "")[:512],
|
||||||
created_at=_utcnow(),
|
created_at=_utcnow(),
|
||||||
updated_at=_utcnow(),
|
updated_at=_utcnow(),
|
||||||
|
|
@ -1307,6 +1320,8 @@ def update_job(db: Session, job_id: str, body: dict[str, Any]) -> dict[str, Any]
|
||||||
):
|
):
|
||||||
if key in body and body.get(key) is not None:
|
if key in body and body.get(key) is not None:
|
||||||
setattr(j, key, str(body.get(key) or ""))
|
setattr(j, key, str(body.get(key) or ""))
|
||||||
|
if "enabled_sheet_ids" in body:
|
||||||
|
j.enabled_sheet_ids = _str_list(body.get("enabled_sheet_ids"))
|
||||||
j.updated_at = _utcnow()
|
j.updated_at = _utcnow()
|
||||||
db.commit()
|
db.commit()
|
||||||
return _job_out(j)
|
return _job_out(j)
|
||||||
|
|
@ -1328,6 +1343,20 @@ def delete_job(db: Session, job_id: str) -> None:
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_run(db: Session, run_id: str) -> dict[str, Any]:
|
||||||
|
"""Delete one compare run and its diffs; leave the job intact."""
|
||||||
|
r = db.get(BizCompareRun, run_id)
|
||||||
|
if not r:
|
||||||
|
raise HTTPException(status_code=404, detail="run_not_found")
|
||||||
|
job_id = str(r.job_id or "")
|
||||||
|
db.query(BizCompareDiff).filter(BizCompareDiff.run_id == run_id).delete(
|
||||||
|
synchronize_session=False
|
||||||
|
)
|
||||||
|
db.delete(r)
|
||||||
|
db.commit()
|
||||||
|
return {"ok": True, "job_id": job_id, "run_id": run_id}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_after_batch(db: Session, job: BizCompareJob) -> str:
|
def _resolve_after_batch(db: Session, job: BizCompareJob) -> str:
|
||||||
if job.mode != "auto":
|
if job.mode != "auto":
|
||||||
return str(job.after_batch_id or "")
|
return str(job.after_batch_id or "")
|
||||||
|
|
@ -1423,6 +1452,9 @@ def run_compare(db: Session, job_id: str, *, force_after_batch_id: str = "") ->
|
||||||
sheets_cfg = template_metrics(tpl)
|
sheets_cfg = template_metrics(tpl)
|
||||||
if not sheets_cfg:
|
if not sheets_cfg:
|
||||||
raise HTTPException(status_code=400, detail="template_has_no_metrics")
|
raise HTTPException(status_code=400, detail="template_has_no_metrics")
|
||||||
|
sheets_cfg = _filter_enabled_sheets(sheets_cfg, getattr(j, "enabled_sheet_ids", None))
|
||||||
|
if not sheets_cfg:
|
||||||
|
raise HTTPException(status_code=400, detail="no_enabled_sheets")
|
||||||
|
|
||||||
pmap = _port_map_dict(db, j.mapping_id)
|
pmap = _port_map_dict(db, j.mapping_id)
|
||||||
norm_rules = template_iface_normalize(tpl)
|
norm_rules = template_iface_normalize(tpl)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ def apply_biz_state_schema(conn: Connection) -> None:
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_task_id ON biz_state_batch (task_id)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_task_id ON biz_state_batch (task_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_command_batch_id ON biz_state_batch_command (batch_id)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_state_batch_command_batch_id ON biz_state_batch_command (batch_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_state_lldp_neighbor_batch_id ON biz_state_lldp_neighbor (batch_id)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_state_lldp_neighbor_batch_id ON biz_state_lldp_neighbor (batch_id)",
|
||||||
|
"ALTER TABLE biz_compare_job ADD COLUMN IF NOT EXISTS enabled_sheet_ids JSON DEFAULT '[]'",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_compare_job_status ON biz_compare_job (status)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_compare_job_status ON biz_compare_job (status)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_compare_run_job_id ON biz_compare_run (job_id)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_compare_run_job_id ON biz_compare_run (job_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS ix_biz_state_vrf_route_batch_id ON biz_state_vrf_route_summary (batch_id)",
|
"CREATE INDEX IF NOT EXISTS ix_biz_state_vrf_route_batch_id ON biz_state_vrf_route_summary (batch_id)",
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,11 @@ def _replace_items(db: Session, task_id: str, items_in: list[dict[str, Any]]) ->
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_task_status(db: Session, task_id: str, status: str) -> dict[str, Any]:
|
||||||
|
"""Set lifecycle status: draft | running | paused | stopped."""
|
||||||
|
return update_task(db, task_id, {"status": status})
|
||||||
|
|
||||||
|
|
||||||
def update_task(db: Session, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
def update_task(db: Session, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
task = db.get(BizStateTask, task_id)
|
task = db.get(BizStateTask, task_id)
|
||||||
if not task:
|
if not task:
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,18 @@ def api_patch_task(
|
||||||
return svc.update_task(db, task_id, body.model_dump(exclude_unset=True))
|
return svc.update_task(db, task_id, body.model_dump(exclude_unset=True))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tasks/{task_id}/pause")
|
||||||
|
def api_pause_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
|
"""Pause periodic schedule; manual collect remains allowed."""
|
||||||
|
return svc.set_task_status(db, task_id, "paused")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tasks/{task_id}/start")
|
||||||
|
def api_start_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
|
"""Enable periodic schedule (requires bindings for non–cutover-HF tasks)."""
|
||||||
|
return svc.set_task_status(db, task_id, "running")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/tasks/{task_id}")
|
@router.delete("/tasks/{task_id}")
|
||||||
def api_delete_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
def api_delete_task(task_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
svc.delete_task(db, task_id)
|
svc.delete_task(db, task_id)
|
||||||
|
|
@ -374,6 +386,8 @@ class CompareJobIn(BaseModel):
|
||||||
before_batch_id: str = ""
|
before_batch_id: str = ""
|
||||||
after_batch_id: str = ""
|
after_batch_id: str = ""
|
||||||
mode: str = "manual"
|
mode: str = "manual"
|
||||||
|
# Empty = all template sheets; non-empty = only these sheet_id values
|
||||||
|
enabled_sheet_ids: list[str] = Field(default_factory=list)
|
||||||
note: str = ""
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -478,6 +492,11 @@ def api_get_run(run_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
return cmp_svc.get_run(db, run_id)
|
return cmp_svc.get_run(db, run_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/compare/runs/{run_id}")
|
||||||
|
def api_delete_run(run_id: str, db: Session = Depends(get_db)) -> dict[str, Any]:
|
||||||
|
return cmp_svc.delete_run(db, run_id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/compare/runs/{run_id}/diffs")
|
@router.get("/compare/runs/{run_id}/diffs")
|
||||||
def api_list_run_diffs(
|
def api_list_run_diffs(
|
||||||
run_id: str,
|
run_id: str,
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,8 @@ class BizCompareJob(Base):
|
||||||
# manual: fixed after_batch; auto: after_batch_id empty → use latest after task batch
|
# manual: fixed after_batch; auto: after_batch_id empty → use latest after task batch
|
||||||
mode: Mapped[str] = mapped_column(String(16), default="manual", index=True)
|
mode: Mapped[str] = mapped_column(String(16), default="manual", index=True)
|
||||||
status: Mapped[str] = mapped_column(String(32), default="draft", index=True) # draft|ready|auto
|
status: Mapped[str] = mapped_column(String(32), default="draft", index=True) # draft|ready|auto
|
||||||
|
# Empty = all template sheets; non-empty = only these sheet_id values
|
||||||
|
enabled_sheet_ids: Mapped[list] = mapped_column(_JsonType, default=list)
|
||||||
note: Mapped[str] = mapped_column(String(512), default="")
|
note: Mapped[str] = mapped_column(String(512), default="")
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive)
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,20 @@ class CompareSheetDefaultsTests(unittest.TestCase):
|
||||||
isis4 = next(s for s in parsed if sheet_key(s) == "isis_adjacency.ipv4")
|
isis4 = next(s for s in parsed if sheet_key(s) == "isis_adjacency.ipv4")
|
||||||
self.assertTrue(str(isis4.get("title") or "").strip())
|
self.assertTrue(str(isis4.get("title") or "").strip())
|
||||||
|
|
||||||
|
def test_filter_enabled_sheets_empty_means_all(self) -> None:
|
||||||
|
from netx_api.biz_state.compare_service import _filter_enabled_sheets, sheet_key
|
||||||
|
|
||||||
|
sheets = [
|
||||||
|
{"sheet_id": "isis_adjacency.ipv4", "metric_id": "isis_adjacency"},
|
||||||
|
{"sheet_id": "isis_adjacency.ipv6", "metric_id": "isis_adjacency"},
|
||||||
|
{"sheet_id": "arp", "metric_id": "arp"},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(_filter_enabled_sheets(sheets, None)), 3)
|
||||||
|
self.assertEqual(len(_filter_enabled_sheets(sheets, [])), 3)
|
||||||
|
only = _filter_enabled_sheets(sheets, ["isis_adjacency.ipv4", "arp"])
|
||||||
|
self.assertEqual([sheet_key(s) for s in only], ["isis_adjacency.ipv4", "arp"])
|
||||||
|
self.assertEqual(_filter_enabled_sheets(sheets, ["missing"]), [])
|
||||||
|
|
||||||
def test_arp_default_sheet_has_row_filters(self) -> None:
|
def test_arp_default_sheet_has_row_filters(self) -> None:
|
||||||
from netx_api.biz_state.compare_service import _default_sheet_for_metric
|
from netx_api.biz_state.compare_service import _default_sheet_for_metric
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ const en = {
|
||||||
colConnect: "Connect",
|
colConnect: "Connect",
|
||||||
start: "Start schedule",
|
start: "Start schedule",
|
||||||
pause: "Pause",
|
pause: "Pause",
|
||||||
|
statusPaused: "Paused",
|
||||||
scheduleEnabled: "Enable schedule",
|
scheduleEnabled: "Enable schedule",
|
||||||
scheduleOn: "Scheduled",
|
scheduleOn: "Scheduled",
|
||||||
scheduleOff: "Manual",
|
scheduleOff: "Manual",
|
||||||
|
|
@ -198,7 +199,7 @@ const en = {
|
||||||
confirmDelete: "Delete this task and all batches?",
|
confirmDelete: "Delete this task and all batches?",
|
||||||
deleted: "Deleted",
|
deleted: "Deleted",
|
||||||
started: "Schedule enabled",
|
started: "Schedule enabled",
|
||||||
paused: "Schedule off (manual only)",
|
paused: "Schedule paused (manual only)",
|
||||||
collecting: "Collecting",
|
collecting: "Collecting",
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
interval: "Interval",
|
interval: "Interval",
|
||||||
|
|
@ -293,6 +294,10 @@ const en = {
|
||||||
templateDeleted: "Template deleted",
|
templateDeleted: "Template deleted",
|
||||||
confirmDeleteTemplate: "Delete this compare template?",
|
confirmDeleteTemplate: "Delete this compare template?",
|
||||||
confirmDeleteJob: "Delete this compare job and all runs?",
|
confirmDeleteJob: "Delete this compare job and all runs?",
|
||||||
|
confirmDeleteRun: "Delete this compare run? Job config is kept.",
|
||||||
|
deleteJob: "Delete job",
|
||||||
|
deleteRun: "Delete run",
|
||||||
|
runDeleted: "Compare run deleted",
|
||||||
emptyTemplates: "No templates yet. Create one or import a JSON file.",
|
emptyTemplates: "No templates yet. Create one or import a JSON file.",
|
||||||
exportTemplate: "Export",
|
exportTemplate: "Export",
|
||||||
importTemplate: "Import",
|
importTemplate: "Import",
|
||||||
|
|
@ -406,6 +411,21 @@ const en = {
|
||||||
sameAsBefore: "Same as before / select…",
|
sameAsBefore: "Same as before / select…",
|
||||||
autoHint: "auto: new after-task batches trigger compare",
|
autoHint: "auto: new after-task batches trigger compare",
|
||||||
createCompare: "Create job",
|
createCompare: "Create job",
|
||||||
|
createSteps: "Create steps",
|
||||||
|
stepBasics: "Basics",
|
||||||
|
stepSheets: "Sheets",
|
||||||
|
stepBatches: "Batches",
|
||||||
|
stepMapping: "Port mapping",
|
||||||
|
next: "Next",
|
||||||
|
back: "Back",
|
||||||
|
enabledSheets: "Compare sheets",
|
||||||
|
enabledSheetsHint: "Toggle sheets; all on by default. Off sheets are skipped.",
|
||||||
|
noTemplateSheets: "This template has no sheets",
|
||||||
|
needJobName: "Enter a job name",
|
||||||
|
needTemplate: "Select a compare template",
|
||||||
|
needSheets: "Enable at least one sheet",
|
||||||
|
needBeforeBatch: "Select before task and batch",
|
||||||
|
needAfterBatch: "Select after batch",
|
||||||
runNow: "Run now",
|
runNow: "Run now",
|
||||||
saveJob: "Save config",
|
saveJob: "Save config",
|
||||||
jobSaved: "Job config saved",
|
jobSaved: "Job config saved",
|
||||||
|
|
@ -414,13 +434,14 @@ const en = {
|
||||||
detail: "Details",
|
detail: "Details",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
tabConfig: "Config / batches",
|
tabConfig: "Job config",
|
||||||
tabResult: "Result",
|
tabResult: "Result",
|
||||||
runs: "Run history",
|
runs: "Run history",
|
||||||
noRuns: "No runs yet — run a compare first",
|
noRuns: "No runs yet — run a compare first",
|
||||||
result: "Result",
|
result: "Result",
|
||||||
pickBatchRun: "Select compare batch",
|
pickBatchRun: "Select compare run",
|
||||||
pickRun: "Select batch…",
|
pickRun: "Select run…",
|
||||||
|
runCount: "{{n}} runs",
|
||||||
resultEmpty: "No matching diff rows",
|
resultEmpty: "No matching diff rows",
|
||||||
resultFilterPh: "Filter key / values…",
|
resultFilterPh: "Filter key / values…",
|
||||||
kindAll: "All",
|
kindAll: "All",
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ const zh = {
|
||||||
colConnect: "连通",
|
colConnect: "连通",
|
||||||
start: "启动周期",
|
start: "启动周期",
|
||||||
pause: "暂停",
|
pause: "暂停",
|
||||||
|
statusPaused: "已暂停",
|
||||||
scheduleEnabled: "启用周期调度",
|
scheduleEnabled: "启用周期调度",
|
||||||
scheduleOn: "周期",
|
scheduleOn: "周期",
|
||||||
scheduleOff: "手动",
|
scheduleOff: "手动",
|
||||||
|
|
@ -198,7 +199,7 @@ const zh = {
|
||||||
confirmDelete: "删除该业务监控任务及所有批次?",
|
confirmDelete: "删除该业务监控任务及所有批次?",
|
||||||
deleted: "已删除",
|
deleted: "已删除",
|
||||||
started: "已启用周期采集",
|
started: "已启用周期采集",
|
||||||
paused: "已关闭周期(仅手动采集)",
|
paused: "已暂停周期(仅手动采集)",
|
||||||
collecting: "采集中",
|
collecting: "采集中",
|
||||||
detail: "详情",
|
detail: "详情",
|
||||||
interval: "采集周期",
|
interval: "采集周期",
|
||||||
|
|
@ -292,7 +293,11 @@ const zh = {
|
||||||
templateSaved: "模板已保存",
|
templateSaved: "模板已保存",
|
||||||
templateDeleted: "模板已删除",
|
templateDeleted: "模板已删除",
|
||||||
confirmDeleteTemplate: "删除该比对模板?",
|
confirmDeleteTemplate: "删除该比对模板?",
|
||||||
confirmDeleteJob: "删除该比对任务及全部运行记录?",
|
confirmDeleteJob: "删除该比对任务及全部比对记录?",
|
||||||
|
confirmDeleteRun: "删除本次比对结果?任务配置将保留。",
|
||||||
|
deleteJob: "删除任务",
|
||||||
|
deleteRun: "删除本次",
|
||||||
|
runDeleted: "比对记录已删除",
|
||||||
emptyTemplates: "暂无模板。可新建,或从 JSON 文件导入。",
|
emptyTemplates: "暂无模板。可新建,或从 JSON 文件导入。",
|
||||||
exportTemplate: "导出",
|
exportTemplate: "导出",
|
||||||
importTemplate: "导入",
|
importTemplate: "导入",
|
||||||
|
|
@ -405,6 +410,21 @@ const zh = {
|
||||||
sameAsBefore: "同操作前 / 选择…",
|
sameAsBefore: "同操作前 / 选择…",
|
||||||
autoHint: "auto:操作后任务新批次到达会自动跑比对",
|
autoHint: "auto:操作后任务新批次到达会自动跑比对",
|
||||||
createCompare: "创建比对任务",
|
createCompare: "创建比对任务",
|
||||||
|
createSteps: "创建步骤",
|
||||||
|
stepBasics: "基本信息",
|
||||||
|
stepSheets: "对比项",
|
||||||
|
stepBatches: "批次",
|
||||||
|
stepMapping: "端口映射",
|
||||||
|
next: "下一步",
|
||||||
|
back: "上一步",
|
||||||
|
enabledSheets: "对比项",
|
||||||
|
enabledSheetsHint: "点击开关;默认全部开启。关闭的项不参与本次比对。",
|
||||||
|
noTemplateSheets: "该模板暂无对比表",
|
||||||
|
needJobName: "请填写任务名称",
|
||||||
|
needTemplate: "请选择比对模板",
|
||||||
|
needSheets: "请至少开启一项对比",
|
||||||
|
needBeforeBatch: "请选择操作前任务与批次",
|
||||||
|
needAfterBatch: "请选择操作后批次",
|
||||||
runNow: "立即比对",
|
runNow: "立即比对",
|
||||||
saveJob: "保存配置",
|
saveJob: "保存配置",
|
||||||
jobSaved: "任务配置已保存",
|
jobSaved: "任务配置已保存",
|
||||||
|
|
@ -413,13 +433,14 @@ const zh = {
|
||||||
detail: "详情",
|
detail: "详情",
|
||||||
edit: "编辑",
|
edit: "编辑",
|
||||||
delete: "删除",
|
delete: "删除",
|
||||||
tabConfig: "配置 / 批次",
|
tabConfig: "任务配置",
|
||||||
tabResult: "比对结果",
|
tabResult: "比对结果",
|
||||||
runs: "历史 Run",
|
runs: "历史比对",
|
||||||
noRuns: "尚无比对记录,请先执行比对",
|
noRuns: "尚无比对记录,请先执行比对",
|
||||||
result: "比对结果",
|
result: "比对结果",
|
||||||
pickBatchRun: "选择比对批次",
|
pickBatchRun: "选择比对记录",
|
||||||
pickRun: "选择批次…",
|
pickRun: "选择比对记录…",
|
||||||
|
runCount: "{{n}} 次",
|
||||||
resultEmpty: "无匹配失败/结果行",
|
resultEmpty: "无匹配失败/结果行",
|
||||||
resultFilterPh: "筛选身份 / 字段值…",
|
resultFilterPh: "筛选身份 / 字段值…",
|
||||||
kindAll: "全部",
|
kindAll: "全部",
|
||||||
|
|
|
||||||
|
|
@ -10821,6 +10821,17 @@ html.login-page--paused .login-page__flare {
|
||||||
max-height: min(55vh, 520px);
|
max-height: min(55vh, 520px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.ct-field-table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
.ct-field-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(15, 23, 42, 0.98) !important;
|
||||||
|
box-shadow: 0 1px 0 rgba(148, 163, 184, 0.22);
|
||||||
|
}
|
||||||
.ct-field-table th,
|
.ct-field-table th,
|
||||||
.ct-field-table td {
|
.ct-field-table td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
@ -11314,13 +11325,19 @@ html.login-page--paused .login-page__flare {
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-board__run-select {
|
.bs-cmp-board__run-select {
|
||||||
flex: 0 1 280px;
|
flex: 0 1 360px;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
max-width: 320px;
|
max-width: 420px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-cmp-board__run-count {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-cmp-board__actions {
|
.bs-cmp-board__actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
bizCompareCreateMapping,
|
bizCompareCreateMapping,
|
||||||
bizCompareCreateTemplate,
|
bizCompareCreateTemplate,
|
||||||
bizCompareDeleteJob,
|
bizCompareDeleteJob,
|
||||||
|
bizCompareDeleteRun,
|
||||||
bizCompareDeleteTemplate,
|
bizCompareDeleteTemplate,
|
||||||
bizCompareDownloadRun,
|
bizCompareDownloadRun,
|
||||||
bizCompareGetRun,
|
bizCompareGetRun,
|
||||||
|
|
@ -35,6 +36,8 @@ import { jobChipColor, NmStatusChip } from "./nmChips";
|
||||||
type PageTab = "templates" | "jobs";
|
type PageTab = "templates" | "jobs";
|
||||||
type JobDetailTab = "config" | "result";
|
type JobDetailTab = "config" | "result";
|
||||||
type KindFilter = "diff" | "all" | "added" | "removed" | "changed" | "unchanged";
|
type KindFilter = "diff" | "all" | "added" | "removed" | "changed" | "unchanged";
|
||||||
|
type CreateJobStep = 0 | 1 | 2 | 3;
|
||||||
|
const CREATE_JOB_STEPS = 4;
|
||||||
|
|
||||||
type TaskOpt = { id: string; ne_name: string; ne_ip: string; vendor: string };
|
type TaskOpt = { id: string; ne_name: string; ne_ip: string; vendor: string };
|
||||||
type BatchOpt = { id: string; status: string; row_count: number; started_at?: string | null };
|
type BatchOpt = { id: string; status: string; row_count: number; started_at?: string | null };
|
||||||
|
|
@ -101,6 +104,7 @@ type Job = {
|
||||||
after_batch_id: string;
|
after_batch_id: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
enabled_sheet_ids?: string[];
|
||||||
note?: string;
|
note?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -774,10 +778,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
|
|
||||||
// job create / detail
|
// job create / detail
|
||||||
const [jobCreateOpen, setJobCreateOpen] = useState(false);
|
const [jobCreateOpen, setJobCreateOpen] = useState(false);
|
||||||
|
const [createStep, setCreateStep] = useState<CreateJobStep>(0);
|
||||||
const [jobId, setJobId] = useState("");
|
const [jobId, setJobId] = useState("");
|
||||||
const [jobDetailTab, setJobDetailTab] = useState<JobDetailTab>("config");
|
const [jobDetailTab, setJobDetailTab] = useState<JobDetailTab>("config");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [templateId, setTemplateId] = useState("");
|
const [templateId, setTemplateId] = useState("");
|
||||||
|
const [enabledSheetIds, setEnabledSheetIds] = useState<string[]>([]);
|
||||||
const [mappingId, setMappingId] = useState("");
|
const [mappingId, setMappingId] = useState("");
|
||||||
const [beforeTaskId, setBeforeTaskId] = useState("");
|
const [beforeTaskId, setBeforeTaskId] = useState("");
|
||||||
const [afterTaskId, setAfterTaskId] = useState("");
|
const [afterTaskId, setAfterTaskId] = useState("");
|
||||||
|
|
@ -873,6 +879,66 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
});
|
});
|
||||||
}, [templates, debouncedListKw]);
|
}, [templates, debouncedListKw]);
|
||||||
|
|
||||||
|
const selectedJobTemplate = useMemo(
|
||||||
|
() => templates.find((x) => x.id === templateId) || null,
|
||||||
|
[templates, templateId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const jobTemplateSheets = useMemo(
|
||||||
|
() => (selectedJobTemplate ? templateSheets(selectedJobTemplate) : []),
|
||||||
|
[selectedJobTemplate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const jobSheetAllIds = useMemo(
|
||||||
|
() => jobTemplateSheets.map((s) => sheetIdentity(s)).filter(Boolean),
|
||||||
|
[jobTemplateSheets],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isJobSheetOn = useCallback(
|
||||||
|
(sid: string) => {
|
||||||
|
if (!enabledSheetIds.length) return true;
|
||||||
|
return enabledSheetIds.includes(sid);
|
||||||
|
},
|
||||||
|
[enabledSheetIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const enabledJobSheetCount = useMemo(() => {
|
||||||
|
if (!jobSheetAllIds.length) return 0;
|
||||||
|
if (!enabledSheetIds.length) return jobSheetAllIds.length;
|
||||||
|
return jobSheetAllIds.filter((id) => enabledSheetIds.includes(id)).length;
|
||||||
|
}, [jobSheetAllIds, enabledSheetIds]);
|
||||||
|
|
||||||
|
const toggleJobSheet = useCallback(
|
||||||
|
(sid: string) => {
|
||||||
|
const all = jobSheetAllIds;
|
||||||
|
if (!all.length) return;
|
||||||
|
const currentlyOn = !enabledSheetIds.length
|
||||||
|
? [...all]
|
||||||
|
: enabledSheetIds.filter((id) => all.includes(id));
|
||||||
|
const next = currentlyOn.includes(sid)
|
||||||
|
? currentlyOn.filter((id) => id !== sid)
|
||||||
|
: [...currentlyOn, sid];
|
||||||
|
// Empty list means "all on" (new template sheets auto-included)
|
||||||
|
setEnabledSheetIds(next.length === all.length ? [] : next);
|
||||||
|
},
|
||||||
|
[jobSheetAllIds, enabledSheetIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setJobTemplateAndSheets = useCallback(
|
||||||
|
(nextTplId: string, presetIds?: string[] | null) => {
|
||||||
|
setTemplateId(nextTplId);
|
||||||
|
const tpl = templates.find((x) => x.id === nextTplId);
|
||||||
|
const all = tpl ? templateSheets(tpl).map((s) => sheetIdentity(s)).filter(Boolean) : [];
|
||||||
|
if (presetIds && presetIds.length) {
|
||||||
|
const kept = presetIds.filter((id) => all.includes(id));
|
||||||
|
setEnabledSheetIds(kept.length === all.length ? [] : kept);
|
||||||
|
} else {
|
||||||
|
setEnabledSheetIds([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[templates],
|
||||||
|
);
|
||||||
|
|
||||||
const activeTplSheet = tplSheets[tplSheetIdx] || null;
|
const activeTplSheet = tplSheets[tplSheetIdx] || null;
|
||||||
const activeTplFields = useMemo(() => {
|
const activeTplFields = useMemo(() => {
|
||||||
if (!activeTplSheet) return [];
|
if (!activeTplSheet) return [];
|
||||||
|
|
@ -1444,8 +1510,18 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetJobForm = (preset?: Partial<Job>) => {
|
const resetJobForm = (preset?: Partial<Job>) => {
|
||||||
|
const tplId = preset?.template_id || templates[0]?.id || "";
|
||||||
setName(preset?.name || t("bizCompare.defaultJobName"));
|
setName(preset?.name || t("bizCompare.defaultJobName"));
|
||||||
setTemplateId(preset?.template_id || templates[0]?.id || "");
|
setTemplateId(tplId);
|
||||||
|
const tpl = templates.find((x) => x.id === tplId);
|
||||||
|
const all = tpl ? templateSheets(tpl).map((s) => sheetIdentity(s)).filter(Boolean) : [];
|
||||||
|
const presetIds = (preset?.enabled_sheet_ids || []).map(String).filter(Boolean);
|
||||||
|
if (presetIds.length) {
|
||||||
|
const kept = presetIds.filter((id) => all.includes(id));
|
||||||
|
setEnabledSheetIds(kept.length === all.length ? [] : kept);
|
||||||
|
} else {
|
||||||
|
setEnabledSheetIds([]);
|
||||||
|
}
|
||||||
setMappingId(preset?.mapping_id || "");
|
setMappingId(preset?.mapping_id || "");
|
||||||
setBeforeTaskId(preset?.before_task_id || "");
|
setBeforeTaskId(preset?.before_task_id || "");
|
||||||
setAfterTaskId(preset?.after_task_id || "");
|
setAfterTaskId(preset?.after_task_id || "");
|
||||||
|
|
@ -1460,15 +1536,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreateJob = () => {
|
const jobConfigBody = () => ({
|
||||||
resetJobForm();
|
|
||||||
setJobCreateOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createJob = async () => {
|
|
||||||
setBusy(true);
|
|
||||||
try {
|
|
||||||
const j = await bizCompareCreateJob({
|
|
||||||
name,
|
name,
|
||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
mapping_id: mappingId,
|
mapping_id: mappingId,
|
||||||
|
|
@ -1477,9 +1545,76 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
before_batch_id: beforeBatchId,
|
before_batch_id: beforeBatchId,
|
||||||
after_batch_id: mode === "manual" ? afterBatchId : "",
|
after_batch_id: mode === "manual" ? afterBatchId : "",
|
||||||
mode,
|
mode,
|
||||||
|
enabled_sheet_ids: enabledSheetIds,
|
||||||
});
|
});
|
||||||
showOk(t("bizCompare.created"));
|
|
||||||
|
const closeCreateJob = () => {
|
||||||
setJobCreateOpen(false);
|
setJobCreateOpen(false);
|
||||||
|
setCreateStep(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateJob = () => {
|
||||||
|
resetJobForm();
|
||||||
|
setCreateStep(0);
|
||||||
|
setJobCreateOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAdvanceCreateStep = (step: CreateJobStep): boolean => {
|
||||||
|
if (step === 0) return Boolean(name.trim() && templateId);
|
||||||
|
if (step === 1) return enabledJobSheetCount > 0;
|
||||||
|
if (step === 2) {
|
||||||
|
if (!beforeTaskId || !beforeBatchId) return false;
|
||||||
|
if (mode === "manual" && !afterBatchId) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createStepBlockReason = (step: CreateJobStep): string | null => {
|
||||||
|
if (step === 0) {
|
||||||
|
if (!name.trim()) return t("bizCompare.needJobName");
|
||||||
|
if (!templateId) return t("bizCompare.needTemplate");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (step === 1) {
|
||||||
|
if (!enabledJobSheetCount) return t("bizCompare.needSheets");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (step === 2) {
|
||||||
|
if (!beforeTaskId || !beforeBatchId) return t("bizCompare.needBeforeBatch");
|
||||||
|
if (mode === "manual" && !afterBatchId) return t("bizCompare.needAfterBatch");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateNext = () => {
|
||||||
|
const reason = createStepBlockReason(createStep);
|
||||||
|
if (reason) {
|
||||||
|
showError(reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCreateStep((s) => Math.min(CREATE_JOB_STEPS - 1, (s + 1) as CreateJobStep) as CreateJobStep);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateBack = () => {
|
||||||
|
setCreateStep((s) => Math.max(0, s - 1) as CreateJobStep);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createJob = async () => {
|
||||||
|
const reason =
|
||||||
|
createStepBlockReason(0) ||
|
||||||
|
createStepBlockReason(1) ||
|
||||||
|
createStepBlockReason(2);
|
||||||
|
if (reason) {
|
||||||
|
showError(reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const j = await bizCompareCreateJob(jobConfigBody());
|
||||||
|
showOk(t("bizCompare.created"));
|
||||||
|
closeCreateJob();
|
||||||
await refresh();
|
await refresh();
|
||||||
await openJob(String(j.id));
|
await openJob(String(j.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1520,18 +1655,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
|
|
||||||
const saveJobConfig = async () => {
|
const saveJobConfig = async () => {
|
||||||
if (!jobId) return;
|
if (!jobId) return;
|
||||||
|
if (!enabledJobSheetCount) {
|
||||||
|
showError(t("bizCompare.needSheets"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizCompareUpdateJob(jobId, {
|
await bizCompareUpdateJob(jobId, jobConfigBody());
|
||||||
name,
|
|
||||||
template_id: templateId,
|
|
||||||
mapping_id: mappingId,
|
|
||||||
before_task_id: beforeTaskId,
|
|
||||||
after_task_id: afterTaskId || beforeTaskId,
|
|
||||||
before_batch_id: beforeBatchId,
|
|
||||||
after_batch_id: mode === "manual" ? afterBatchId : "",
|
|
||||||
mode,
|
|
||||||
});
|
|
||||||
showOk(t("bizCompare.jobSaved"));
|
showOk(t("bizCompare.jobSaved"));
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1543,15 +1673,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
|
|
||||||
const runNow = async () => {
|
const runNow = async () => {
|
||||||
if (!jobId) return;
|
if (!jobId) return;
|
||||||
|
if (!enabledJobSheetCount) {
|
||||||
|
showError(t("bizCompare.needSheets"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizCompareUpdateJob(jobId, {
|
await bizCompareUpdateJob(jobId, jobConfigBody());
|
||||||
before_batch_id: beforeBatchId,
|
|
||||||
after_batch_id: mode === "manual" ? afterBatchId : "",
|
|
||||||
mode,
|
|
||||||
mapping_id: mappingId,
|
|
||||||
template_id: templateId,
|
|
||||||
});
|
|
||||||
const run = await bizCompareRunJob(jobId);
|
const run = await bizCompareRunJob(jobId);
|
||||||
setRunDetail(run);
|
setRunDetail(run);
|
||||||
setJobDetailTab("result");
|
setJobDetailTab("result");
|
||||||
|
|
@ -1578,36 +1706,120 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderJobForm = (compact = false) => (
|
const removeRun = async (runId: string) => {
|
||||||
<div className="bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
if (!runId) return;
|
||||||
<label className="ui-field ui-field--full">
|
if (!window.confirm(t("bizCompare.confirmDeleteRun"))) return;
|
||||||
<span className="ui-field__label">{t("bizCompare.jobName")}</span>
|
setBusy(true);
|
||||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
try {
|
||||||
</label>
|
const wasCurrent = String(runDetail?.id || "") === runId;
|
||||||
<FieldSelect
|
await bizCompareDeleteRun(runId);
|
||||||
label={t("bizCompare.template")}
|
let nextRuns: typeof runs = [];
|
||||||
value={templateId}
|
if (jobId) {
|
||||||
onChange={(e) => setTemplateId(e.target.value)}
|
const r = await bizCompareListRuns(jobId);
|
||||||
fullWidth
|
nextRuns = r.items || [];
|
||||||
>
|
} else {
|
||||||
{templates.map((tpl) => {
|
nextRuns = (runs || []).filter((r) => String(r.id) !== runId);
|
||||||
const mids = tpl.metric_ids || templateSheets(tpl).map((s) => s.metric_id);
|
}
|
||||||
|
setRuns(nextRuns);
|
||||||
|
if (wasCurrent) {
|
||||||
|
if (nextRuns.length) {
|
||||||
|
await loadRun(String(nextRuns[0].id));
|
||||||
|
} else {
|
||||||
|
setRunDetail(null);
|
||||||
|
setResultSheetId("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showOk(t("bizCompare.runDeleted"));
|
||||||
|
} catch (e) {
|
||||||
|
showError(formatErr(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSheetChips = () => (
|
||||||
|
<div className="bm-create__pane" style={{ padding: 0, border: "none", background: "transparent" }}>
|
||||||
|
<div className="bm-mapping__label">
|
||||||
|
{t("bizCompare.enabledSheets")}
|
||||||
|
<span className="muted" style={{ fontWeight: 400, marginLeft: 8 }}>
|
||||||
|
{enabledJobSheetCount}/{jobSheetAllIds.length || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="muted bm-hint">{t("bizCompare.enabledSheetsHint")}</p>
|
||||||
|
<div className="bm-metric-chips">
|
||||||
|
{jobTemplateSheets.map((s) => {
|
||||||
|
const sid = sheetIdentity(s);
|
||||||
|
const on = isJobSheetOn(sid);
|
||||||
return (
|
return (
|
||||||
<option key={tpl.id} value={tpl.id}>
|
<button
|
||||||
{tpl.name} ({mids.length} {t("bizCompare.sheetsUnit")})
|
key={sid}
|
||||||
</option>
|
type="button"
|
||||||
|
className={`bm-metric-chip${on ? " is-on" : ""}`}
|
||||||
|
onClick={() => toggleJobSheet(sid)}
|
||||||
|
title={s.metric_id !== sid ? s.metric_id : undefined}
|
||||||
|
>
|
||||||
|
{sheetLabel(s)}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</FieldSelect>
|
</div>
|
||||||
|
{!jobTemplateSheets.length ? (
|
||||||
|
<p className="muted bm-hint">{t("bizCompare.noTemplateSheets")}</p>
|
||||||
|
) : null}
|
||||||
|
{jobTemplateSheets.length && !enabledJobSheetCount ? (
|
||||||
|
<p className="form-error bm-hint">{t("bizCompare.needSheets")}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderMappingBlock = () => (
|
||||||
|
<div className="bs-cmp-mapping">
|
||||||
|
<h4 style={{ margin: "8px 0" }}>{t("bizCompare.mapping")}</h4>
|
||||||
|
<p className="muted">{t("bizCompare.mappingOptionalHint")}</p>
|
||||||
|
<div className="filter-inline" style={{ marginBottom: 8 }}>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
label={t("bizCompare.mode")}
|
value={mappingId}
|
||||||
value={mode}
|
onChange={(e) => {
|
||||||
onChange={(e) => setMode(e.target.value as "manual" | "auto")}
|
const id = e.target.value;
|
||||||
fullWidth
|
if (id) loadMappingText(id);
|
||||||
|
else {
|
||||||
|
setMappingId("");
|
||||||
|
setMapText("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="manual">{t("bizCompare.modeManual")}</option>
|
<option value="">{t("bizCompare.newMapping")}</option>
|
||||||
<option value="auto">{t("bizCompare.modeAuto")}</option>
|
{mappings.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</FieldSelect>
|
</FieldSelect>
|
||||||
|
<Input value={mapName} placeholder={t("bizCompare.mapName")} onChange={(e) => setMapName(e.target.value)} />
|
||||||
|
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void saveMapping()}>
|
||||||
|
{t("bizCompare.saveMapping")}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void doValidate()}>
|
||||||
|
{t("bizCompare.validateMapping")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={mapText}
|
||||||
|
onChange={(e) => setMapText(e.target.value)}
|
||||||
|
placeholder={t("bizCompare.mapHint")}
|
||||||
|
rows={5}
|
||||||
|
style={{ width: "100%", fontFamily: "ui-monospace, monospace" }}
|
||||||
|
/>
|
||||||
|
{validateOut ? (
|
||||||
|
<pre className="muted" style={{ fontSize: 12, maxHeight: 120, overflow: "auto" }}>
|
||||||
|
{JSON.stringify(validateOut, null, 2)}
|
||||||
|
</pre>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBatchFields = () => (
|
||||||
|
<>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
label={t("bizCompare.beforeTask")}
|
label={t("bizCompare.beforeTask")}
|
||||||
value={beforeTaskId}
|
value={beforeTaskId}
|
||||||
|
|
@ -1664,52 +1876,42 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
) : (
|
) : (
|
||||||
<p className="muted">{t("bizCompare.autoHint")}</p>
|
<p className="muted">{t("bizCompare.autoHint")}</p>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
{!compact ? (
|
const renderJobForm = () => (
|
||||||
<div className="bs-cmp-mapping">
|
<div className="bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||||
<h4 style={{ margin: "8px 0" }}>{t("bizCompare.mapping")}</h4>
|
<label className="ui-field ui-field--full">
|
||||||
<p className="muted">{t("bizCompare.mappingOptionalHint")}</p>
|
<span className="ui-field__label">{t("bizCompare.jobName")}</span>
|
||||||
<div className="filter-inline" style={{ marginBottom: 8 }}>
|
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
|
</label>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
value={mappingId}
|
label={t("bizCompare.template")}
|
||||||
onChange={(e) => {
|
value={templateId}
|
||||||
const id = e.target.value;
|
onChange={(e) => setJobTemplateAndSheets(e.target.value)}
|
||||||
if (id) loadMappingText(id);
|
fullWidth
|
||||||
else {
|
|
||||||
setMappingId("");
|
|
||||||
setMapText("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="">{t("bizCompare.newMapping")}</option>
|
{templates.map((tpl) => {
|
||||||
{mappings.map((m) => (
|
const n = templateSheets(tpl).length;
|
||||||
<option key={m.id} value={m.id}>
|
return (
|
||||||
{m.name}
|
<option key={tpl.id} value={tpl.id}>
|
||||||
|
{tpl.name} ({n} {t("bizCompare.sheetsUnit")})
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</FieldSelect>
|
</FieldSelect>
|
||||||
<Input value={mapName} placeholder={t("bizCompare.mapName")} onChange={(e) => setMapName(e.target.value)} />
|
<FieldSelect
|
||||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void saveMapping()}>
|
label={t("bizCompare.mode")}
|
||||||
{t("bizCompare.saveMapping")}
|
value={mode}
|
||||||
</Button>
|
onChange={(e) => setMode(e.target.value as "manual" | "auto")}
|
||||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void doValidate()}>
|
fullWidth
|
||||||
{t("bizCompare.validateMapping")}
|
>
|
||||||
</Button>
|
<option value="manual">{t("bizCompare.modeManual")}</option>
|
||||||
</div>
|
<option value="auto">{t("bizCompare.modeAuto")}</option>
|
||||||
<textarea
|
</FieldSelect>
|
||||||
value={mapText}
|
{renderSheetChips()}
|
||||||
onChange={(e) => setMapText(e.target.value)}
|
{renderBatchFields()}
|
||||||
placeholder={t("bizCompare.mapHint")}
|
{renderMappingBlock()}
|
||||||
rows={5}
|
|
||||||
style={{ width: "100%", fontFamily: "ui-monospace, monospace" }}
|
|
||||||
/>
|
|
||||||
{validateOut ? (
|
|
||||||
<pre className="muted" style={{ fontSize: 12, maxHeight: 120, overflow: "auto" }}>
|
|
||||||
{JSON.stringify(validateOut, null, 2)}
|
|
||||||
</pre>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1896,7 +2098,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
isDisabled={busy}
|
isDisabled={busy}
|
||||||
onPress={() => void removeJob(j.id)}
|
onPress={() => void removeJob(j.id)}
|
||||||
>
|
>
|
||||||
{t("bizCompare.delete")}
|
{t("bizCompare.deleteJob")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -2101,8 +2303,9 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
onChange={(next) => updateActiveSheet({ row_filters: fromOrGroups(next) })}
|
onChange={(next) => updateActiveSheet({ row_filters: fromOrGroups(next) })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="pt-list-table-wrap ct-field-table-wrap">
|
<>
|
||||||
<p className="muted ct-field-hint">{t("bizCompare.fieldsHintShort")}</p>
|
<p className="muted ct-field-hint">{t("bizCompare.fieldsHintShort")}</p>
|
||||||
|
<div className="pt-list-table-wrap ct-field-table-wrap">
|
||||||
<table className="data-table pt-list-table ct-field-table">
|
<table className="data-table pt-list-table ct-field-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -2276,6 +2479,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -2292,20 +2496,118 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</AppModalShell>
|
</AppModalShell>
|
||||||
|
|
||||||
{/* Create job */}
|
{/* Create job — guided steps */}
|
||||||
<AppModalShell open={jobCreateOpen} onClose={() => setJobCreateOpen(false)} size="lg">
|
<AppModalShell
|
||||||
|
open={jobCreateOpen}
|
||||||
|
onClose={closeCreateJob}
|
||||||
|
size="lg"
|
||||||
|
className="bm-create-modal"
|
||||||
|
>
|
||||||
<Modal.Header>
|
<Modal.Header>
|
||||||
<Modal.Heading>{t("bizCompare.createCompare")}</Modal.Heading>
|
<Modal.Heading>{t("bizCompare.createCompare")}</Modal.Heading>
|
||||||
<Modal.CloseTrigger />
|
<Modal.CloseTrigger />
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body className="flex flex-col gap-3">{renderJobForm(true)}</Modal.Body>
|
<Modal.Body className="flex flex-col gap-3 bm-create">
|
||||||
|
<nav className="bm-steps" aria-label={t("bizCompare.createSteps")}>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
t("bizCompare.stepBasics"),
|
||||||
|
t("bizCompare.stepSheets"),
|
||||||
|
t("bizCompare.stepBatches"),
|
||||||
|
t("bizCompare.stepMapping"),
|
||||||
|
] as const
|
||||||
|
).map((label, i) => {
|
||||||
|
const done = i < createStep;
|
||||||
|
const active = i === createStep;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={label}
|
||||||
|
type="button"
|
||||||
|
className={`bm-steps__item${active ? " is-active" : ""}${done ? " is-done" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (i <= createStep) {
|
||||||
|
setCreateStep(i as CreateJobStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ok = true;
|
||||||
|
for (let s = 0; s < i; s++) {
|
||||||
|
if (!canAdvanceCreateStep(s as CreateJobStep)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) setCreateStep(i as CreateJobStep);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="bm-steps__num">{done ? "✓" : i + 1}</span>
|
||||||
|
<span className="bm-steps__label">{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{createStep === 0 ? (
|
||||||
|
<div className="bm-create__pane bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||||
|
<label className="ui-field ui-field--full">
|
||||||
|
<span className="ui-field__label">{t("bizCompare.jobName")}</span>
|
||||||
|
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<FieldSelect
|
||||||
|
label={t("bizCompare.template")}
|
||||||
|
value={templateId}
|
||||||
|
onChange={(e) => setJobTemplateAndSheets(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{templates.map((tpl) => {
|
||||||
|
const n = templateSheets(tpl).length;
|
||||||
|
return (
|
||||||
|
<option key={tpl.id} value={tpl.id}>
|
||||||
|
{tpl.name} ({n} {t("bizCompare.sheetsUnit")})
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FieldSelect>
|
||||||
|
<FieldSelect
|
||||||
|
label={t("bizCompare.mode")}
|
||||||
|
value={mode}
|
||||||
|
onChange={(e) => setMode(e.target.value as "manual" | "auto")}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<option value="manual">{t("bizCompare.modeManual")}</option>
|
||||||
|
<option value="auto">{t("bizCompare.modeAuto")}</option>
|
||||||
|
</FieldSelect>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{createStep === 1 ? <div className="bm-create__pane">{renderSheetChips()}</div> : null}
|
||||||
|
|
||||||
|
{createStep === 2 ? (
|
||||||
|
<div className="bm-create__pane bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||||
|
{renderBatchFields()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{createStep === 3 ? <div className="bm-create__pane">{renderMappingBlock()}</div> : null}
|
||||||
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
|
<Button size="sm" variant="secondary" onPress={closeCreateJob}>
|
||||||
|
{t("bizState.cancel")}
|
||||||
|
</Button>
|
||||||
|
<div className="bm-create__footer-spacer" />
|
||||||
|
{createStep > 0 ? (
|
||||||
|
<Button size="sm" variant="secondary" onPress={onCreateBack}>
|
||||||
|
{t("bizCompare.back")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{createStep < CREATE_JOB_STEPS - 1 ? (
|
||||||
|
<Button size="sm" variant="primary" onPress={onCreateNext}>
|
||||||
|
{t("bizCompare.next")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void createJob()}>
|
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void createJob()}>
|
||||||
{t("bizCompare.createCompare")}
|
{t("bizCompare.createCompare")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="ghost" onPress={() => setJobCreateOpen(false)}>
|
)}
|
||||||
{t("bizState.cancel")}
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</AppModalShell>
|
</AppModalShell>
|
||||||
|
|
||||||
|
|
@ -2343,7 +2645,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{jobDetailTab === "config" ? (
|
{jobDetailTab === "config" ? (
|
||||||
renderJobForm(false)
|
renderJobForm()
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
ref={boardRef}
|
ref={boardRef}
|
||||||
|
|
@ -2375,13 +2677,20 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
);
|
);
|
||||||
const bl = sideDeviceName(before);
|
const bl = sideDeviceName(before);
|
||||||
const al = sideDeviceName(after);
|
const al = sideDeviceName(after);
|
||||||
|
const when = formatSystemTime((r as any).created_at) || "";
|
||||||
return (
|
return (
|
||||||
<option key={r.id} value={r.id}>
|
<option key={r.id} value={r.id}>
|
||||||
|
{when ? `${when} · ` : ""}
|
||||||
{bl} {sideCollectTime(before)} → {al} {sideCollectTime(after)}
|
{bl} {sideCollectTime(before)} → {al} {sideCollectTime(after)}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
{runs.length ? (
|
||||||
|
<span className="muted bs-cmp-board__run-count">
|
||||||
|
{t("bizCompare.runCount", { n: String(runs.length) })}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
{runDetail ? (
|
{runDetail ? (
|
||||||
<div className="bs-cmp-sides" aria-label={t("bizCompare.sidesTitle")}>
|
<div className="bs-cmp-sides" aria-label={t("bizCompare.sidesTitle")}>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
|
@ -2438,6 +2747,14 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
>
|
>
|
||||||
{t("bizCompare.exportTables")}
|
{t("bizCompare.exportTables")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={busy || !runDetail?.id}
|
||||||
|
onPress={() => void removeRun(String(runDetail?.id || ""))}
|
||||||
|
>
|
||||||
|
{t("bizCompare.deleteRun")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -2782,6 +3099,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void runNow()}>
|
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void runNow()}>
|
||||||
{t("bizCompare.runNow")}
|
{t("bizCompare.runNow")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{jobId ? (
|
||||||
|
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeJob(jobId)}>
|
||||||
|
{t("bizCompare.deleteJob")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -2793,6 +3115,14 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
>
|
>
|
||||||
{t("bizCompare.exportTables")}
|
{t("bizCompare.exportTables")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={busy || !runDetail?.id}
|
||||||
|
onPress={() => void removeRun(String(runDetail?.id || ""))}
|
||||||
|
>
|
||||||
|
{t("bizCompare.deleteRun")}
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void runNow()}>
|
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void runNow()}>
|
||||||
{t("bizCompare.runNow")}
|
{t("bizCompare.runNow")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -2801,11 +3131,6 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
||||||
<Button size="sm" variant="ghost" onPress={closeJob}>
|
<Button size="sm" variant="ghost" onPress={closeJob}>
|
||||||
{t("bizState.cancel")}
|
{t("bizState.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
{jobId ? (
|
|
||||||
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeJob(jobId)}>
|
|
||||||
{t("bizCompare.delete")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</AppModalShell>
|
</AppModalShell>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,11 @@ import {
|
||||||
bizStateListProfiles,
|
bizStateListProfiles,
|
||||||
bizStateListTasks,
|
bizStateListTasks,
|
||||||
bizStatePatchTask,
|
bizStatePatchTask,
|
||||||
|
bizStatePauseTask,
|
||||||
bizStatePurgeTask,
|
bizStatePurgeTask,
|
||||||
bizStateSetBatchBaseline,
|
bizStateSetBatchBaseline,
|
||||||
bizStateSetBindings,
|
bizStateSetBindings,
|
||||||
|
bizStateStartTask,
|
||||||
fetchCliTargets,
|
fetchCliTargets,
|
||||||
formatErr,
|
formatErr,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
|
|
@ -538,7 +540,11 @@ export function BizStatePage() {
|
||||||
const setScheduleEnabled = async (id: string, enabled: boolean) => {
|
const setScheduleEnabled = async (id: string, enabled: boolean) => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await bizStatePatchTask(id, { status: enabled ? "running" : "paused" });
|
if (enabled) {
|
||||||
|
await bizStateStartTask(id);
|
||||||
|
} else {
|
||||||
|
await bizStatePauseTask(id);
|
||||||
|
}
|
||||||
showOk(enabled ? t("bizState.started") : t("bizState.paused"));
|
showOk(enabled ? t("bizState.started") : t("bizState.paused"));
|
||||||
if (taskId === id) await loadTask(id);
|
if (taskId === id) await loadTask(id);
|
||||||
await refreshTasks();
|
await refreshTasks();
|
||||||
|
|
@ -981,10 +987,12 @@ export function BizStatePage() {
|
||||||
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
<div className="pt-list-actions" style={{ flexWrap: "wrap", gap: 4 }}>
|
||||||
{row.collect_running ? (
|
{row.collect_running ? (
|
||||||
<NmStatusChip color="accent">{t("bizState.collecting")}</NmStatusChip>
|
<NmStatusChip color="accent">{t("bizState.collecting")}</NmStatusChip>
|
||||||
|
) : row.status === "running" ? (
|
||||||
|
<NmStatusChip color="success">{t("bizState.scheduleOn")}</NmStatusChip>
|
||||||
|
) : row.status === "paused" ? (
|
||||||
|
<NmStatusChip color="warning">{t("bizState.statusPaused")}</NmStatusChip>
|
||||||
) : (
|
) : (
|
||||||
<NmStatusChip color={row.status === "running" ? "success" : "default"}>
|
<NmStatusChip color="default">{t("bizState.scheduleOff")}</NmStatusChip>
|
||||||
{row.status === "running" ? t("bizState.scheduleOn") : t("bizState.scheduleOff")}
|
|
||||||
</NmStatusChip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -997,6 +1005,25 @@ export function BizStatePage() {
|
||||||
<Button size="sm" variant="ghost" onPress={() => void openTask(row.id, "batches")}>
|
<Button size="sm" variant="ghost" onPress={() => void openTask(row.id, "batches")}>
|
||||||
{t("bizState.batches")}
|
{t("bizState.batches")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{row.status === "running" ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void setScheduleEnabled(row.id, false)}
|
||||||
|
>
|
||||||
|
{t("bizState.pause")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void setScheduleEnabled(row.id, true)}
|
||||||
|
>
|
||||||
|
{t("bizState.start")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -1161,6 +1188,25 @@ export function BizStatePage() {
|
||||||
/>
|
/>
|
||||||
<span>{t("bizState.scheduleEnabled")}</span>
|
<span>{t("bizState.scheduleEnabled")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{detail.status === "running" ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void setScheduleEnabled(taskId, false)}
|
||||||
|
>
|
||||||
|
{t("bizState.pause")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={busy}
|
||||||
|
onPress={() => void setScheduleEnabled(taskId, true)}
|
||||||
|
>
|
||||||
|
{t("bizState.start")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<label className="config-sync-policy-field">
|
<label className="config-sync-policy-field">
|
||||||
<span>{t("bizState.interval")}</span>
|
<span>{t("bizState.interval")}</span>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -1804,6 +1804,12 @@ export const bizStateGetTask = (taskId: string) =>
|
||||||
export const bizStatePatchTask = (taskId: string, body: Record<string, unknown>) =>
|
export const bizStatePatchTask = (taskId: string, body: Record<string, unknown>) =>
|
||||||
apiPatch<Record<string, unknown>>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}`, body);
|
apiPatch<Record<string, unknown>>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}`, body);
|
||||||
|
|
||||||
|
export const bizStatePauseTask = (taskId: string) =>
|
||||||
|
apiPost<Record<string, unknown>>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/pause`, {});
|
||||||
|
|
||||||
|
export const bizStateStartTask = (taskId: string) =>
|
||||||
|
apiPost<Record<string, unknown>>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/start`, {});
|
||||||
|
|
||||||
export const bizStateDeleteTask = (taskId: string) =>
|
export const bizStateDeleteTask = (taskId: string) =>
|
||||||
apiDelete<{ ok: boolean }>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}`);
|
apiDelete<{ ok: boolean }>(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}`);
|
||||||
|
|
||||||
|
|
@ -1985,6 +1991,11 @@ export const bizCompareListRuns = (jobId: string, limit = 20) =>
|
||||||
export const bizCompareGetRun = (runId: string) =>
|
export const bizCompareGetRun = (runId: string) =>
|
||||||
apiGet<Record<string, unknown>>(`/v1/biz-state/compare/runs/${encodeURIComponent(runId)}`);
|
apiGet<Record<string, unknown>>(`/v1/biz-state/compare/runs/${encodeURIComponent(runId)}`);
|
||||||
|
|
||||||
|
export const bizCompareDeleteRun = (runId: string) =>
|
||||||
|
apiDelete<{ ok: boolean; job_id?: string; run_id?: string }>(
|
||||||
|
`/v1/biz-state/compare/runs/${encodeURIComponent(runId)}`,
|
||||||
|
);
|
||||||
|
|
||||||
export const bizCompareListRunDiffs = (params: {
|
export const bizCompareListRunDiffs = (params: {
|
||||||
runId: string;
|
runId: string;
|
||||||
metricId?: string;
|
metricId?: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue