import { Button, Input, Modal } from "@heroui/react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { AppModalShell } from "../../components/ui/AppModalShell"; import { FieldSelect } from "../../components/ui/FieldSelect"; import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useToast } from "../../hooks/useToast"; import { useI18n } from "../../i18n"; import { bizCompareListMetrics, bizCompareListTemplates, bizMonitorCreateTemplate, bizMonitorDeleteTemplate, bizMonitorListTemplates, bizMonitorUpdateTemplate, formatErr, } from "../../services/api"; type MetricField = { name: string; display_name?: string }; type MetricSchema = { metric_id: string; fields: MetricField[] }; type CompareSheet = { sheet_id?: string; title?: string; metric_id: string; key_fields?: string[]; iface_fields?: string[]; compare_fields?: string[]; display_fields?: string[]; }; type CompareTpl = { id: string; name: string; metrics?: CompareSheet[] }; /** Leaf: presence (row existence) or value (field + strategy). */ type RuleCond = { type: "presence" | "value"; field?: string; op?: string; value?: string | string[]; }; /** OR-of-AND: each inner list is AND; outer list is OR. */ type CondGroup = RuleCond[]; type DualPat = { old_groups?: CondGroup[]; new_groups?: CondGroup[]; /** Legacy — migrated into groups on normalize. */ old?: string[]; new?: string[]; old_mode?: "any" | "all"; new_mode?: "any" | "all"; old_conds?: RuleCond[]; new_conds?: RuleCond[]; }; type SheetOverride = { metric_id: string; sheet_id?: string; status_fields?: string[]; down_values?: string[]; up_values?: string[]; success?: DualPat[]; anomaly?: DualPat[]; skip_dual?: boolean; field_tokens?: Array<{ side: string; field: string; in: string[] }>; }; type MonitorTpl = { id: string; name: string; compare_template_id: string; compare_template_name?: string; collect_metric_ids?: string[]; defaults?: Record; sheet_overrides?: SheetOverride[]; note?: string; }; const PRESENCE_TOKENS = [ { id: "removed", labelKey: "tokRemoved" }, { id: "added", labelKey: "tokAdded" }, { id: "unchanged", labelKey: "tokUnchanged" }, { id: "changed", labelKey: "tokChanged" }, ] as const; const VALUE_OPS = [ "eq", "ne", "in", "not_in", "empty", "not_empty", "changed", "unchanged", ] as const; const OPS_NEED_VALUE = new Set(["eq", "ne", "in", "not_in"]); function emptyCond(type: RuleCond["type"] = "presence", field = ""): RuleCond { if (type === "value") return { type: "value", field, op: "eq", value: "" }; return { type: "presence", value: "removed" }; } function emptyGroup(type: RuleCond["type"] = "presence", field = ""): CondGroup { return [emptyCond(type, field)]; } function migrateLegacyCond(raw: Record): RuleCond { const typ = String(raw.type || "").toLowerCase(); if (typ === "status") { // Legacy status class: bind a field later; empty field still matches the old class. return { type: "value", field: "", op: "eq", value: String(raw.value ?? raw.status ?? "") }; } if (typ === "field" || typ === "value") { return { type: "value", field: String(raw.field || ""), op: String(raw.op || "eq"), value: (raw.value as string | string[]) ?? "", }; } if (typ === "kind" || typ === "presence" || raw.kind != null) { return { type: "presence", value: String(raw.value ?? raw.kind ?? "removed"), }; } if (raw.field) { return { type: "value", field: String(raw.field), op: String(raw.op || "eq"), value: (raw.value as string | string[]) ?? "", }; } return { type: "presence", value: String(raw.value || "removed") }; } function tokenToCond(tok: string): RuleCond { if (PRESENCE_TOKENS.some((x) => x.id === tok)) return { type: "presence", value: tok }; if (tok === "up" || tok === "down" || tok === "other") { return { type: "value", field: "", op: "eq", value: tok }; } if (tok.startsWith("field:")) { const rest = tok.slice("field:".length); const i = rest.indexOf(":"); if (i > 0) { return { type: "value", field: rest.slice(0, i), op: "eq", value: rest.slice(i + 1) }; } } return { type: "presence", value: tok }; } function groupsFromFlat(conds: RuleCond[], mode: "any" | "all"): CondGroup[] { if (!conds.length) return []; if (mode === "all") return [conds]; return conds.map((c) => [c]); } function groupsFromLegacyTokens(tokens: string[] | undefined): CondGroup[] { return (tokens || []).map((tok) => [tokenToCond(tok)]); } function normalizeGroups( groups: CondGroup[] | undefined, flatConds: RuleCond[] | undefined, mode: "any" | "all" | undefined, legacyTokens: string[] | undefined, ): CondGroup[] { if (Array.isArray(groups)) { if (!groups.length) return []; return groups .map((g) => (g || []) .filter(Boolean) .map((c) => migrateLegacyCond(c as unknown as Record)), ) .filter((g) => g.length > 0); } if (flatConds?.length) { return groupsFromFlat( flatConds.map((c) => migrateLegacyCond(c as unknown as Record)), mode || "any", ); } return groupsFromLegacyTokens(legacyTokens); } function normalizePat(pat: DualPat, opts?: { allowEmptySide?: boolean }): DualPat { const allowEmpty = Boolean(opts?.allowEmptySide); const old_groups = normalizeGroups(pat.old_groups, pat.old_conds, pat.old_mode, pat.old); const new_groups = normalizeGroups(pat.new_groups, pat.new_conds, pat.new_mode, pat.new); if (allowEmpty) { return { old_groups, new_groups }; } return { old_groups: old_groups.length ? old_groups : [emptyGroup("presence")], new_groups: new_groups.length ? new_groups : [emptyGroup("presence")], }; } function successFromPresence(oldVals: string[], newVals: string[]): DualPat { return normalizePat({ old_groups: oldVals.map((v) => [{ type: "presence", value: v }]), new_groups: newVals.map((v) => [{ type: "presence", value: v }]), }); } function anomalyBothGone(): DualPat { return normalizePat( { old_groups: [[{ type: "presence", value: "removed" }]], new_groups: [[{ type: "presence", value: "removed" }]], }, { allowEmptySide: true }, ); } function anomalySideOnly( side: "old" | "new", groups: CondGroup[], ): DualPat { return normalizePat( { old_groups: side === "old" ? groups : [], new_groups: side === "new" ? groups : [], }, { allowEmptySide: true }, ); } function defaultAnomalyForState(field: string, downValues: string[]): DualPat[] { const downGroup: CondGroup = [{ type: "value", field, op: "in", value: downValues }]; return [ anomalyBothGone(), anomalySideOnly("old", [[{ type: "presence", value: "removed" }], downGroup]), anomalySideOnly("new", [[{ type: "presence", value: "removed" }], downGroup]), ]; } function defaultAnomalyPresenceOnly(): DualPat[] { return [ anomalyBothGone(), anomalySideOnly("old", [[{ type: "presence", value: "removed" }]]), anomalySideOnly("new", [[{ type: "presence", value: "removed" }]]), ]; } function emptyOverride(metricId: string, sheetId = ""): SheetOverride { return { metric_id: metricId, sheet_id: sheetId || metricId, status_fields: [], down_values: [], up_values: [], success: [successFromPresence(["removed"], ["added"])], anomaly: defaultAnomalyPresenceOnly(), skip_dual: false, }; } function sheetIdentity(s: { sheet_id?: string; metric_id?: string } | null | undefined): string { return String(s?.sheet_id || s?.metric_id || "").trim(); } function sheetLabel(s: { title?: string; sheet_id?: string; metric_id?: string } | null | undefined): string { return String(s?.title || s?.sheet_id || s?.metric_id || "").trim() || "—"; } export function presetForMetric(metricId: string, sheetId = ""): SheetOverride { const mid = metricId; const sid = sheetId || mid; const withId = (ov: SheetOverride): SheetOverride => ({ ...ov, metric_id: mid, sheet_id: sid, }); if (mid === "interface_brief") { const upConds: RuleCond[] = [ { type: "value", field: "admin", op: "in", value: ["up"] }, { type: "value", field: "phy", op: "in", value: ["up"] }, ]; return withId({ metric_id: mid, status_fields: ["admin", "phy", "prot"], down_values: ["down"], up_values: ["up"], success: [ normalizePat({ old_groups: [ [{ type: "presence", value: "removed" }], [ { type: "value", field: "admin", op: "in", value: ["down"] }, { type: "value", field: "phy", op: "in", value: ["down"] }, ], ], new_groups: [ [{ type: "presence", value: "added" }, ...upConds], [{ type: "presence", value: "unchanged" }, ...upConds], upConds, ], }), ], anomaly: defaultAnomalyForState("admin", ["down"]), skip_dual: false, }); } if (mid === "bgp_peer" || mid.startsWith("bgp_peer")) { const upConds: RuleCond[] = [{ type: "value", field: "state", op: "eq", value: "established" }]; return withId({ metric_id: mid, status_fields: ["state"], down_values: ["idle", "active", "connect", "down"], up_values: ["established"], success: [ normalizePat({ old_groups: [ [{ type: "presence", value: "removed" }], [ { type: "value", field: "state", op: "in", value: ["idle", "active", "connect", "down"], }, ], ], new_groups: [ [{ type: "presence", value: "added" }, ...upConds], [{ type: "presence", value: "unchanged" }, ...upConds], upConds, ], }), ], anomaly: defaultAnomalyForState("state", ["idle", "active", "connect", "down"]), skip_dual: false, }); } if (mid === "arp" || mid === "nd6_cache" || mid === "lldp_neighbor") { return withId({ metric_id: mid, status_fields: [], down_values: [], up_values: [], success: [successFromPresence(["removed"], ["added"])], anomaly: defaultAnomalyPresenceOnly(), skip_dual: false, }); } if (mid.includes("isis") || mid.includes("ospf") || mid.includes("adjacency")) { const upConds: RuleCond[] = [ { type: "value", field: "state", op: "in", value: ["up", "full", "2way"] }, ]; return withId({ metric_id: mid, status_fields: ["state"], down_values: ["down", "init", "idle"], up_values: ["up", "full", "2way"], success: [ normalizePat({ old_groups: [ [{ type: "presence", value: "removed" }], [ { type: "value", field: "state", op: "in", value: ["down", "init", "idle"], }, ], ], new_groups: [ [{ type: "presence", value: "added" }, ...upConds], [{ type: "presence", value: "unchanged" }, ...upConds], upConds, ], }), ], anomaly: defaultAnomalyForState("state", ["down", "init", "idle"]), skip_dual: false, }); } if (mid.includes("route") || mid.includes("vrf")) { return withId({ metric_id: mid, status_fields: [], down_values: [], up_values: [], success: [successFromPresence(["removed"], ["added"])], anomaly: defaultAnomalyPresenceOnly(), skip_dual: false, }); } return emptyOverride(mid, sid); } function skipPreset(metricId: string, sheetId = ""): SheetOverride { return { ...emptyOverride(metricId, sheetId), skip_dual: true, success: [], anomaly: [] }; } function overrideFor(overrides: SheetOverride[], metricId: string, sheetId = ""): SheetOverride { const sid = sheetId || metricId; const found = overrides.find((o) => (o.sheet_id || o.metric_id) === sid) || overrides.find((o) => !o.sheet_id && o.metric_id === metricId); const merged = found ? { ...emptyOverride(metricId, sid), ...found, metric_id: metricId, sheet_id: sid } : emptyOverride(metricId, sid); return { ...merged, success: (merged.success || []).map((p) => normalizePat(p)), anomaly: (merged.anomaly || []).map((p) => normalizePat(p, { allowEmptySide: true })), }; } function csvValues(raw: string): string[] { return raw .split(/[,;\s]+/) .map((x) => x.trim().toLowerCase()) .filter(Boolean); } function Chip({ active, label, onClick, }: { active: boolean; label: string; onClick: () => void; }) { return ( ); } function patchGroupCond( groups: CondGroup[], gi: number, ci: number, patch: Partial | RuleCond, ): CondGroup[] { return groups.map((g, i) => i === gi ? g.map((x, j) => (j === ci ? { ...x, ...patch } : x)) : g, ); } function SideGroupsEditor({ allowEmpty, groups, fieldChoices, t, onChange, }: { allowEmpty: boolean; groups: CondGroup[]; fieldChoices: MetricField[]; t: (k: string, vars?: Record) => string; onChange: (next: CondGroup[]) => void; }) { const dontCare = allowEmpty && groups.length === 0; const setGroups = (next: CondGroup[]) => { if (!next.length && !allowEmpty) onChange([emptyGroup()]); else onChange(next); }; return (
{allowEmpty ? ( ) : null} {dontCare ? (

{t("bizMonitorTpl.sideDontCareHint")}

) : ( <> {groups.map((group, gi) => (
{gi > 0 ?
{t("bizMonitorTpl.or")}
: null}
{t("bizMonitorTpl.groupN", { n: String(gi + 1) })}
{group.map((cond, ci) => (
{ci > 0 ? (
{t("bizMonitorTpl.and")}
) : null}
{cond.type === "presence" ? ( ) : ( <> { const raw = e.target.value; const val = cond.op === "in" || cond.op === "not_in" ? csvValues(raw) : raw; setGroups(patchGroupCond(groups, gi, ci, { value: val })); }} /> )}
))}
))} )}
); } function DualPatternList({ kind, patterns, fieldChoices, t, onChange, }: { kind: "success" | "anomaly"; patterns: DualPat[]; fieldChoices: MetricField[]; t: (k: string, vars?: Record) => string; onChange: (next: DualPat[]) => void; }) { const allowEmpty = kind === "anomaly"; const cardClass = kind === "anomaly" ? "mt-rule-card mt-rule-card--anomaly" : "mt-rule-card"; const rowKey = kind === "anomaly" ? "anomalyRow" : "successRow"; const addKey = kind === "anomaly" ? "addAnomalyRow" : "addSuccessRow"; const hintKey = kind === "anomaly" ? "anomalyPatternsHintShort" : "successPatternsHintShort"; const setPat = (pi: number, nextPat: DualPat) => { const list = [...patterns]; list[pi] = normalizePat(nextPat, { allowEmptySide: allowEmpty }); onChange(list); }; const updateSideGroups = (pi: number, side: "old" | "new", groups: CondGroup[]) => { const pat = normalizePat(patterns[pi] || {}, { allowEmptySide: allowEmpty }); const nextGroups = groups.length > 0 ? groups : allowEmpty ? [] : [emptyGroup()]; if (side === "old") setPat(pi, { ...pat, old_groups: nextGroups }); else setPat(pi, { ...pat, new_groups: nextGroups }); }; const defaultNewPat = (): DualPat => kind === "anomaly" ? anomalyBothGone() : successFromPresence(["removed"], ["added"]); return (

{t(`bizMonitorTpl.${hintKey}`)}

{patterns.map((rawPat, pi) => { const pat = normalizePat(rawPat, { allowEmptySide: allowEmpty }); return (
{t(`bizMonitorTpl.${rowKey}`, { n: String(pi + 1) })}
{t("bizMonitorTpl.oldSideAny")}
updateSideGroups(pi, "old", g)} />
→
{t("bizMonitorTpl.newSideAny")}
updateSideGroups(pi, "new", g)} />
); })}
); } export function BizMonitorTemplatesPage() { const { t } = useI18n(); const { showOk, showError } = useToast(); const [items, setItems] = useState([]); const [compareTpls, setCompareTpls] = useState([]); const [metricSchemas, setMetricSchemas] = useState([]); const [busy, setBusy] = useState(false); const [listKw, setListKw] = useState(""); const debouncedKw = useDebouncedValue(listKw, 250); const [editOpen, setEditOpen] = useState(false); const [editId, setEditId] = useState(""); const [name, setName] = useState(""); const [compareId, setCompareId] = useState(""); const [note, setNote] = useState(""); const [outOfExpect, setOutOfExpect] = useState("strict"); const [collectIds, setCollectIds] = useState([]); const [overrides, setOverrides] = useState([]); const [activeSheetIdx, setActiveSheetIdx] = useState(0); const [ruleTab, setRuleTab] = useState<"success" | "anomaly">("success"); const [showMore, setShowMore] = useState(false); const [showAdvancedJson, setShowAdvancedJson] = useState(false); const [overridesText, setOverridesText] = useState("[]"); const refresh = useCallback(async () => { const [mon, cmp, metrics] = await Promise.all([ bizMonitorListTemplates(), bizCompareListTemplates(), bizCompareListMetrics(), ]); setItems((mon.items || []) as MonitorTpl[]); setCompareTpls( ((cmp.items || []) as Record[]).map((x) => ({ id: String(x.id || ""), name: String(x.name || x.id || ""), metrics: Array.isArray(x.metrics) ? (x.metrics as CompareSheet[]) : [], })), ); setMetricSchemas( ((metrics.items || []) as MetricSchema[]).map((m) => ({ metric_id: m.metric_id, fields: (m.fields || []).map((f) => ({ name: f.name, display_name: f.display_name, })), })), ); }, []); useEffect(() => { void (async () => { try { await refresh(); } catch (e) { showError(formatErr(e)); } })(); }, [refresh, showError]); const filtered = useMemo(() => { const kw = debouncedKw.trim().toLowerCase(); if (!kw) return items; return items.filter((row) => { const blob = `${row.name} ${row.compare_template_name || ""} ${row.note || ""} ${row.compare_template_id}`.toLowerCase(); return blob.includes(kw); }); }, [items, debouncedKw]); const selectedCompare = useMemo( () => compareTpls.find((c) => c.id === compareId) || null, [compareTpls, compareId], ); const sheets = useMemo(() => selectedCompare?.metrics || [], [selectedCompare]); const activeSheet = sheets[activeSheetIdx] || sheets[0] || null; const activeMetricId = activeSheet?.metric_id || ""; const activeSheetId = activeSheet ? sheetIdentity(activeSheet) : ""; const activeOverride = useMemo( () => activeMetricId ? overrideFor(overrides, activeMetricId, activeSheetId) : emptyOverride(""), [overrides, activeMetricId, activeSheetId], ); const activeFields = useMemo(() => { const schema = metricSchemas.find((m) => m.metric_id === activeMetricId); return schema?.fields || []; }, [metricSchemas, activeMetricId]); const sheetKey = useMemo(() => sheets.map((s) => sheetIdentity(s)).join("|"), [sheets]); useEffect(() => { if (!sheets.length) { setActiveSheetIdx(0); return; } if (activeSheetIdx >= sheets.length) setActiveSheetIdx(0); }, [sheets, activeSheetIdx]); useEffect(() => { if (!sheetKey) return; setOverrides((prev) => { const byId = new Map(prev.map((o) => [o.sheet_id || o.metric_id, o])); return sheets.map( (s) => byId.get(sheetIdentity(s)) || emptyOverride(s.metric_id, sheetIdentity(s)), ); }); }, [compareId, sheetKey]); const syncOverride = (metricId: string, patch: Partial, sheetId = "") => { const sid = sheetId || sheetIdentity({ metric_id: metricId, sheet_id: sheetId }); setOverrides((prev) => { const others = prev.filter((o) => (o.sheet_id || o.metric_id) !== sid); const base = overrideFor(prev, metricId, sid); return [...others, { ...base, ...patch, metric_id: metricId, sheet_id: sid }]; }); }; const openCreate = () => { setEditId(""); setName(""); setCompareId(compareTpls[0]?.id || ""); setNote(""); setOutOfExpect("strict"); setCollectIds([]); setOverrides([]); setActiveSheetIdx(0); setRuleTab("success"); setShowMore(false); setShowAdvancedJson(false); setOverridesText("[]"); setEditOpen(true); }; const openEdit = (row: MonitorTpl) => { setEditId(row.id); setName(row.name || ""); setCompareId(row.compare_template_id || ""); setNote(row.note || ""); setOutOfExpect(String((row.defaults || {}).out_of_expect || "strict")); setCollectIds(Array.isArray(row.collect_metric_ids) ? [...row.collect_metric_ids] : []); setOverrides(Array.isArray(row.sheet_overrides) ? (row.sheet_overrides as SheetOverride[]) : []); setActiveSheetIdx(0); setRuleTab("success"); setShowMore(false); setShowAdvancedJson(false); setOverridesText(JSON.stringify(row.sheet_overrides || [], null, 2)); setEditOpen(true); }; const closeEdit = () => setEditOpen(false); const applyPreset = (kind: "auto" | "skip") => { if (!activeMetricId || !activeSheetId) return; syncOverride( activeMetricId, kind === "skip" ? skipPreset(activeMetricId, activeSheetId) : presetForMetric(activeMetricId, activeSheetId), activeSheetId, ); }; const applyAllPresets = () => { setOverrides(sheets.map((s) => presetForMetric(s.metric_id, sheetIdentity(s)))); }; const save = async () => { if (!name.trim()) { showError(t("bizMonitorTpl.needName")); return; } let sheet_overrides = overrides .filter((o) => o.metric_id) .map((o) => ({ ...o, success: (o.success || []).map((p) => normalizePat(p)), anomaly: (o.anomaly || []).map((p) => normalizePat(p, { allowEmptySide: true })), })); if (showAdvancedJson) { try { const parsed = JSON.parse(overridesText || "[]") as unknown[]; if (!Array.isArray(parsed)) throw new Error("overrides"); sheet_overrides = (parsed as SheetOverride[]).map((o) => ({ ...o, success: (o.success || []).map((p) => normalizePat(p)), anomaly: (o.anomaly || []).map((p) => normalizePat(p, { allowEmptySide: true })), })); } catch { showError(t("bizMonitorTpl.overridesInvalid")); return; } } const body = { name: name.trim(), compare_template_id: compareId, collect_metric_ids: collectIds, defaults: { dual_mode: "migrate_pair", out_of_expect: outOfExpect || "strict" }, sheet_overrides, note: note.trim(), }; setBusy(true); try { if (editId) { await bizMonitorUpdateTemplate(editId, body); showOk(t("bizMonitorTpl.saved")); } else { await bizMonitorCreateTemplate(body); showOk(t("bizMonitorTpl.created")); } closeEdit(); await refresh(); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const remove = async (id: string) => { if (!window.confirm(t("bizMonitorTpl.confirmDelete"))) return; setBusy(true); try { await bizMonitorDeleteTemplate(id); showOk(t("bizMonitorTpl.deleted")); await refresh(); } catch (e) { showError(formatErr(e)); } finally { setBusy(false); } }; const fieldChoices: MetricField[] = activeFields.length > 0 ? activeFields : [...(activeSheet?.compare_fields || []), ...(activeSheet?.key_fields || [])].map((n) => ({ name: n, })); return (

{t("bizMonitorTpl.title")}

{t("bizMonitorTpl.hint")}

{t("bizMonitorTpl.openCompare")}

{t("bizMonitorTpl.kpiTotal")}
{items.length}
setListKw(e.target.value)} />
{filtered.map((row) => ( ))} {!filtered.length ? ( ) : null}
{t("bizMonitorTpl.colName")} {t("bizMonitorTpl.colCompare")} {t("bizMonitorTpl.colCollect")} {t("bizMonitorTpl.colNote")} {t("bizMonitorTpl.colActions")}
{row.name}
{row.compare_template_name || row.compare_template_id || "—"} {(row.collect_metric_ids || []).length ? (row.collect_metric_ids || []).join(", ") : t("bizMonitorTpl.collectAll")} {row.note || "—"}
{t("bizMonitorTpl.empty")}
{editId ? t("bizMonitorTpl.edit") : t("bizMonitorTpl.create")} {/* Basics */}
setName(e.target.value)} placeholder={t("bizMonitorTpl.namePh")} aria-label={t("bizMonitorTpl.colName")} /> { setCompareId(e.target.value); setActiveSheetIdx(0); }} fullWidth > {compareTpls.map((c) => ( ))} setOutOfExpect(e.target.value)} fullWidth >
{!sheets.length ? (

{t("bizMonitorTpl.needCompareFirst")}

) : (
{/* Left sheet nav */} {/* Right rule editor */} {activeSheet ? (
{sheetLabel(activeSheet)}
{activeMetricId} {t("bizMonitorTpl.compare")}{" "} {(activeSheet.compare_fields || []).length ? (activeSheet.compare_fields || []).join(" · ") : t("bizMonitorTpl.presenceOnly")} {t("bizMonitorTpl.editInCompare")}
{!activeOverride.skip_dual ? ( <>
{ruleTab === "success" ? ( syncOverride(activeMetricId, { success }, activeSheetId) } /> ) : ( syncOverride(activeMetricId, { anomaly }, activeSheetId) } /> )} ) : (

{t("bizMonitorTpl.skipDualHint")}

)}
) : null}
)}
{showMore ? (
setNote(e.target.value)} placeholder={t("bizMonitorTpl.colNote")} aria-label={t("bizMonitorTpl.colNote")} />
{t("bizMonitorTpl.collectTitle")}

{t("bizMonitorTpl.collectHint")}

{Array.from(new Set(sheets.map((s) => s.metric_id).filter(Boolean))).map((mid) => { const checked = collectIds.length === 0 || collectIds.includes(mid); const explicit = collectIds.length > 0; const allMids = Array.from( new Set(sheets.map((s) => s.metric_id).filter(Boolean)), ); return ( { if (!explicit) { setCollectIds(allMids.filter((id) => id !== mid)); return; } setCollectIds((prev) => { if (prev.includes(mid)) { const next = prev.filter((x) => x !== mid); return next.length === allMids.length ? [] : next; } const next = [...prev, mid]; return next.length === allMids.length ? [] : next; }); }} /> ); })} {collectIds.length ? ( ) : null}
{showAdvancedJson ? (