Unify compare search via live source lookup; stratify success samples.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-08 17:47:40 +08:00
parent dd4195cdbc
commit b8c0f96982
8 changed files with 546 additions and 40 deletions

View file

@ -509,14 +509,16 @@ const en = {
sheetPending: "Pending",
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 sample of {{listed}} (hydrated from source tables for cutover spot-check). Pass rate uses all {{total}}.",
unchangedSampleHint: "{{total}} success rows total; browsing a stratified sample of {{listed}}. Search any route for live source lookup. Pass rate uses all {{total}}.",
liveSearchHint: "Search: live verdict from source batches (tabs only filter kind). Works for fail, success, and added.",
liveSearchTruncatedHint: "Search results truncated — narrow the query (full prefix / neighbor).",
storeUnchanged: "Store success rows",
storeUnchangedAuto: "Auto (full if small / sample 5k + hydrate if large)",
storeUnchangedSample: "Sample only (up to 5k keys + hydrate)",
storeUnchangedKeys: "All success keys (full browse; large write still slow)",
storeUnchangedAlways: "Always full JSON (slow on huge sheets)",
storeUnchangedNever: "Never (counts only)",
storeUnchangedHint: "Cutover focuses on fails and pass rate; success defaults to a sample. Pass rate always uses the full success count.",
storeUnchangedHint: "Cutover focuses on fails and pass rate; success defaults to a stratified sample. Type a filter to look up any route from source tables. Pass rate uses the full success count.",
saveJob: "Save config",
jobSaved: "Job config saved",
jobDeleted: "Compare job deleted",
@ -543,7 +545,7 @@ const en = {
pickRun: "Select run…",
runCount: "{{n}} runs",
resultEmpty: "No matching diff rows",
resultFilterPh: "Filter key / values…",
resultFilterPh: "Search prefix / neighbor / RD… (live source lookup)",
kindAll: "All",
kindDiff: "Fail",
kindAdded: "Added",
@ -575,7 +577,7 @@ const en = {
filterFailField: "Failed compare field",
filterFailFieldAll: "Any failed field",
resultEmpty: "No matching rows",
resultFilterPh: "Filter identity / values…",
resultFilterPh: "Search prefix / neighbor / RD… (live source lookup)",
diffCount: "{{n}} failed",
passRate: "Pass rate",
passOk: "All passed",

View file

@ -508,14 +508,16 @@ const zh = {
sheetPending: "等待中",
ranWithDuration: "比对完成(耗时 {{s}} 秒)",
unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。",
unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(从原表补全显示,割接抽查用)。通过率按全部 {{total}} 计。",
unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。",
liveSearchHint: "搜索:原表即时判定(页签只过滤种类)。失败/成功/新增均可查到。",
liveSearchTruncatedHint: "搜索结果已截断,请收窄条件(如完整前缀 / neighbor)。",
storeUnchanged: "成功行保存",
storeUnchangedAuto: "自动(小表全量 / 大表抽样 5000+原表补全)",
storeUnchangedSample: "仅抽样(最多 5000,身份键+原表补全)",
storeUnchangedKeys: "全量身份键(可翻完全部成功,大表写入仍较久)",
storeUnchangedAlways: "始终全量 JSON(大表很慢,慎用)",
storeUnchangedNever: "不保存(只计数量)",
storeUnchangedHint: "割接优先看失败与通过率;成功默认抽样抽查。通过率始终按全部成功计数。",
storeUnchangedHint: "割接优先看失败与通过率;成功默认分层抽样。输入筛选可回查原表(不依赖抽样)。通过率按全部成功计数。",
saveJob: "保存配置",
jobSaved: "任务配置已保存",
jobDeleted: "比对任务已删除",
@ -542,7 +544,7 @@ const zh = {
pickRun: "选择比对记录…",
runCount: "{{n}} 次",
resultEmpty: "无匹配失败/结果行",
resultFilterPh: "筛选身份 / 字段值…",
resultFilterPh: "搜索前缀 / neighbor / RD…(回查原表)",
kindAll: "全部",
kindDiff: "失败",
kindAdded: "新增",

View file

@ -38,6 +38,7 @@ import { jobChipColor, NmStatusChip } from "./nmChips";
type PageTab = "templates" | "jobs";
type JobDetailTab = "config" | "runs" | "result";
type KindFilter = "diff" | "all" | "added" | "removed" | "changed" | "unchanged";
type DiffListSource = "stored" | "live" | "";
type CreateJobStep = 0 | 1 | 2 | 3;
const CREATE_JOB_STEPS = 4;
@ -847,6 +848,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const [resultTotal, setResultTotal] = useState(0);
const [pagedDiffs, setPagedDiffs] = useState<DiffRow[]>([]);
const [diffsLoading, setDiffsLoading] = useState(false);
const [diffsSource, setDiffsSource] = useState<DiffListSource>("");
const [diffsTruncated, setDiffsTruncated] = useState(false);
const boardRef = useRef<HTMLDivElement | null>(null);
const tableScrollRef = useRef<HTMLDivElement | null>(null);
const tableScrollPosRef = useRef({ top: 0, left: 0 });
@ -1069,6 +1072,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
if (!runId || !mid || jobDetailTab !== "result" || sheetStillRunning) {
setPagedDiffs([]);
setResultTotal(0);
setDiffsSource("");
setDiffsTruncated(false);
return;
}
let cancelled = false;
@ -1086,6 +1091,10 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
if (cancelled) return;
setPagedDiffs((res.items || []) as DiffRow[]);
setResultTotal(Number(res.total || 0));
setDiffsSource(
String((res as any).source || "").toLowerCase() === "live" ? "live" : "stored",
);
setDiffsTruncated(Boolean((res as any).truncated));
const pages = Math.max(
1,
Math.ceil(Number(res.total || 0) / Number(res.page_size || resultPageSize)),
@ -1257,8 +1266,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
String(activeSheetCard?.status || ""),
);
const activeAdded = Number(activeSheetCard?.added || 0);
const activeRemoved = Number(activeSheetCard?.removed || 0);
const activeChanged = Number(activeSheetCard?.changed || 0);
const showFailCol =
kindFilter === "diff" || kindFilter === "all" || kindFilter === "added";
kindFilter === "diff" ||
kindFilter === "all" ||
kindFilter === "added" ||
kindFilter === "removed" ||
kindFilter === "changed";
const isLiveSearch = diffsSource === "live" && Boolean(debouncedResultKw.trim());
const resultEmptyColSpan =
1 +
(showFailCol ? 1 : 0) +
@ -3504,6 +3520,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{(
[
["diff", activeFail, "diff"],
["removed", activeRemoved, "removed"],
["changed", activeChanged, "changed"],
["added", activeAdded, "added"],
["unchanged", activeSuccess, "unchanged"],
["all", null, "all"],
@ -3523,7 +3541,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
? t("bizCompare.kindAll")
: id === "added"
? t("bizCompare.kindAddedShort")
: t("bizCompare.kindSuccess")}
: id === "removed"
? t("bizCompare.kindRemovedShort")
: id === "changed"
? t("bizCompare.kindChangedShort")
: t("bizCompare.kindSuccess")}
{n !== null ? (
<>
{" "}
@ -3543,11 +3565,31 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
</span>
</div>
{kindFilter === "unchanged" &&
{isLiveSearch ? (
<p className="muted bm-hint" style={{ margin: "4px 0 0" }}>
{diffsTruncated
? t("bizCompare.liveSearchTruncatedHint")
: t("bizCompare.liveSearchHint")}
</p>
) : null}
{!isLiveSearch &&
kindFilter === "unchanged" &&
(runDetail?.summary?.unchanged_truncated ||
(Number(runDetail?.summary?.unchanged || 0) >
Number(runDetail?.summary?.unchanged_listed || 0) &&
Number(runDetail?.summary?.unchanged_listed || 0) > 0)) ? (
(Number(
(activeRunSheet?.summary as any)?.unchanged ||
runDetail?.summary?.unchanged ||
0,
) >
Number(
(activeRunSheet?.summary as any)?.unchanged_listed ??
runDetail?.summary?.unchanged_listed ??
0,
) &&
Number(
(activeRunSheet?.summary as any)?.unchanged_listed ??
runDetail?.summary?.unchanged_listed ??
0,
) > 0)) ? (
<p className="muted bm-hint" style={{ margin: "4px 0 0" }}>
{t("bizCompare.unchangedSampleHint", {
listed: String(

View file

@ -2185,6 +2185,10 @@ export const bizCompareListRunDiffs = (params: {
page_size: number;
metric_id: string;
items: Record<string, unknown>[];
source?: string;
truncated?: boolean;
live_before_matched?: number;
live_after_matched?: number;
}>(`/v1/biz-state/compare/runs/${encodeURIComponent(params.runId)}/diffs?${p.toString()}`);
};