From 8a79e962a889902143d98e785b5b05d22b2d1479 Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 8 Oct 2026 23:37:54 +0800 Subject: [PATCH] Tighten biz compare fullscreen board for cutover scanning. Collapse equal pair cells, hide display columns by default, fold field filters, and keep failed sheets first so diffs stand out. Co-authored-by: Cursor --- web/src/i18n/en.ts | 8 + web/src/i18n/zh.ts | 8 + web/src/index.css | 79 ++++++- web/src/pages/network/BizComparePage.tsx | 252 +++++++++++++++-------- 4 files changed, 259 insertions(+), 88 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 478d12a..5246ade 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -511,6 +511,9 @@ const en = { phaseQueued: "Queued", phaseLoading: "Loading", phaseSqlCount: "Counting", + phaseSqlProject: "Preparing tables", + phaseSqlJoin: "Joining", + phaseSqlFailFetch: "Fetching fails", phaseComparing: "Comparing", phasePersisting: "Writing", phasePersistingFail: "Writing fails", @@ -520,6 +523,11 @@ const en = { phaseCancelled: "Cancelled", sheetPending: "Pending", colProgress: "Progress / result", + passRateScope: "all rows", + keyFiltersToggle: "Field filters", + hideDisplayCols: "Hide display cols", + showDisplayCols: "Show display cols", + hideDisplayColsHint: "Compact: hide display-only columns so keys and diffs stand out", ranWithDuration: "Compare finished ({{s}}s)", unchangedNotStored: "Success rows were counted but not stored. Set “Store success rows” to sample and re-run for spot-check.", unchangedSampleHint: "{{total}} success rows total; browsing a stratified sample of {{listed}}. Search any route for live source lookup. Pass rate uses all {{total}}.", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7369dfd..168cdf4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -510,6 +510,9 @@ const zh = { phaseQueued: "排队中", phaseLoading: "加载数据", phaseSqlCount: "库内统计", + phaseSqlProject: "准备比对表", + phaseSqlJoin: "库内比对", + phaseSqlFailFetch: "拉取失败行", phaseComparing: "比对中", phasePersisting: "写入结果", phasePersistingFail: "写入失败行", @@ -519,6 +522,11 @@ const zh = { phaseCancelled: "已取消", sheetPending: "等待中", colProgress: "进度 / 结果", + passRateScope: "全表", + keyFiltersToggle: "字段筛选", + hideDisplayCols: "隐藏展示列", + showDisplayCols: "显示展示列", + hideDisplayColsHint: "紧凑模式:默认隐藏不参与比对的展示列,突出 Key 与差异", ranWithDuration: "比对完成(耗时 {{s}} 秒)", unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。", unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。", diff --git a/web/src/index.css b/web/src/index.css index d3e61d5..2046460 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -12970,10 +12970,6 @@ body:has(.bs-cmp-board:fullscreen) { vertical-align: middle; } -.bs-cmp-val-cell--diff { - background: rgba(245, 158, 11, 0.08); -} - .bs-cmp-pair { display: flex; flex-direction: column; @@ -12981,6 +12977,10 @@ body:has(.bs-cmp-board:fullscreen) { min-width: 4.5rem; } +.bs-cmp-pair--same { + min-width: 3rem; +} + .bs-cmp-pair__row { display: grid; grid-template-columns: 1.6em minmax(0, 1fr); @@ -13005,6 +13005,20 @@ body:has(.bs-cmp-board:fullscreen) { color: #86efac; } +.bs-cmp-val-cell--pair.is-same { + background: transparent; +} + +.bs-cmp-val--same { + color: #cbd5e1; + opacity: 0.92; +} + +.bs-cmp-val-cell--diff { + background: rgba(245, 158, 11, 0.1); + box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.22); +} + .bs-cmp-val { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; color: #e2e8f0; @@ -13012,6 +13026,63 @@ body:has(.bs-cmp-board:fullscreen) { line-height: 1.35; } +.bs-cmp-progress.is-compact { + padding: 6px 10px; + gap: 4px; +} + +.bs-cmp-progress.is-compact .bs-cmp-progress__track { + height: 4px; +} + +.bs-cmp-progress.is-compact .bs-cmp-progress__meta { + font-size: 0.75rem; +} + +.bs-cmp-strip__pass-scope { + margin-left: 4px; + font-size: 9px; + font-weight: 600; + color: #64748b; + text-transform: none; +} + +.bs-cmp-strip__toggle { + flex: 0 0 auto; + height: 28px; + padding: 0 8px; + border-radius: 6px; + border: 1px solid rgba(148, 163, 184, 0.28); + background: rgba(15, 23, 42, 0.45); + color: #cbd5e1; + font-size: 11px; + cursor: pointer; + white-space: nowrap; +} + +.bs-cmp-strip__toggle:hover { + border-color: rgba(96, 165, 250, 0.45); + color: #f1f5f9; +} + +.bs-cmp-strip__toggle.is-active { + border-color: rgba(59, 130, 246, 0.5); + background: rgba(37, 99, 235, 0.18); + color: #93c5fd; +} + +.bs-cmp-strip__hint { + flex: 1 1 100%; + margin: 0; + font-size: 0.75rem; + line-height: 1.35; +} + +.bs-cmp-strip .bs-cmp-key-filters { + flex: 1 1 100%; + margin-top: 0; +} + .bs-cmp-val.is-empty { color: #64748b; font-style: italic; diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 5ed8927..aa3a018 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -196,6 +196,8 @@ function PairCell(props: { const post = afterText || "—"; const isAdded = kind === "added"; const isRemoved = kind === "removed"; + const same = + !mismatch && !isAdded && !isRemoved && String(beforeText || "") === String(afterText || ""); // Whole-row missing/extra: emphasize the present side; do not strike it out. const preClass = [ "bs-cmp-val", @@ -217,20 +219,31 @@ function PairCell(props: { -
-
- {beforeLabel} - {isAdded ? "—" : pre} + {same ? ( +
+ {pre || "—"}
-
- {afterLabel} - {isRemoved ? "—" : post} + ) : ( +
+
+ {beforeLabel} + {isAdded ? "—" : pre} +
+
+ {afterLabel} + {isRemoved ? "—" : post} +
-
+ )} {reason ?
{reason}
: null} ); @@ -363,6 +376,9 @@ function localizeRunPhase(phase: string | undefined, t: (key: string) => string) queued: "bizCompare.phaseQueued", loading: "bizCompare.phaseLoading", sql_count: "bizCompare.phaseSqlCount", + sql_project: "bizCompare.phaseSqlProject", + sql_join: "bizCompare.phaseSqlJoin", + sql_fail_fetch: "bizCompare.phaseSqlFailFetch", comparing: "bizCompare.phaseComparing", done: "bizCompare.phaseDone", failed: "bizCompare.phaseFailed", @@ -944,6 +960,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage const [resultKw, setResultKw] = useState(""); const debouncedResultKw = useDebouncedValue(resultKw, 300); const [resultKeyFilters, setResultKeyFilters] = useState>({}); + const [keyFiltersOpen, setKeyFiltersOpen] = useState(false); + const [hideDisplayCols, setHideDisplayCols] = useState(true); const debouncedKeyFiltersJson = useDebouncedValue(JSON.stringify(resultKeyFilters), 300); const debouncedKeyFilters = useMemo(() => { try { @@ -959,6 +977,10 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage }, [debouncedKeyFiltersJson]); const hasResultSearch = Boolean(debouncedResultKw.trim()) || Object.keys(debouncedKeyFilters).length > 0; + const activeKeyFilterCount = Object.keys(resultKeyFilters).filter((k) => + String(resultKeyFilters[k] || "").trim(), + ).length; + const keyFiltersVisible = keyFiltersOpen || activeKeyFilterCount > 0; const [resultPage, setResultPage] = useState(1); const [resultPageSize, setResultPageSize] = useState(100); const [resultTotal, setResultTotal] = useState(0); @@ -1419,7 +1441,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage 1 + (showFailCol ? 1 : 0) + resultColumns.keys.length + - Math.max(resultColumns.extras.length, 0); + Math.max( + resultColumns.extras.filter( + (f) => resultColumns.compareSet.has(f) || !hideDisplayCols, + ).length, + 0, + ); const rememberTableScroll = useCallback(() => { const wrap = tableScrollRef.current; @@ -3404,7 +3431,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{runDetail && runIsActive ? ( -
+
{t("bizCompare.runStatusRunning")} {runEngine === "sql" || runEngine === "python" ? ( @@ -3537,36 +3564,54 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{(sheetCards.length ? sheetCards - : runSheets.map((s) => { - const removed = Number(s.summary?.removed || 0); - const changed = Number(s.summary?.changed || 0); - const unchanged = Number(s.summary?.unchanged || 0); - const fail = removed + changed; - const judged = fail + unchanged; - const st = String((s as any).status || "done"); - const pending = ["pending", "running", "queued"].includes(st); - return { - sheet_id: sheetIdentity(s), - title: sheetLabel(s), - metric_id: s.metric_id, - mode: s.mode, - status: st, - added: s.summary?.added, - removed, - changed, - unchanged, - before_count: s.summary?.before_count, - after_count: s.summary?.after_count, - fail_count: fail, - success_count: unchanged, - diff_count: fail, - pass_rate: pending - ? null - : judged - ? Math.round((unchanged / judged) * 1000) / 10 - : 100, - }; - }) + : [...runSheets] + .map((s) => { + const removed = Number(s.summary?.removed || 0); + const changed = Number(s.summary?.changed || 0); + const unchanged = Number(s.summary?.unchanged || 0); + const fail = removed + changed; + const judged = fail + unchanged; + const st = String((s as any).status || "done"); + const pending = ["pending", "running", "queued"].includes(st); + return { + sheet_id: sheetIdentity(s), + title: sheetLabel(s), + metric_id: s.metric_id, + mode: s.mode, + status: st, + added: s.summary?.added, + removed, + changed, + unchanged, + before_count: s.summary?.before_count, + after_count: s.summary?.after_count, + fail_count: fail, + success_count: unchanged, + diff_count: fail, + pass_rate: pending + ? null + : judged + ? Math.round((unchanged / judged) * 1000) / 10 + : 100, + }; + }) + .sort((a, b) => { + const pa = ["pending", "running", "queued"].includes( + String(a.status || ""), + ) + ? 1 + : 0; + const pb = ["pending", "running", "queued"].includes( + String(b.status || ""), + ) + ? 1 + : 0; + if (pa !== pb) return pa - pb; + const da = Number(a.fail_count || 0); + const db = Number(b.fail_count || 0); + if (da !== db) return db - da; + return String(a.metric_id).localeCompare(String(b.metric_id)); + }) ).map((c) => { const fail = sheetFailOf(c); const ok = sheetSuccessOf(c); @@ -3586,11 +3631,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage className={`bs-cmp-nav__item${active ? " is-active" : ""}${ pending ? " is-pending" : fail > 0 ? " has-diff" : " is-clean" }`} - onClick={() => setResultSheetId(id)} + onClick={() => { + setResultSheetId(id); + // Cutover default: land on fails when switching sheets + if (fail > 0 && kindFilter === "unchanged") { + setKindFilter("diff"); + } + }} title={ pending ? `${label} · ${t("bizCompare.sheetPending")}` - : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate}%` + : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate ?? "…"}%` } > @@ -3650,9 +3701,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage : ` · ${activeSheetCard?.before_count ?? 0}→${activeSheetCard?.after_count ?? 0}`}
-
+
{t("bizCompare.passRate")} + + {t("bizCompare.passRateScope")} + {activeSheetPending || activePassRate === null @@ -3708,9 +3765,31 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage {diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`} + {resultSearchKeyFields.length ? ( + + ) : null} +
- {resultSearchKeyFields.length ? ( -
+ {resultSearchKeyFields.length && keyFiltersVisible ? ( +
{resultSearchKeyFields.map((f) => ( ))} - {Object.keys(resultKeyFilters).some((k) => - String(resultKeyFilters[k] || "").trim(), - ) ? ( + {activeKeyFilterCount ? (