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 <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-08 23:37:54 +08:00
parent ee3ed26c88
commit 8a79e962a8
4 changed files with 259 additions and 88 deletions

View file

@ -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}}.",

View file

@ -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}} 计。",

View file

@ -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;

View file

@ -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: {
<td
className={`bs-cmp-val-cell bs-cmp-val-cell--pair${
mismatch || isAdded || isRemoved ? " bs-cmp-val-cell--diff" : ""
}${isAdded ? " is-added" : ""}${isRemoved ? " is-removed" : ""}${
}${same ? " is-same" : ""}${isAdded ? " is-added" : ""}${isRemoved ? " is-removed" : ""}${
zoneStart ? " bs-cmp-zone-start" : ""
}`}
title={
same
? pre
: `${beforeLabel} ${isAdded ? "—" : pre} → ${afterLabel} ${isRemoved ? "—" : post}`
}
>
<div className="bs-cmp-pair">
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{beforeLabel}</span>
<span className={preClass}>{isAdded ? "—" : pre}</span>
{same ? (
<div className="bs-cmp-pair bs-cmp-pair--same">
<span className="bs-cmp-val bs-cmp-val--same">{pre || "—"}</span>
</div>
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{afterLabel}</span>
<span className={postClass}>{isRemoved ? "—" : post}</span>
) : (
<div className="bs-cmp-pair">
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{beforeLabel}</span>
<span className={preClass}>{isAdded ? "—" : pre}</span>
</div>
<div className="bs-cmp-pair__row">
<span className="bs-cmp-pair__tag">{afterLabel}</span>
<span className={postClass}>{isRemoved ? "—" : post}</span>
</div>
</div>
</div>
)}
{reason ? <div className="bs-cmp-val-reason muted">{reason}</div> : null}
</td>
);
@ -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<Record<string, string>>({});
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
</div>
{runDetail && runIsActive ? (
<div className="bs-cmp-progress" role="status" aria-live="polite">
<div className="bs-cmp-progress is-compact" role="status" aria-live="polite">
<div className="bs-cmp-progress__head">
<NmStatusChip color="accent">{t("bizCompare.runStatusRunning")}</NmStatusChip>
{runEngine === "sql" || runEngine === "python" ? (
@ -3537,36 +3564,54 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<div className="bs-cmp-nav__list" role="tablist">
{(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 ?? "…"}%`
}
>
<span className="bs-cmp-nav__dot" aria-hidden />
@ -3650,9 +3701,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
: ` · ${activeSheetCard?.before_count ?? 0}→${activeSheetCard?.after_count ?? 0}`}
</span>
</div>
<div className="bs-cmp-strip__pass" title={t("bizCompare.passRate")}>
<div
className="bs-cmp-strip__pass"
title={`${t("bizCompare.passRate")} · ${t("bizCompare.passRateScope")}`}
>
<span className="bs-cmp-strip__pass-label">
{t("bizCompare.passRate")}
<span className="bs-cmp-strip__pass-scope">
{t("bizCompare.passRateScope")}
</span>
</span>
<span className="bs-cmp-strip__pass-value">
{activeSheetPending || activePassRate === null
@ -3708,9 +3765,31 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<span className="muted bs-sheet-count">
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
</span>
{resultSearchKeyFields.length ? (
<button
type="button"
className={`bs-cmp-strip__toggle${keyFiltersVisible ? " is-active" : ""}`}
onClick={() => setKeyFiltersOpen((v) => !v)}
>
{t("bizCompare.keyFiltersToggle")}
{activeKeyFilterCount
? ` (${activeKeyFilterCount})`
: ""}
</button>
) : null}
<button
type="button"
className={`bs-cmp-strip__toggle${hideDisplayCols ? " is-active" : ""}`}
title={t("bizCompare.hideDisplayColsHint")}
onClick={() => setHideDisplayCols((v) => !v)}
>
{hideDisplayCols
? t("bizCompare.showDisplayCols")
: t("bizCompare.hideDisplayCols")}
</button>
</div>
{resultSearchKeyFields.length ? (
<div className="bs-cmp-key-filters" style={{ marginTop: 6 }}>
{resultSearchKeyFields.length && keyFiltersVisible ? (
<div className="bs-cmp-key-filters">
{resultSearchKeyFields.map((f) => (
<label key={f} className="bs-cmp-key-filter">
<span className="muted">{f}</span>
@ -3735,9 +3814,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
/>
</label>
))}
{Object.keys(resultKeyFilters).some((k) =>
String(resultKeyFilters[k] || "").trim(),
) ? (
{activeKeyFilterCount ? (
<button
type="button"
className="linkish"
@ -3749,7 +3826,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
</div>
) : null}
{isLiveSearch ? (
<p className="muted bm-hint" style={{ margin: "4px 0 0" }}>
<p className="muted bm-hint bs-cmp-strip__hint">
{diffsTruncated
? t("bizCompare.liveSearchTruncatedHint")
: t("bizCompare.liveSearchHint")}
@ -3773,7 +3850,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
runDetail?.summary?.unchanged_listed ??
0,
) > 0)) ? (
<p className="muted bm-hint" style={{ margin: "4px 0 0" }}>
<p className="muted bm-hint bs-cmp-strip__hint">
{t("bizCompare.unchangedSampleHint", {
listed: String(
Number(
@ -3808,9 +3885,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const compareCols = resultColumns.extras.filter((f) =>
resultColumns.compareSet.has(f),
);
const displayCols = resultColumns.extras.filter(
(f) => !resultColumns.compareSet.has(f),
);
const displayCols = hideDisplayCols
? []
: resultColumns.extras.filter(
(f) => !resultColumns.compareSet.has(f),
);
const verdictColSpan = showFailCol ? 2 : 1;
const hasGroups =
keyCols.length + compareCols.length + displayCols.length > 0;
@ -3942,36 +4021,41 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{cellText(d.key?.[k] ?? pre[k] ?? post[k]) || "—"}
</td>
))}
{resultColumns.extras.map((f, fi) => {
const pv = cellText(pre[f]);
const av = cellText(post[f]);
const ch = d.changes?.[f];
const isCmp = resultColumns.compareSet.has(f);
const prev = resultColumns.extras[fi - 1];
const prevCmp = prev
? resultColumns.compareSet.has(prev)
: null;
const zoneStart = fi === 0 || prevCmp !== isCmp;
const mismatch =
d.kind === "added" || d.kind === "removed"
? Boolean(pv || av)
: isCmp
? Boolean(ch)
: pv !== av;
return (
<PairCell
key={f}
beforeText={pv}
afterText={av}
kind={d.kind}
mismatch={mismatch}
reason={ch?.reason}
beforeLabel={t("bizCompare.pairBefore")}
afterLabel={t("bizCompare.pairAfter")}
zoneStart={zoneStart}
/>
);
})}
{resultColumns.extras
.filter(
(f) =>
resultColumns.compareSet.has(f) || !hideDisplayCols,
)
.map((f, fi, visibleExtras) => {
const pv = cellText(pre[f]);
const av = cellText(post[f]);
const ch = d.changes?.[f];
const isCmp = resultColumns.compareSet.has(f);
const prev = visibleExtras[fi - 1];
const prevCmp = prev
? resultColumns.compareSet.has(prev)
: null;
const zoneStart = fi === 0 || prevCmp !== isCmp;
const mismatch =
d.kind === "added" || d.kind === "removed"
? Boolean(pv || av)
: isCmp
? Boolean(ch)
: pv !== av;
return (
<PairCell
key={f}
beforeText={pv}
afterText={av}
kind={d.kind}
mismatch={mismatch}
reason={ch?.reason}
beforeLabel={t("bizCompare.pairBefore")}
afterLabel={t("bizCompare.pairAfter")}
zoneStart={zoneStart}
/>
);
})}
</tr>
);
})}