Redesign biz compare: editable templates and pre/post result workbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-17 17:26:52 +08:00
parent 4811a25ac3
commit 14f5870e19
8 changed files with 1133 additions and 298 deletions

View file

@ -90,11 +90,80 @@ def _template_out(t: BizCompareTemplate) -> dict[str, Any]:
def list_templates(db: Session) -> list[dict[str, Any]]:
ensure_default_lldp_template(db)
ensure_default_templates(db)
rows = db.query(BizCompareTemplate).order_by(BizCompareTemplate.name.asc()).all()
return [_template_out(t) for t in rows]
def ensure_default_vrf_template(db: Session) -> BizCompareTemplate:
row = (
db.query(BizCompareTemplate)
.filter(
BizCompareTemplate.metric_id == "vrf_route_summary",
BizCompareTemplate.name == "VRF route summary default",
)
.one_or_none()
)
if row:
return row
fields = metric_field_map().get("vrf_route_summary") or []
keys = [f.name for f in fields if f.is_key] or ["vrf", "source"]
ifaces = [f.name for f in fields if f.is_interface]
compare = [f.name for f in fields if f.role in ("state", "identity", "meta") or f.is_key]
seen: set[str] = set()
cmp_out: list[str] = []
for n in compare:
if n not in seen:
seen.add(n)
cmp_out.append(n)
row = BizCompareTemplate(
id=uuid4().hex,
name="VRF route summary default",
metric_id="vrf_route_summary",
key_fields=keys,
iface_fields=ifaces,
compare_fields=cmp_out or ["networks"],
ignore_fields=[],
note="Built-in template for per-VRF route summary cutover compare",
created_at=_utcnow(),
updated_at=_utcnow(),
)
db.add(row)
db.commit()
db.refresh(row)
return row
def ensure_default_templates(db: Session) -> None:
ensure_default_lldp_template(db)
ensure_default_vrf_template(db)
def list_metric_schemas() -> list[dict[str, Any]]:
"""Field catalog for template editors (key / iface / compare pickers)."""
out: list[dict[str, Any]] = []
for metric_id, fields in sorted(metric_field_map().items()):
if metric_id in ("vrf_list",):
continue
out.append(
{
"metric_id": metric_id,
"fields": [
{
"name": f.name,
"display_name": f.display_name or f.name,
"dtype": f.dtype,
"is_key": bool(f.is_key),
"is_interface": bool(f.is_interface),
"role": f.role,
}
for f in fields
],
}
)
return out
def create_template(db: Session, body: dict[str, Any]) -> dict[str, Any]:
metric_id = str(body.get("metric_id") or "").strip()
if not metric_id:
@ -125,6 +194,10 @@ def update_template(db: Session, template_id: str, body: dict[str, Any]) -> dict
raise HTTPException(status_code=404, detail="template_not_found")
if "name" in body:
t.name = str(body.get("name") or "")[:256]
if "metric_id" in body and body.get("metric_id") is not None:
mid = str(body.get("metric_id") or "").strip()
if mid:
t.metric_id = mid
if "key_fields" in body:
keys = [str(x) for x in (body.get("key_fields") or []) if str(x).strip()]
if not keys:
@ -279,6 +352,7 @@ def validate_mapping(
after_batch_id: str,
template_id: str = "",
) -> dict[str, Any]:
ensure_default_templates(db)
tpl = db.get(BizCompareTemplate, template_id) if template_id else ensure_default_lldp_template(db)
if not tpl:
raise HTTPException(status_code=404, detail="template_not_found")
@ -316,7 +390,7 @@ def list_jobs(db: Session) -> list[dict[str, Any]]:
def create_job(db: Session, body: dict[str, Any]) -> dict[str, Any]:
ensure_default_lldp_template(db)
ensure_default_templates(db)
template_id = str(body.get("template_id") or "").strip()
if not template_id:
tpl = ensure_default_lldp_template(db)
@ -458,6 +532,7 @@ def get_run(db: Session, run_id: str) -> dict[str, Any]:
r = db.get(BizCompareRun, run_id)
if not r:
raise HTTPException(status_code=404, detail="run_not_found")
tpl = db.get(BizCompareTemplate, r.template_id) if r.template_id else None
return {
"id": r.id,
"job_id": r.job_id,
@ -472,6 +547,7 @@ def get_run(db: Session, run_id: str) -> dict[str, Any]:
"mapping_stats": r.mapping_stats_json or {},
"message": r.message,
"created_at": r.created_at.isoformat() + "Z" if r.created_at else None,
"template": _template_out(tpl) if tpl else None,
}

