mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 11:20:48 +08:00
Add per-field compare result filters for live source lookup.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b8c0f96982
commit
7b5dd3eada
7 changed files with 256 additions and 36 deletions
|
|
@ -510,8 +510,9 @@ const en = {
|
|||
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}}.",
|
||||
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).",
|
||||
liveSearchHint: "Field filters look up source batches live; tabs only filter kind. E.g. direction=out, network=1.1.1.1.",
|
||||
liveSearchTruncatedHint: "Search results truncated — narrow the field filters.",
|
||||
clearKeyFilters: "Clear fields",
|
||||
storeUnchanged: "Store success rows",
|
||||
storeUnchangedAuto: "Auto (full if small / sample 5k + hydrate if large)",
|
||||
storeUnchangedSample: "Sample only (up to 5k keys + hydrate)",
|
||||
|
|
@ -545,7 +546,7 @@ const en = {
|
|||
pickRun: "Select run…",
|
||||
runCount: "{{n}} runs",
|
||||
resultEmpty: "No matching diff rows",
|
||||
resultFilterPh: "Search prefix / neighbor / RD… (live source lookup)",
|
||||
resultFilterPh: "Free text, or direction:out network:1.1.1.1",
|
||||
kindAll: "All",
|
||||
kindDiff: "Fail",
|
||||
kindAdded: "Added",
|
||||
|
|
@ -577,7 +578,7 @@ const en = {
|
|||
filterFailField: "Failed compare field",
|
||||
filterFailFieldAll: "Any failed field",
|
||||
resultEmpty: "No matching rows",
|
||||
resultFilterPh: "Search prefix / neighbor / RD… (live source lookup)",
|
||||
resultFilterPh: "Free text, or direction:out network:1.1.1.1",
|
||||
diffCount: "{{n}} failed",
|
||||
passRate: "Pass rate",
|
||||
passOk: "All passed",
|
||||
|
|
|
|||
|
|
@ -509,8 +509,9 @@ const zh = {
|
|||
ranWithDuration: "比对完成(耗时 {{s}} 秒)",
|
||||
unchangedNotStored: "成功行仅统计数量未落库。可在任务配置将「成功行保存」改为抽样后重新比对(抽查用)。",
|
||||
unchangedSampleHint: "成功共 {{total}} 条,明细抽样 {{listed}} 条(分层抽查)。要查任意路由请输入筛选条件——将按原表即时判定。通过率按全部 {{total}} 计。",
|
||||
liveSearchHint: "搜索:原表即时判定(页签只过滤种类)。失败/成功/新增均可查到。",
|
||||
liveSearchTruncatedHint: "搜索结果已截断,请收窄条件(如完整前缀 / neighbor)。",
|
||||
liveSearchHint: "按字段回查原表即时判定;页签只过滤种类。例:direction=out,network=1.1.1.1。",
|
||||
liveSearchTruncatedHint: "搜索结果已截断,请再收窄字段条件。",
|
||||
clearKeyFilters: "清空字段",
|
||||
storeUnchanged: "成功行保存",
|
||||
storeUnchangedAuto: "自动(小表全量 / 大表抽样 5000+原表补全)",
|
||||
storeUnchangedSample: "仅抽样(最多 5000,身份键+原表补全)",
|
||||
|
|
@ -544,7 +545,7 @@ const zh = {
|
|||
pickRun: "选择比对记录…",
|
||||
runCount: "{{n}} 次",
|
||||
resultEmpty: "无匹配失败/结果行",
|
||||
resultFilterPh: "搜索前缀 / neighbor / RD…(回查原表)",
|
||||
resultFilterPh: "自由词,或 direction:out network:1.1.1.1",
|
||||
kindAll: "全部",
|
||||
kindDiff: "失败",
|
||||
kindAdded: "新增",
|
||||
|
|
|
|||
|
|
@ -12504,6 +12504,36 @@ body:has(.bs-cmp-board:fullscreen) {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bs-cmp-key-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bs-cmp-key-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 110px;
|
||||
max-width: 160px;
|
||||
flex: 0 1 140px;
|
||||
}
|
||||
|
||||
.bs-cmp-key-filter > span {
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bs-cmp-key-filter .input,
|
||||
.bs-cmp-key-filter [data-slot="input"],
|
||||
.bs-cmp-key-filter input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-cmp-kind-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -843,6 +843,22 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const [kindFilter, setKindFilter] = useState<KindFilter>("diff");
|
||||
const [resultKw, setResultKw] = useState("");
|
||||
const debouncedResultKw = useDebouncedValue(resultKw, 300);
|
||||
const [resultKeyFilters, setResultKeyFilters] = useState<Record<string, string>>({});
|
||||
const debouncedKeyFiltersJson = useDebouncedValue(JSON.stringify(resultKeyFilters), 300);
|
||||
const debouncedKeyFilters = useMemo(() => {
|
||||
try {
|
||||
const o = JSON.parse(debouncedKeyFiltersJson || "{}") as Record<string, string>;
|
||||
const out: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(o || {})) {
|
||||
if (String(k || "").trim() && String(v || "").trim()) out[String(k)] = String(v).trim();
|
||||
}
|
||||
return out;
|
||||
} catch {
|
||||
return {} as Record<string, string>;
|
||||
}
|
||||
}, [debouncedKeyFiltersJson]);
|
||||
const hasResultSearch =
|
||||
Boolean(debouncedResultKw.trim()) || Object.keys(debouncedKeyFilters).length > 0;
|
||||
const [resultPage, setResultPage] = useState(1);
|
||||
const [resultPageSize, setResultPageSize] = useState(100);
|
||||
const [resultTotal, setResultTotal] = useState(0);
|
||||
|
|
@ -1058,10 +1074,22 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
[runSheets, resultSheetId],
|
||||
);
|
||||
|
||||
// Reset page when sheet / filter / page size changes
|
||||
// Clear field filters when switching sheet
|
||||
useEffect(() => {
|
||||
setResultKeyFilters({});
|
||||
}, [resultSheetId]);
|
||||
|
||||
// Reset page when sheet / filter / page size changes
|
||||
useEffect(() => {
|
||||
setResultPage(1);
|
||||
}, [resultSheetId, kindFilter, debouncedResultKw, resultPageSize, runDetail?.id]);
|
||||
}, [
|
||||
resultSheetId,
|
||||
kindFilter,
|
||||
debouncedResultKw,
|
||||
debouncedKeyFiltersJson,
|
||||
resultPageSize,
|
||||
runDetail?.id,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const runId = String(runDetail?.id || "");
|
||||
|
|
@ -1085,6 +1113,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
metricId: mid,
|
||||
kind: kindFilter,
|
||||
kw: debouncedResultKw.trim(),
|
||||
qf: debouncedKeyFilters,
|
||||
page: resultPage,
|
||||
pageSize: resultPageSize,
|
||||
});
|
||||
|
|
@ -1120,6 +1149,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
activeRunSheet?.status,
|
||||
kindFilter,
|
||||
debouncedResultKw,
|
||||
debouncedKeyFiltersJson,
|
||||
resultPage,
|
||||
resultPageSize,
|
||||
jobDetailTab,
|
||||
|
|
@ -1274,7 +1304,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
kindFilter === "added" ||
|
||||
kindFilter === "removed" ||
|
||||
kindFilter === "changed";
|
||||
const isLiveSearch = diffsSource === "live" && Boolean(debouncedResultKw.trim());
|
||||
const isLiveSearch = diffsSource === "live" && hasResultSearch;
|
||||
const resultSearchKeyFields = useMemo(() => {
|
||||
const keys = (activeRunSheet?.key_fields || []).map((f) => String(f || "").trim()).filter(Boolean);
|
||||
// Prefer BGP-ish fields first for compact UI
|
||||
const prefer = ["direction", "neighbor", "network", "rd", "afi", "vrf", "local_as"];
|
||||
const ranked = [
|
||||
...prefer.filter((p) => keys.includes(p)),
|
||||
...keys.filter((k) => !prefer.includes(k)),
|
||||
];
|
||||
return ranked.slice(0, 8);
|
||||
}, [activeRunSheet?.key_fields]);
|
||||
const resultEmptyColSpan =
|
||||
1 +
|
||||
(showFailCol ? 1 : 0) +
|
||||
|
|
@ -3565,6 +3605,45 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
{diffsLoading ? "…" : `${pagedDiffs.length}/${resultTotal}`}
|
||||
</span>
|
||||
</div>
|
||||
{resultSearchKeyFields.length ? (
|
||||
<div className="bs-cmp-key-filters" style={{ marginTop: 6 }}>
|
||||
{resultSearchKeyFields.map((f) => (
|
||||
<label key={f} className="bs-cmp-key-filter">
|
||||
<span className="muted">{f}</span>
|
||||
<Input
|
||||
value={resultKeyFilters[f] || ""}
|
||||
placeholder={
|
||||
f === "direction"
|
||||
? "in / out"
|
||||
: f === "network"
|
||||
? "1.1.1.1"
|
||||
: ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setResultKeyFilters((prev) => {
|
||||
const next = { ...prev };
|
||||
const v = e.target.value;
|
||||
if (!v.trim()) delete next[f];
|
||||
else next[f] = v;
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
{Object.keys(resultKeyFilters).some((k) =>
|
||||
String(resultKeyFilters[k] || "").trim(),
|
||||
) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="linkish"
|
||||
onClick={() => setResultKeyFilters({})}
|
||||
>
|
||||
{t("bizCompare.clearKeyFilters")}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{isLiveSearch ? (
|
||||
<p className="muted bm-hint" style={{ margin: "4px 0 0" }}>
|
||||
{diffsTruncated
|
||||
|
|
|
|||
|
|
@ -2170,6 +2170,8 @@ export const bizCompareListRunDiffs = (params: {
|
|||
metricId?: string;
|
||||
kind?: string;
|
||||
kw?: string;
|
||||
/** Field filters e.g. { direction: "out", network: "1.1.1.1" } */
|
||||
qf?: Record<string, string>;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}) => {
|
||||
|
|
@ -2177,6 +2179,15 @@ export const bizCompareListRunDiffs = (params: {
|
|||
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);
|
||||
if (params.qf && Object.keys(params.qf).length) {
|
||||
const cleaned: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(params.qf)) {
|
||||
const key = String(k || "").trim();
|
||||
const val = String(v || "").trim();
|
||||
if (key && val) cleaned[key] = val;
|
||||
}
|
||||
if (Object.keys(cleaned).length) p.set("qf", JSON.stringify(cleaned));
|
||||
}
|
||||
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<{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue