mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Redesign biz compare: editable templates and pre/post result workbook.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4811a25ac3
commit
14f5870e19
8 changed files with 1133 additions and 298 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: "变更",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue