Make biz compare template-driven with field rules, board UX, and import/export.

Operators pick metrics then Key/Compare/Display rules; seed built-ins only once; fail-first verdict board for cutover review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-18 01:30:57 +08:00
parent 21b254120e
commit d232044750
11 changed files with 1978 additions and 345 deletions

View file

@ -254,12 +254,32 @@ const en = {
templateDeleted: "Template deleted",
confirmDeleteTemplate: "Delete this compare template?",
confirmDeleteJob: "Delete this compare job and all runs?",
emptyTemplates: "No templates yet. Create one or use built-ins.",
emptyTemplates: "No templates yet. Create one or import a JSON file.",
exportTemplate: "Export",
importTemplate: "Import",
templateExported: "Template exported",
templateImported: "Template imported",
templateImportInvalid: "Invalid file: need name and metrics",
templateImportHint: "Import/export JSON to share templates privately (no job/batch data)",
templateHint:
"A template holds multiple metric sheets. Key: row identity (no separate compare). Interface: port mapping. Compare: value checks after key match. Leave compare empty for presence-only (entry set match).",
"Pick metrics, then set Key / interface / compare / display. Compare modes: exact, abs delta, or relative %. Display can include context-only columns. Empty compare = presence-only.",
stepPickMetrics: "① Pick metrics",
stepConfigRules: "② Configure compare rules",
pickMetricsHint: "Check metrics to add. Then configure each sheet below.",
noMetricsYet: "Select at least one metric above",
selectedCount: "{{n}} selected",
colFields: "Fields",
keyFields: "Key",
ifaceFields: "Interface",
compareFields: "Compare",
compareMode: "Mode",
tolerance: "Tolerance",
displayField: "Display",
keyAlwaysDisplay: "Key columns are always shown",
modeEq: "Exact equal",
modeNumeric: "Abs delta",
modePercent: "Relative %",
normalizeField: "Normalize",
ignoreFields: "Ignore",
field: "Field",
note: "Note",
@ -267,6 +287,23 @@ const en = {
keyIsIdentity: "Key aligns rows; not used as a value-compare field",
metricsRequired: "Add at least one metric sheet",
noMetricFields: "No fields for this metric",
rowFilters: "Row filter rules",
rowFiltersHint:
"Filter rows before compare. Conditions in a group are AND; groups are OR. Selecting ARP seeds editable default rules you can change.",
rowFiltersEmpty: "No filters → compare all rows. Add a condition group to start.",
filterClear: "Clear rules",
filterAddOr: "Add group (OR)",
filterAddAnd: "Add condition (AND)",
filterRemoveGroup: "Remove group",
filterOr: "OR",
filterField: "Field",
filterOp: "Op",
filterValue: "Value",
normalizeNone: "None",
normalizeLower: "lower",
normalizeUpper: "upper",
normalizeMac: "MAC",
normalizeEmpty: "empty→blank",
templateFilterPh: "Filter template / metric",
jobFilterPh: "Filter job / template / status",
addMetric: "Add metric",
@ -286,9 +323,10 @@ const en = {
validateMapping: "Validate",
validateNeed: "Select mapping and before/after batches first",
mapHint:
"Optional. Leave empty to ignore port renames (match on non-interface keys). Or one before_if,after_if pair per line",
"One pair per line: before_if,after_if (comma / tab / pipe). Example:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# comments start with #. Not slash-grouped A,B/C,D.",
mappingSaved: "Mapping saved",
mappingOptionalHint: "No port map: ignore local port changes; match on non-interface keys.",
mappingOptionalHint:
"Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map.",
createJob: "Create compare job",
defaultJobName: "Cutover compare",
jobName: "Name",
@ -322,32 +360,49 @@ const en = {
resultEmpty: "No matching diff rows",
resultFilterPh: "Filter key / values…",
kindAll: "All",
kindDiff: "Diffs only",
kindAdded: "Added only",
kindRemoved: "Removed only",
kindChanged: "Changed only",
kindUnchanged: "Unchanged only",
kindAddedShort: "Added",
kindRemovedShort: "Removed",
kindChangedShort: "Changed",
kindUnchangedShort: "Same",
diffCount: "{{n}} diffs",
kindDiff: "Failures only",
kindAdded: "Extra only",
kindRemoved: "Missing only",
kindChanged: "Mismatch only",
kindUnchanged: "Match only",
kindAddedShort: "Extra",
kindRemovedShort: "Missing",
kindChangedShort: "Mismatch",
kindUnchangedShort: "Match",
kindFail: "Fail",
kindPass: "Pass",
verdictPass: "Pass",
verdictFail: "Fail",
failCount: "Failed rows",
missCount: "Missing",
extraCount: "Extra",
mismatchCount: "Mismatch",
matchCount: "Match",
failFields: "Failed fields",
failFieldsEmpty: "—",
failWholeRow: "Whole row",
sheetAllPass: "All match",
filterFailField: "Failed compare field",
filterFailFieldAll: "Any failed field",
resultEmpty: "No matching rows",
resultFilterPh: "Filter identity / values…",
diffCount: "{{n}} failed",
passRate: "Pass rate",
passOk: "In sync",
passWarn: "Has diffs",
runOverview: "Run overview",
sheetCurrent: "Current sheet",
diffCountLabel: "Diff rows",
diffRate: "Diff rate",
passOk: "All passed",
passWarn: "Has failures",
runOverview: "Verdict",
sheetCurrent: "Current check",
diffCountLabel: "Failed rows",
diffRate: "Fail rate",
beforeCount: "Before rows",
afterCount: "After rows",
matchedRows: "Matched",
totalRows: "Total rows",
sheetCount: "Sheets",
sheetNavTitle: "Items",
sheetNavHint: "{{n}} items · click to switch",
sheetFailCount: "{{n}} with diffs",
topChangedFields: "Top changed fields",
sheetNavTitle: "Checks",
sheetNavHint: "{{n}} checks · click to switch",
sheetFailCount: "{{n}} failed",
topChangedFields: "Top failed fields",
exportTables: "Download tables",
exportOk: "Compare tables exported",
fullscreen: "Fullscreen board",
@ -358,9 +413,10 @@ const en = {
colName: "Name",
colMode: "Mode",
colStatus: "Status",
colKind: "Kind",
colKind: "Verdict",
colFailFields: "Failed fields",
colActions: "Actions",
colKey: "Key",
colKey: "Identity",
colChange: "Changes",
},
portTraffic: {

View file

@ -254,12 +254,32 @@ const zh = {
templateDeleted: "模板已删除",
confirmDeleteTemplate: "删除该比对模板?",
confirmDeleteJob: "删除该比对任务及全部运行记录?",
emptyTemplates: "暂无模板。可新建或使用系统默认。",
emptyTemplates: "暂无模板。可新建,或从 JSON 文件导入。",
exportTemplate: "导出",
importTemplate: "导入",
templateExported: "模板已导出",
templateImported: "模板已导入",
templateImportInvalid: "导入文件无效:需要 name 与 metrics",
templateImportHint: "导入/导出 JSON,便于私下传递模板(不含任务/批次数据)",
templateHint:
"一个模板可包含多张监控表。Key:对齐前后条目(选中后即为行身份,无需再勾比对);接口:参与端口映射;比对:勾选具体参数做值检查。若不勾任何比对字段,则只校验条目是否一致(存在性)。",
"先勾选监控项,再配置 Key / 接口 / 比对 / 展示。比对可设完全相等、绝对容差或相对偏差%;展示列可含不参与比对的上下文。不勾比对字段则只校验条目存在性。",
stepPickMetrics: "① 选择监控项",
stepConfigRules: "② 配置比对规则",
pickMetricsHint: "勾选后加入模板;可多选。选好后在下方按表配置参数。",
noMetricsYet: "请先在上方勾选至少一个监控项",
selectedCount: "已选 {{n}} 项",
colFields: "字段数",
keyFields: "Key",
ifaceFields: "接口",
compareFields: "比对",
compareMode: "比对方式",
tolerance: "容差",
displayField: "展示",
keyAlwaysDisplay: "Key 列始终展示",
modeEq: "完全相等",
modeNumeric: "绝对偏差",
modePercent: "相对偏差 %",
normalizeField: "归一化",
ignoreFields: "忽略",
field: "字段",
note: "备注",
@ -267,6 +287,23 @@ const zh = {
keyIsIdentity: "Key 用于对齐行,不作为值比对字段",
metricsRequired: "请至少添加一张监控表",
noMetricFields: "该 metric 无字段定义",
rowFilters: "行过滤规则",
rowFiltersHint:
"比对前按规则筛选行。组内条件为「且」,组与组之间为「或」。勾选 ARP 等监控项时会带上默认可编辑规则,可按需改删。",
rowFiltersEmpty: "无过滤则比对全部行。可添加条件组。",
filterClear: "清空规则",
filterAddOr: "添加条件组(或)",
filterAddAnd: "组内加条件(且)",
filterRemoveGroup: "删除本组",
filterOr: "或",
filterField: "字段",
filterOp: "运算",
filterValue: "值",
normalizeNone: "无",
normalizeLower: "小写",
normalizeUpper: "大写",
normalizeMac: "MAC",
normalizeEmpty: "空值归一",
templateFilterPh: "筛选模板名 / metric",
jobFilterPh: "筛选任务名 / 模板 / 状态",
addMetric: "添加监控项",
@ -285,9 +322,11 @@ const zh = {
saveMapping: "保存映射",
validateMapping: "校验映射",
validateNeed: "请先选择映射与前后批次",
mapHint: "可选。不填表示忽略端口变化(同设备比对按非接口 Key 对齐)。填写时每行 before_if,after_if",
mapHint:
"每行一对:操作前接口,操作后接口(逗号/Tab/竖线分隔)。例:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# 井号开头为注释。不是 A,B/C,D 这种斜杠分组。",
mappingSaved: "映射已保存",
mappingOptionalHint: "未填端口映射:忽略本端端口变化,仅按邻居等非接口 Key 对齐。",
mappingOptionalHint:
"可选。留空=忽略本端端口改名,按非接口 Key 对齐。填写后才做端口映射校验。",
createJob: "建立比对任务",
defaultJobName: "割接比对",
jobName: "名称",
@ -318,35 +357,50 @@ const zh = {
result: "比对结果",
pickBatchRun: "选择比对批次",
pickRun: "选择批次…",
resultEmpty: "无匹配差异行",
resultFilterPh: "筛选 key / 字段值…",
resultEmpty: "无匹配失败/结果行",
resultFilterPh: "筛选身份 / 字段值…",
kindAll: "全部",
kindDiff: "仅差异",
kindAdded: "仅新增",
kindRemoved: "仅删除",
kindChanged: "仅变更",
kindUnchanged: "仅未变",
kindAddedShort: "新增",
kindRemovedShort: "删除",
kindChangedShort: "变更",
kindUnchangedShort: "未变",
diffCount: "差异 {{n}}",
kindDiff: "仅失败",
kindAdded: "仅多余",
kindRemoved: "仅缺失",
kindChanged: "仅不一致",
kindUnchanged: "仅一致",
kindAddedShort: "多余",
kindRemovedShort: "缺失",
kindChangedShort: "不一致",
kindUnchangedShort: "一致",
kindFail: "失败",
kindPass: "通过",
verdictPass: "通过",
verdictFail: "不通过",
failCount: "失败条目",
missCount: "缺失",
extraCount: "多余",
mismatchCount: "不一致",
matchCount: "一致",
failFields: "失败项",
failFieldsEmpty: "—",
failWholeRow: "整行",
sheetAllPass: "全部一致",
filterFailField: "失败比对项",
filterFailFieldAll: "全部失败项",
diffCount: "失败 {{n}}",
passRate: "通过率",
passOk: "前后一致",
passWarn: "存在差异",
runOverview: "本次总览",
sheetCurrent: "当前表",
diffCountLabel: "差异条目",
diffRate: "差异率",
passOk: "全部通过",
passWarn: "存在失败",
runOverview: "总裁决",
sheetCurrent: "当前检查项",
diffCountLabel: "失败条目",
diffRate: "失败率",
beforeCount: "操作前行数",
afterCount: "操作后行数",
matchedRows: "对齐条目",
totalRows: "合计条目",
sheetCount: "监控表",
sheetNavTitle: "对比项",
sheetNavTitle: "检查项",
sheetNavHint: "共 {{n}} 项 · 点击切换",
sheetFailCount: "{{n}} 项有差异",
topChangedFields: "变更字段 Top",
sheetFailCount: "{{n}} 项失败",
topChangedFields: "失败字段 Top",
exportTables: "下载表格",
exportOk: "已导出比对表格",
fullscreen: "大屏全屏",
@ -357,9 +411,10 @@ const zh = {
colName: "名称",
colMode: "模式",
colStatus: "状态",
colKind: "类型",
colKind: "结论",
colFailFields: "失败项",
colActions: "操作",
colKey: "Key",
colKey: "身份",
colChange: "变更",
},
portTraffic: {

View file

@ -10206,12 +10206,12 @@ html.login-page--paused .login-page__flare {
.bs-cmp-nav__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 1px;
align-items: flex-start;
gap: 2px;
font-size: 10px;
font-variant-numeric: tabular-nums;
color: var(--bs-cmp-muted);
line-height: 1.15;
line-height: 1.25;
}
.bs-cmp-nav__meta em {
@ -10615,9 +10615,53 @@ html.login-page--paused .login-page__flare {
white-space: nowrap;
}
.bs-cmp-overview__verdict {
font-size: 1.05rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.bs-cmp-overview.is-ok .bs-cmp-overview__verdict {
color: #4ade80;
}
.bs-cmp-overview.is-warn .bs-cmp-overview__verdict {
color: #f87171;
}
.bs-cmp-nav__fail {
color: #f87171 !important;
font-style: normal;
}
.bs-cmp-nav__pass {
color: #4ade80 !important;
font-style: normal;
}
.bs-cmp-badge--fail {
background: rgba(248, 113, 113, 0.2);
color: #fecaca;
}
.bs-cmp-badge--pass {
background: rgba(74, 222, 128, 0.15);
color: #bbf7d0;
}
.bs-cmp-col-kind {
width: 72px;
white-space: nowrap;
width: 7.5rem;
white-space: normal;
}
.bs-cmp-col-kind .bs-cmp-badge {
display: inline-block;
margin: 0 4px 4px 0;
}
.bs-cmp-col-fail,
.bs-cmp-fail-cell {
max-width: 11rem;
font-size: 0.85em;
color: var(--bs-cmp-muted, #94a3b8);
}
.bs-cmp-val-reason {
font-size: 0.75em;
margin-top: 2px;
}
.bs-cmp-strip__kind--diff b {
color: #f87171;
}
.bs-cmp-badge {

File diff suppressed because it is too large Load diff

View file

@ -1854,6 +1854,11 @@ export const bizCompareListMetrics = () =>
role: string;
}>;
}>;
row_filter_presets?: Array<{
id: string;
label: string;
row_filters: Array<Record<string, unknown>>;
}>;
}>("/v1/biz-state/compare/metrics");
export const bizCompareCreateTemplate = (body: Record<string, unknown>) =>