View file

@ -222,6 +222,16 @@ class TemplateIn(BaseModel):
note: str = ""
class TemplatePatchIn(BaseModel):
name: str | None = None
metric_id: str | None = None
key_fields: list[str] | None = None
iface_fields: list[str] | None = None
compare_fields: list[str] | None = None
ignore_fields: list[str] | None = None
note: str | None = None
class MappingRowIn(BaseModel):
before_if: str
after_if: str
@ -252,6 +262,11 @@ class CompareJobIn(BaseModel):
note: str = ""
@router.get("/compare/metrics")
def api_list_compare_metrics() -> dict[str, Any]:
return {"items": cmp_svc.list_metric_schemas()}
@router.get("/compare/templates")
def api_list_templates(db: Session = Depends(get_db)) -> dict[str, Any]:
return {"items": cmp_svc.list_templates(db)}
@ -264,7 +279,7 @@ def api_create_template(body: TemplateIn, db: Session = Depends(get_db)) -> dict
@router.patch("/compare/templates/{template_id}")
def api_patch_template(
template_id: str, body: TemplateIn, db: Session = Depends(get_db)
template_id: str, body: TemplatePatchIn, db: Session = Depends(get_db)
) -> dict[str, Any]:
return cmp_svc.update_template(db, template_id, body.model_dump(exclude_unset=True))

View file

@ -45,7 +45,7 @@ src/
| `/network/tasks/port-traffic` | 端口流量监控(设备管理) | `network` |
| `/network/tasks/port-traffic/wall` | 流量大屏列表(打开独立页签) | `network` |
| `/network/tasks/biz-state` | 业务状态监控(LLDP / VRF 关联采集) | `network` |
| `/network/tasks/biz-compare` | 业务状态比对(模板/映射/前后 diff) | `network` |
| `/network/tasks/biz-compare` | 业务状态比对(模板 + 任务 + pre/post 结果) | `network` |
| `/port-traffic/wall/:boardId` | 流量大屏专有页签(无网络侧栏) | `port-traffic-wall` |
| `/topology` | 拓扑管理(模式化编辑器:选择/平移/拖动/连线、框选、自动布局、拖放添加) | `topology` |
| `/webcrt` | WebCRT 终端 | `webcrt` |
@ -131,7 +131,8 @@ src/
- API:`/v1/biz-state/profiles`、`/discover`、`/tasks*`、`/batches*`、`/batches/{id}/export`、`/compare/*`
- **ParseProfile**:命令模板 + TextFSM + 回调 + schema;LLDP 与拓扑 **共享解析**(`lldp_shared`),业务流程写批次表,拓扑写 Fabric
- Phase1:LLDP 邻居快照;建任务默认启用对应厂商 LLDP profile;支持自定义只采不解析行
- Phase2:比对模板、端口映射、CompareJob(manual/auto)、`/network/tasks/biz-compare`
- Phase2:比对模板(可选 metric / key / 接口 / 比对字段)、端口映射、CompareJob 列表与详情、`pre/post` 结果表(高亮 + 筛选)、`/network/tasks/biz-compare`
- API 补充:`GET /v1/biz-state/compare/metrics`(模板字段目录)
- Phase3:VRF 发现→人选关联→带参采集(`*.route_vrf_summary`);`PUT …/items/{id}/bindings`
- 调度:`NETX_BIZ_STATE_SCHEDULER_ENABLED`(默认开),tick `NETX_BIZ_STATE_SCHEDULER_TICK_SEC`
- 前端:`/network/tasks/biz-state`(勾选监控项 / 发现 VRF / 批次 / 导出 zip)

View file

@ -229,14 +229,36 @@ const en = {
},
bizCompare: {
title: "Business compare",
templates: "Compare templates",
newTemplate: "New template",
editTemplate: "Edit template",
saveTemplate: "Save template",
templateSaved: "Template saved",
templateDeleted: "Template deleted",
confirmDeleteTemplate: "Delete this compare template?",
emptyTemplates: "No templates yet. Create one or use built-ins.",
templateHint:
"Field roles: Key aligns rows; interface fields use port mapping; compare fields show pre/post; ignore skips diff.",
keyFields: "Key",
ifaceFields: "Interface",
compareFields: "Compare",
ignoreFields: "Ignore",
field: "Field",
note: "Note",
keyRequired: "Select at least one key field",
noMetricFields: "No fields for this metric",
templateFilterPh: "Filter template / metric",
jobFilterPh: "Filter job / template / status",
mapping: "Port mapping",
newMapping: "New mapping…",
mapName: "Mapping name",
saveMapping: "Save mapping",
validateMapping: "Validate",
validateNeed: "Select mapping and before/after batches first",
mapHint: "before_if,after_if one pair per line",
mappingSaved: "Mapping saved",
createJob: "Create compare job",
defaultJobName: "Cutover compare",
jobName: "Name",
template: "Template",
mode: "Mode",
@ -251,10 +273,25 @@ const en = {
autoHint: "auto: new after-task batches trigger compare",
createCompare: "Create job",
runNow: "Run now",
saveJob: "Save config",
jobSaved: "Job config saved",
jobList: "Compare jobs",
detail: "Details",
edit: "Edit",
delete: "Delete",
tabConfig: "Config / batches",
tabResult: "Result",
runs: "Run history",
noRuns: "No runs yet — run a compare first",
result: "Result",
pickRun: "Pick a run or click Run now",
pickRun: "Pick a run below or click Run now",
resultEmpty: "No matching diff rows",
resultFilterPh: "Filter key / values…",
kindAll: "All kinds",
kindAdded: "Added only",
kindRemoved: "Removed only",
kindChanged: "Changed only",
kindUnchanged: "Unchanged only",
emptyJobs: "No compare jobs yet",
created: "Compare job created",
ran: "Compare finished",
@ -262,6 +299,7 @@ const en = {
colMode: "Mode",
colStatus: "Status",
colKind: "Kind",
colActions: "Actions",
colKey: "Key",
colChange: "Changes",
},

View file

@ -229,14 +229,35 @@ const zh = {
},
bizCompare: {
title: "业务状态比对",
templates: "比对模板",
newTemplate: "新建模板",
editTemplate: "编辑模板",
saveTemplate: "保存模板",
templateSaved: "模板已保存",
templateDeleted: "模板已删除",
confirmDeleteTemplate: "删除该比对模板?",
emptyTemplates: "暂无模板。可新建或使用系统默认。",
templateHint: "勾选字段角色:Key 用于对齐行;接口字段参与端口映射;比对字段左右对照;忽略字段不参与 diff。",
keyFields: "Key",
ifaceFields: "接口",
compareFields: "比对",
ignoreFields: "忽略",
field: "字段",
note: "备注",
keyRequired: "至少选择一个 Key 字段",
noMetricFields: "该 metric 无字段定义",
templateFilterPh: "筛选模板名 / metric",
jobFilterPh: "筛选任务名 / 模板 / 状态",
mapping: "端口映射",
newMapping: "新建映射…",
mapName: "映射名称",
saveMapping: "保存映射",
validateMapping: "校验映射",
validateNeed: "请先选择映射与前后批次",
mapHint: "before_if,after_if 每行一对",
mappingSaved: "映射已保存",
createJob: "建立比对任务",
defaultJobName: "割接比对",
jobName: "名称",
template: "模板",
mode: "模式",
@ -251,10 +272,25 @@ const zh = {
autoHint: "auto:操作后任务新批次到达会自动跑比对",
createCompare: "创建比对任务",
runNow: "立即比对",
jobList: "比对任务列表",
saveJob: "保存配置",
jobSaved: "任务配置已保存",
jobList: "比对任务",
detail: "详情",
edit: "编辑",
delete: "删除",
tabConfig: "配置 / 批次",
tabResult: "比对结果",
runs: "历史 Run",
noRuns: "尚无比对记录,请先执行比对",
result: "比对结果",
pickRun: "选择 Run 或点「立即比对」",
pickRun: "选择底部 Run 或点「立即比对」",
resultEmpty: "无匹配差异行",
resultFilterPh: "筛选 key / 字段值…",
kindAll: "全部类型",
kindAdded: "仅新增",
kindRemoved: "仅删除",
kindChanged: "仅变更",
kindUnchanged: "仅未变",
emptyJobs: "暂无比对任务",
created: "比对任务已创建",
ran: "比对完成",
@ -262,6 +298,7 @@ const zh = {
colMode: "模式",
colStatus: "状态",
colKind: "类型",
colActions: "操作",
colKey: "Key",
colChange: "变更",
},

View file

@ -9993,6 +9993,49 @@ html.login-page--paused .login-page__flare {
margin-bottom: 4px;
}
/* biz_state compare result workbook */
.bs-cmp-job-body {
min-height: min(64vh, 680px);
}
.bs-cmp-result-table {
flex: 1 1 auto;
min-height: 260px;
max-height: min(46vh, 520px);
}
.bs-cmp-diff-table .bs-cmp-pair-head {
text-align: center;
}
.bs-cmp-diff-table .bs-cmp-pre-head,
.bs-cmp-diff-table .bs-cmp-post-head {
font-size: 11px;
opacity: 0.8;
}
.bs-cmp-row--added {
background: rgba(34, 197, 94, 0.08);
}
.bs-cmp-row--removed {
background: rgba(239, 68, 68, 0.08);
}
.bs-cmp-row--changed {
background: rgba(245, 158, 11, 0.06);
}
.bs-cmp-cell--pre {
background: rgba(239, 68, 68, 0.14);
font-weight: 600;
}
.bs-cmp-cell--post {
background: rgba(34, 197, 94, 0.14);
font-weight: 600;
}
.nm-config-modal__body {
flex: 1 1 auto;
width: 100%;

File diff suppressed because it is too large Load diff

View file

@ -1841,9 +1841,33 @@ export const bizStateDownloadExport = async (batchId: string): Promise<void> =>
export const bizCompareListTemplates = () =>
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-state/compare/templates");
export const bizCompareListMetrics = () =>
apiGet<{
items: Array<{
metric_id: string;
fields: Array<{
name: string;
display_name: string;
dtype: string;
is_key: boolean;
is_interface: boolean;
role: string;
}>;
}>;
}>("/v1/biz-state/compare/metrics");
export const bizCompareCreateTemplate = (body: Record<string, unknown>) =>
apiPost<Record<string, unknown>>("/v1/biz-state/compare/templates", body);
export const bizCompareUpdateTemplate = (templateId: string, body: Record<string, unknown>) =>
apiPatch<Record<string, unknown>>(
`/v1/biz-state/compare/templates/${encodeURIComponent(templateId)}`,
body,
);
export const bizCompareDeleteTemplate = (templateId: string) =>
apiDelete<{ ok: boolean }>(`/v1/biz-state/compare/templates/${encodeURIComponent(templateId)}`);
export const bizCompareListMappings = () =>
apiGet<{ items: Record<string, unknown>[] }>("/v1/biz-state/compare/mappings");