Store compare diffs in row table with server-side paging per sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-17 19:59:06 +08:00
parent 33264d7b76
commit 499a5fec2e
8 changed files with 487 additions and 69 deletions

View file

@ -1,5 +1,6 @@
import { Button, Input, Modal } from "@heroui/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ListPager } from "../../components/ListPager";
import { AppModalShell } from "../../components/ui/AppModalShell";
import { FieldSelect } from "../../components/ui/FieldSelect";
import { useDebouncedValue } from "../../hooks/useDebouncedValue";
@ -16,6 +17,7 @@ import {
bizCompareListJobs,
bizCompareListMappings,
bizCompareListMetrics,
bizCompareListRunDiffs,
bizCompareListRuns,
bizCompareListTemplates,
bizCompareRunJob,
@ -197,10 +199,15 @@ export function BizComparePage() {
const [runDetail, setRunDetail] = useState<any>(null);
const [resultSheetId, setResultSheetId] = useState("");
// result filters
// result filters (server-paged)
const [kindFilter, setKindFilter] = useState<KindFilter>("diff");
const [resultKw, setResultKw] = useState("");
const debouncedResultKw = useDebouncedValue(resultKw, 200);
const debouncedResultKw = useDebouncedValue(resultKw, 300);
const [resultPage, setResultPage] = useState(1);
const [resultPageSize, setResultPageSize] = useState(100);
const [resultTotal, setResultTotal] = useState(0);
const [pagedDiffs, setPagedDiffs] = useState<DiffRow[]>([]);
const [diffsLoading, setDiffsLoading] = useState(false);
const boardRef = useRef<HTMLDivElement | null>(null);
const [boardFs, setBoardFs] = useState(false);
@ -316,38 +323,71 @@ export function BizComparePage() {
[runSheets, resultSheetId],
);
// Reset page when sheet / filter / page size changes
useEffect(() => {
setResultPage(1);
}, [resultSheetId, kindFilter, debouncedResultKw, resultPageSize, runDetail?.id]);
useEffect(() => {
const runId = String(runDetail?.id || "");
const mid = resultSheetId || activeRunSheet?.metric_id || "";
if (!runId || !mid || jobDetailTab !== "result") {
setPagedDiffs([]);
setResultTotal(0);
return;
}
let cancelled = false;
void (async () => {
setDiffsLoading(true);
try {
const res = await bizCompareListRunDiffs({
runId,
metricId: mid,
kind: kindFilter,
kw: debouncedResultKw.trim(),
page: resultPage,
pageSize: resultPageSize,
});
if (cancelled) return;
setPagedDiffs((res.items || []) as DiffRow[]);
setResultTotal(Number(res.total || 0));
const pages = Math.max(
1,
Math.ceil(Number(res.total || 0) / Number(res.page_size || resultPageSize)),
);
if (resultPage > pages) setResultPage(pages);
} catch (e) {
if (!cancelled) {
setPagedDiffs([]);
setResultTotal(0);
showError(formatErr(e));
}
} finally {
if (!cancelled) setDiffsLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [
runDetail?.id,
resultSheetId,
activeRunSheet?.metric_id,
kindFilter,
debouncedResultKw,
resultPage,
resultPageSize,
jobDetailTab,
showError,
]);
const resultColumns = useMemo(() => {
const keys =
activeRunSheet?.key_fields?.length
? activeRunSheet.key_fields
: Object.keys((activeRunSheet?.diffs?.[0]?.key as any) || {});
const keys = activeRunSheet?.key_fields?.length
? activeRunSheet.key_fields
: Object.keys((pagedDiffs[0]?.key as Record<string, unknown>) || {});
const compare = (activeRunSheet?.compare_fields || []).filter((f) => !keys.includes(f));
return { keys, compare, presence: !(activeRunSheet?.compare_fields || []).length };
}, [activeRunSheet]);
const filteredDiffs = useMemo(() => {
const diffs = (activeRunSheet?.diffs || []) as DiffRow[];
const kw = debouncedResultKw.trim().toLowerCase();
return diffs.filter((d) => {
if (kindFilter === "diff") {
if (d.kind === "unchanged") return false;
} else if (kindFilter !== "all" && d.kind !== kindFilter) {
return false;
}
if (!kw) return true;
const blob = [
d.kind,
...Object.values(d.key || {}),
...Object.values(d.before || {}),
...Object.values(d.after || {}),
JSON.stringify(d.changes || {}),
]
.map(cellText)
.join(" ")
.toLowerCase();
return blob.includes(kw);
});
}, [activeRunSheet, kindFilter, debouncedResultKw]);
}, [activeRunSheet, pagedDiffs]);
const kindLabel = (kind: string) => {
if (kind === "added") return t("bizCompare.kindAddedShort");
@ -358,6 +398,7 @@ export function BizComparePage() {
};
const summary = runDetail?.summary || {};
const resultPages = Math.max(1, Math.ceil(resultTotal / Math.max(1, resultPageSize)));
const sheetCards = useMemo(() => {
const raw = (summary.sheet_cards || []) as Array<{
metric_id: string;
@ -1484,7 +1525,9 @@ export function BizComparePage() {
onChange={(e) => setResultKw(e.target.value)}
/>
<span className="muted bs-sheet-count">
{filteredDiffs.length}/{(activeRunSheet?.diffs || []).length}
{diffsLoading
? "…"
: `${pagedDiffs.length}/${resultTotal}`}
{resultColumns.presence ? ` · ${t("bizCompare.presenceShort")}` : ""}
</span>
</div>
@ -1503,7 +1546,7 @@ export function BizComparePage() {
</tr>
</thead>
<tbody>
{filteredDiffs.slice(0, 2000).map((d, i) => {
{pagedDiffs.map((d, i) => {
const pre = (d.mapped_before || d.before || {}) as Record<
string,
unknown
@ -1569,7 +1612,7 @@ export function BizComparePage() {
</tr>
);
})}
{runDetail && !filteredDiffs.length ? (
{runDetail && !diffsLoading && !pagedDiffs.length ? (
<tr>
<td
colSpan={
@ -1585,6 +1628,20 @@ export function BizComparePage() {
</tbody>
</table>
</div>
<ListPager
page={resultPage}
pages={resultPages}
total={resultTotal}
pageSize={resultPageSize}
pageSizeOptions={[50, 100, 200, 500]}
onPageChange={setResultPage}
onPageSizeChange={(n) => {
setResultPageSize(n);
setResultPage(1);
}}
disabled={diffsLoading || !runDetail}
/>
</div>
</div>
) : (

View file

@ -1912,6 +1912,29 @@ export const bizCompareListRuns = (jobId: string, limit = 20) =>
export const bizCompareGetRun = (runId: string) =>
apiGet<Record<string, unknown>>(`/v1/biz-state/compare/runs/${encodeURIComponent(runId)}`);
export const bizCompareListRunDiffs = (params: {
runId: string;
metricId?: string;
kind?: string;
kw?: string;
page?: number;
pageSize?: number;
}) => {
const p = new URLSearchParams();
if (params.metricId) p.set("metric_id", params.metricId);
if (params.kind) p.set("kind", params.kind);
if (params.kw) p.set("kw", params.kw);
p.set("page", String(Math.max(1, Number(params.page || 1))));
p.set("page_size", String(Math.max(1, Math.min(500, Number(params.pageSize || 100)))));
return apiGet<{
total: number;
page: number;
page_size: number;
metric_id: string;
items: Record<string, unknown>[];
}>(`/v1/biz-state/compare/runs/${encodeURIComponent(params.runId)}/diffs?${p.toString()}`);
};
export const bizCompareDownloadRun = async (runId: string): Promise<void> => {
const path = `/v1/biz-state/compare/runs/${encodeURIComponent(runId)}/export`;
const res = await fetch(path, { method: "GET", credentials: fetchCreds, headers: authHeaders() });