mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
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:
parent
3d2489041a
commit
34a98fefcf
5 changed files with 57 additions and 34 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue