Fix cutover compare tab switching and order detail columns Key/Compare/Display.

Remount BizComparePage per route so templates↔jobs no longer stuck; align column order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 16:52:37 +08:00
parent 3d2489041a
commit 34a98fefcf
5 changed files with 57 additions and 34 deletions

View file

@ -157,31 +157,32 @@ def effective_display_fields(
compare_fields: Sequence[str],
display_fields: Sequence[str] | None = None,
) -> list[str]:
"""Result-table columns: explicit display, else key+compare (legacy).
"""Result-table columns in Key → Compare → Display-only order.
Keys always lead; remaining display/compare fields follow in given order
without duplicates.
Keys always lead; compare fields follow in template order; remaining
display (context) fields come last. Compare fields are included even if
the UI forgot to tick display.
"""
keys = [str(x).strip() for x in (key_fields or []) if str(x).strip()]
key_set = set(keys)
compare = [str(x).strip() for x in (compare_fields or []) if str(x).strip()]
raw_disp = display_fields
if raw_disp is None:
# Legacy templates: show key + compare only
extra = [f for f in compare if f not in key_set]
return keys + extra
disp = [str(x).strip() for x in raw_disp if str(x).strip()]
# Force keys first (always visible)
compare = [
str(x).strip()
for x in (compare_fields or [])
if str(x).strip() and str(x).strip() not in key_set
]
if display_fields is None:
return keys + compare
disp = [str(x).strip() for x in display_fields if str(x).strip()]
compare_set = set(compare)
out: list[str] = list(keys)
seen = set(keys)
for f in disp:
for f in compare:
if f in seen:
continue
out.append(f)
seen.add(f)
# Ensure compare fields appear even if UI forgot to tick display
for f in compare:
if f in seen or f in key_set:
for f in disp:
if f in seen or f in key_set or f in compare_set:
continue
out.append(f)
seen.add(f)

View file

@ -1560,14 +1560,14 @@ def _csv_cell(v: Any) -> str:
def _sheet_csv(sheet: dict[str, Any]) -> str:
keys = list(sheet.get("key_fields") or [])
key_set = set(keys)
compare = list(sheet.get("compare_fields") or [])
compare = [f for f in list(sheet.get("compare_fields") or []) if f not in key_set]
compare_set = set(compare)
display = effective_display_fields(
key_fields=keys,
compare_fields=compare,
display_fields=list(sheet.get("display_fields") or []) or None,
)
# Non-key display columns: compare fields get pre/post; display-only get single value col
# Non-key columns already ordered Key→Compare→Display by effective_display_fields
extra = [f for f in display if f not in key_set]
headers = ["kind", *keys]
for f in extra:

View file

@ -289,8 +289,8 @@ class CompareRulesTests(unittest.TestCase):
compare_fields=["mac"],
display_fields=["vrf", "age", "ip"],
)
# keys first, then display order extras, compare forced if missing
self.assertEqual(explicit, ["ip", "iface", "vrf", "age", "mac"])
# Key → Compare → Display-only (compare forced even if not ticked)
self.assertEqual(explicit, ["ip", "iface", "mac", "vrf", "age"])
def test_display_only_field_does_not_change(self) -> None:
"""Context column in display but not compare → value drift ignored."""

View file

@ -144,11 +144,14 @@ function ProtectedApp() {
element={<Navigate to="/network/cutover/biz-migration" replace />}
/>
<Route path="cutover/biz-state" element={<BizStatePage />} />
<Route path="cutover/biz-compare" element={<BizComparePage pageMode="jobs" />} />
<Route
path="cutover/biz-compare"
element={<BizComparePage key="biz-compare-jobs" pageMode="jobs" />}
/>
<Route path="cutover/biz-migration" element={<BizMigrationPage />} />
<Route
path="cutover/compare-templates"
element={<BizComparePage pageMode="templates" />}
element={<BizComparePage key="biz-compare-templates" pageMode="templates" />}
/>
<Route path="cutover/monitor-templates" element={<BizMonitorTemplatesPage />} />
</Route>

View file

@ -754,6 +754,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
const { showOk, showError } = useToast();
const [pageTab, setPageTab] = useState<PageTab>(pageMode === "templates" ? "templates" : "jobs");
// Route-bound modes must follow pageMode; internal tabs only when pageMode === "all".
// Without this, React reuses the same component instance across
// /biz-compare ↔ /compare-templates and pageTab stays stuck.
const activeTab: PageTab =
pageMode === "templates" ? "templates" : pageMode === "jobs" ? "jobs" : pageTab;
const showTabSwitch = pageMode === "all";
const [busy, setBusy] = useState(false);
@ -838,6 +843,20 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refresh]);
// Keep local tab + dismiss overlays when route mode flips (component may be reused).
useEffect(() => {
if (pageMode === "templates") setPageTab("templates");
else if (pageMode === "jobs") setPageTab("jobs");
setTplOpen(false);
setJobCreateOpen(false);
setCreateStep(0);
setJobId("");
setRuns([]);
setRunDetail(null);
setResultSheetId("");
setBoardFs(false);
}, [pageMode]);
useEffect(() => {
void (async () => {
if (!beforeTaskId) {
@ -1046,12 +1065,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
let display = (activeRunSheet?.display_fields || []).filter(Boolean);
if (!display.length) {
display = [...keys, ...compare];
} else {
// keys first, then rest of display order
const rest = display.filter((f) => !keySet.has(f));
display = [...keys, ...rest];
}
let extras = display.filter((f) => !keySet.has(f));
// Column order: Key → Compare → Display-only (context)
// Compare fields always appear (even if display unticked), matching backend.
const compareCols = [...compare];
const displayOnly = display.filter((f) => !keySet.has(f) && !compareSet.has(f));
let extras = [...compareCols, ...displayOnly];
// Presence / sparse display: if no value columns, pull fields from sample
// added/removed rows so 缺失/多余 still show side data.
if (!extras.length && pagedDiffs.length) {
@ -1066,7 +1085,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
}
return {
keys,
compare,
compare: compareCols,
extras,
compareSet,
presence: !(activeRunSheet?.compare_fields || []).length,
@ -1922,7 +1941,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
{pageMode === "templates" ? t("bizCompare.templates") : t("bizCompare.title")}
</h2>
<div className="btn-row">
{pageTab === "templates" ? (
{activeTab === "templates" ? (
<>
<input
ref={tplImportRef}
@ -1959,16 +1978,16 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<div className="btn-row nm-config-modal__tabs" role="tablist">
<Button
size="sm"
variant={pageTab === "jobs" ? "primary" : "secondary"}
className={pageTab === "jobs" ? "is-active" : undefined}
variant={activeTab === "jobs" ? "primary" : "secondary"}
className={activeTab === "jobs" ? "is-active" : undefined}
onPress={() => setPageTab("jobs")}
>
{t("bizCompare.jobList")}
</Button>
<Button
size="sm"
variant={pageTab === "templates" ? "primary" : "secondary"}
className={pageTab === "templates" ? "is-active" : undefined}
variant={activeTab === "templates" ? "primary" : "secondary"}
className={activeTab === "templates" ? "is-active" : undefined}
onPress={() => setPageTab("templates")}
>
{t("bizCompare.templates")}
@ -1980,13 +1999,13 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
<Input
value={listKw}
placeholder={
pageTab === "jobs" ? t("bizCompare.jobFilterPh") : t("bizCompare.templateFilterPh")
activeTab === "jobs" ? t("bizCompare.jobFilterPh") : t("bizCompare.templateFilterPh")
}
onChange={(e) => setListKw(e.target.value)}
/>
</div>
{pageTab === "templates" ? (
{activeTab === "templates" ? (
<div className="pt-list-table-wrap">
<p className="muted" style={{ margin: "0 0 8px" }}>
{t("bizCompare.templateImportHint")}