From e474c0a431058ff21db891fbd1824f9cc575ea72 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 23 Jun 2026 20:44:33 +0800 Subject: [PATCH] feat(ume): ne_type filter, rule edit, and pager layout Match key alerts by optional inventory ne_types, add edit dialog to update device types on existing rules, and keep card pager controls on one row. Co-authored-by: Cursor --- netx_api/key_alert_matcher.py | 66 ++++++++++- netx_api/main.py | 47 +++++++- netx_api/models.py | 1 + tests/test_key_alert_matcher.py | 45 +++++++- web/src/constants/queryKeys.ts | 1 + web/src/i18n/en.ts | 13 ++- web/src/i18n/zh.ts | 13 ++- web/src/index.css | 8 +- web/src/pages/UmePage.tsx | 196 +++++++++++++++++++++++++++++--- web/src/services/api.ts | 11 +- web/src/types.ts | 6 + 11 files changed, 383 insertions(+), 24 deletions(-) diff --git a/netx_api/key_alert_matcher.py b/netx_api/key_alert_matcher.py index 63bfda9..5bc2871 100644 --- a/netx_api/key_alert_matcher.py +++ b/netx_api/key_alert_matcher.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import re import threading import time @@ -7,8 +8,8 @@ from typing import Any from sqlalchemy.orm import Session -from .models import UmeKeyAlertRule -from .ume_sync_service import _is_alarm_cleared, _pick, _s, notification_id_from_norm +from .models import UmeInventoryNE, UmeKeyAlertRule +from .ume_sync_service import _derive_ne_id_from_alarm, _is_alarm_cleared, _pick, _s, notification_id_from_norm _RULE_CACHE_LOCK = threading.Lock() _RULE_CACHE: list[UmeKeyAlertRule] = [] @@ -75,6 +76,65 @@ def rule_match_type(row: UmeKeyAlertRule) -> str: return "keyword" if pk.startswith("kw:") else "notification_id" +def serialize_rule_ne_types(ne_types: list[str]) -> str: + cleaned = parse_rule_items(",".join(str(x or "").strip() for x in (ne_types or []) if str(x or "").strip())) + return json.dumps(cleaned, ensure_ascii=False) + + +def rule_ne_types(row: UmeKeyAlertRule) -> list[str]: + raw = str(getattr(row, "ne_types", "") or "").strip() + if not raw or raw == "[]": + return [] + try: + parsed = json.loads(raw) + if isinstance(parsed, list): + return parse_rule_items(",".join(str(x or "").strip() for x in parsed if str(x or "").strip())) + except json.JSONDecodeError: + pass + return parse_rule_items(raw) + + +def parse_rule_ne_types_payload(raw: Any) -> list[str]: + if raw is None: + return [] + if isinstance(raw, list): + return parse_rule_items(",".join(str(x or "").strip() for x in raw if str(x or "").strip())) + if isinstance(raw, str): + text = raw.strip() + if not text: + return [] + if text.startswith("["): + try: + parsed = json.loads(text) + if isinstance(parsed, list): + return parse_rule_items(",".join(str(x or "").strip() for x in parsed if str(x or "").strip())) + except json.JSONDecodeError: + pass + return parse_rule_items(text) + return [] + + +def _alarm_ne_type(db: Session, norm: dict[str, Any]) -> str: + ne_id = _s(_derive_ne_id_from_alarm(norm)) + if not ne_id: + return "" + row = db.get(UmeInventoryNE, ne_id) + if row is None: + return "" + return str(row.ne_type or "").strip() + + +def _ne_type_matches_rule(db: Session, rule: UmeKeyAlertRule, norm: dict[str, Any]) -> bool: + allowed = rule_ne_types(rule) + if not allowed: + return True + ne_type = _alarm_ne_type(db, norm) + if not ne_type: + return False + allowed_fold = {_fold(x) for x in allowed} + return _fold(ne_type) in allowed_fold + + def invalidate_key_alert_rule_cache() -> None: global _RULE_CACHE_LOADED_AT with _RULE_CACHE_LOCK: @@ -136,6 +196,8 @@ def match_key_alert_rule( for rule in _load_enabled_rules(db): if not _rule_matches_norm(rule, norm): continue + if not _ne_type_matches_rule(db, rule, norm): + continue if act in {"inserted", "updated"}: if _is_alarm_cleared(norm): continue diff --git a/netx_api/main.py b/netx_api/main.py index a546e5c..6b29ead 100644 --- a/netx_api/main.py +++ b/netx_api/main.py @@ -68,9 +68,12 @@ from .key_alert_config import ( from .key_alert_matcher import ( invalidate_key_alert_rule_cache, normalize_match_type, + parse_rule_ne_types_payload, rule_match_type, rule_match_value, + rule_ne_types, rule_storage_key, + serialize_rule_ne_types, ) from .oclaw_alarm_forwarder import forwarder_status, shutdown_oclaw_alarm_forwarder, start_oclaw_alarm_forwarder from .ume_token_store import ( @@ -739,6 +742,10 @@ def _migrate_key_alert_rule_schema() -> None: "add forward_log rule_key", "ALTER TABLE ume_key_alert_forward_log ADD COLUMN IF NOT EXISTS rule_key VARCHAR(128) DEFAULT ''", ), + ( + "add rule ne_types", + "ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS ne_types TEXT DEFAULT '[]'", + ), ] for label, sql in steps: try: @@ -1335,6 +1342,7 @@ def ume_list_key_alert_rules( "match_value": rule_match_value(row), "enabled": bool(int(row.enabled or 0)), "label": str(row.label or ""), + "ne_types": rule_ne_types(row), "created_at": (_ensure_utc(row.created_at) or datetime.now(timezone.utc)).isoformat(), "updated_at": (_ensure_utc(row.updated_at) or datetime.now(timezone.utc)).isoformat(), "forward_stats": stat_map.get(str(row.notification_id or ""), { @@ -1395,6 +1403,7 @@ def ume_upsert_key_alert_rule(payload: dict[str, Any], db: Session = Depends(get if not label: raise HTTPException(status_code=400, detail="label_required") enabled = 1 if bool(payload.get("enabled", True)) else 0 + ne_types_list = parse_rule_ne_types_payload(payload.get("ne_types")) now = datetime.now(timezone.utc).replace(tzinfo=None) try: storage_key = rule_storage_key(match_type=match_type, value=match_value) @@ -1408,6 +1417,7 @@ def ume_upsert_key_alert_rule(payload: dict[str, Any], db: Session = Depends(get row.match_value = match_value row.enabled = enabled row.label = label + row.ne_types = serialize_rule_ne_types(ne_types_list) row.updated_at = now saved = { "notification_id": storage_key, @@ -1415,13 +1425,14 @@ def ume_upsert_key_alert_rule(payload: dict[str, Any], db: Session = Depends(get "match_value": match_value, "enabled": bool(enabled), "label": label, + "ne_types": ne_types_list, } try: db.commit() except Exception as exc: db.rollback() msg = str(exc).lower() - if "match_type" in msg or "match_value" in msg or "undefinedcolumn" in msg: + if "match_type" in msg or "match_value" in msg or "ne_types" in msg or "undefinedcolumn" in msg: raise HTTPException( status_code=503, detail="key_alert_schema_outdated: restart netx API to apply database migration", @@ -1439,10 +1450,15 @@ def ume_patch_key_alert_rule(rule_key: str, payload: dict[str, Any], db: Session row = db.get(UmeKeyAlertRule, key) if row is None: raise HTTPException(status_code=404, detail="rule_not_found") - if "enabled" not in payload: - raise HTTPException(status_code=400, detail="enabled_required") + has_enabled = "enabled" in payload + has_ne_types = "ne_types" in payload + if not has_enabled and not has_ne_types: + raise HTTPException(status_code=400, detail="patch_fields_required") now = datetime.now(timezone.utc).replace(tzinfo=None) - row.enabled = 1 if bool(payload.get("enabled")) else 0 + if has_enabled: + row.enabled = 1 if bool(payload.get("enabled")) else 0 + if has_ne_types: + row.ne_types = serialize_rule_ne_types(parse_rule_ne_types_payload(payload.get("ne_types"))) row.updated_at = now db.commit() invalidate_key_alert_rule_cache() @@ -1454,6 +1470,7 @@ def ume_patch_key_alert_rule(rule_key: str, payload: dict[str, Any], db: Session "match_value": rule_match_value(row), "enabled": bool(int(row.enabled or 0)), "label": str(row.label or ""), + "ne_types": rule_ne_types(row), }, } @@ -1700,6 +1717,28 @@ def ume_runtime_task_resume(task: str) -> dict[str, Any]: return {"ok": True, "task": tid, "runtime_tasks": _list_runtime_tasks()} +@app.get("/v1/ume/inventory/ne-types") +def ume_list_inventory_ne_types( + limit: int = Query(default=500, ge=1, le=2000), + db: Session = Depends(get_db), +) -> dict[str, Any]: + from sqlalchemy import func + + rows = ( + db.query( + UmeInventoryNE.ne_type, + func.count(UmeInventoryNE.ne_id).label("ne_count"), + ) + .filter(UmeInventoryNE.ne_type != "") + .group_by(UmeInventoryNE.ne_type) + .order_by(func.count(UmeInventoryNE.ne_id).desc(), UmeInventoryNE.ne_type.asc()) + .limit(limit) + .all() + ) + items = [{"ne_type": str(ne_type or ""), "ne_count": int(ne_count or 0)} for ne_type, ne_count in rows if str(ne_type or "").strip()] + return {"items": items, "total": len(items)} + + @app.get("/v1/ume/inventory/ne") def ume_list_ne( keyword: str | None = Query(default=None), diff --git a/netx_api/models.py b/netx_api/models.py index fd76a09..6649784 100644 --- a/netx_api/models.py +++ b/netx_api/models.py @@ -222,6 +222,7 @@ class UmeKeyAlertRule(Base): enabled: Mapped[int] = mapped_column(Integer, default=1) forward_on_clear: Mapped[int] = mapped_column(Integer, default=0) label: Mapped[str] = mapped_column(String(256), default="") + ne_types: Mapped[str] = mapped_column(Text, default="[]") created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True) diff --git a/tests/test_key_alert_matcher.py b/tests/test_key_alert_matcher.py index f395b22..4ea05f0 100644 --- a/tests/test_key_alert_matcher.py +++ b/tests/test_key_alert_matcher.py @@ -14,7 +14,7 @@ from netx_api.key_alert_matcher import ( parse_rule_items, rule_storage_key, ) -from netx_api.models import UmeKeyAlertRule +from netx_api.models import UmeInventoryNE, UmeKeyAlertRule from netx_api.ume_sync_service import apply_alarm_to_current, notification_id_from_norm @@ -128,6 +128,49 @@ class KeyAlertMatcherTests(unittest.TestCase): msg=f"expected match for cause={cause!r}", ) + def test_ne_type_filter(self) -> None: + from netx_api.key_alert_matcher import serialize_rule_ne_types + + self.db.add( + UmeInventoryNE( + ne_id="NE-PTN-1", + ne_name="PTN-A", + ne_type="PTN 9700", + ) + ) + self.db.add( + UmeInventoryNE( + ne_id="NE-ROUTER-1", + ne_name="Router-A", + ne_type="ZXR10 M6000", + ) + ) + self.db.add( + UmeKeyAlertRule( + notification_id="NID-NE-TYPE", + match_type="notification_id", + match_value="NID-NE-TYPE", + enabled=1, + label="PTN only", + ne_types=serialize_rule_ne_types(["PTN 9700"]), + ) + ) + self.db.commit() + invalidate_key_alert_rule_cache() + + ptn_norm = { + "notificationId": "NID-NE-TYPE", + "is-cleared": False, + "ne-id": "NE-PTN-1", + } + router_norm = { + "notificationId": "NID-NE-TYPE", + "is-cleared": False, + "ne-id": "NE-ROUTER-1", + } + self.assertIsNotNone(match_key_alert_rule(self.db, norm=ptn_norm, action="inserted")) + self.assertIsNone(match_key_alert_rule(self.db, norm=router_norm, action="inserted")) + if __name__ == "__main__": unittest.main() diff --git a/web/src/constants/queryKeys.ts b/web/src/constants/queryKeys.ts index 750326b..7d84e1d 100644 --- a/web/src/constants/queryKeys.ts +++ b/web/src/constants/queryKeys.ts @@ -5,6 +5,7 @@ export const queryKeys = { umeTokenStatus: ["umeTokenStatus"] as const, umeAlarmSubscription: ["umeAlarmSubscription"] as const, umeKeyAlertMonitorAll: ["umeKeyAlertMonitor"] as const, + umeInventoryNeTypes: ["umeInventoryNeTypes"] as const, umeKeyAlertMonitor: ( page: number, pageSize: number, diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6330070..089f83a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -337,7 +337,7 @@ const en = { }, keyAlert: { title: "AI alarm monitor (OClaw WhatsApp)", - help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.), pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.", + help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.", ws: "OClaw WSS", wsConnected: "connected", wsDisconnected: "disconnected", @@ -352,16 +352,27 @@ const en = { matchValuePhKeyword: "Enter keyword (case-insensitive)", label: "Rule description", labelPh: "Required, e.g. BGP Down", + neTypesLabel: "Device types (optional, multi-select)", + neTypesHint: "Leave empty to match all device types. Options come from synced UME Inventory ne_type.", + neTypesEmpty: "No device types yet — sync Inventory first", + neTypesAll: "All", forwardOnClear: "Push on alarm clear", forwardOnClearHelp: "Global switch for all monitor rules. By default only new/updated alarms are pushed; when enabled, clear notifications are also sent when UME reports alarms cleared.", add: "Add rules", adding: "Adding…", delete: "Delete", + edit: "Edit", + editTitle: "Edit monitor rule", + save: "Save", + saving: "Saving…", + cancel: "Cancel", + editOk: "Rule updated", pickFromAlarms: "Load alarm hints", emptyRules: "No monitor rules yet", colType: "type", colMatch: "match", colLabel: "description", + colNeTypes: "device types", colForwardClear: "on clear", colPublished: "published", colAttempts: "attempts", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 421f95e..b50df89 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -335,7 +335,7 @@ const zh = { }, keyAlert: { title: "AI 告警监控(OClaw WhatsApp)", - help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段),经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。", + help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。", ws: "OClaw WSS", wsConnected: "已连接", wsDisconnected: "未连接", @@ -350,16 +350,27 @@ const zh = { matchValuePhKeyword: "输入匹配关键字(不区分大小写)", label: "规则描述", labelPh: "必填,如 BGP Down", + neTypesLabel: "设备类型(可选,多选)", + neTypesHint: "不选表示全部设备类型;选项来自 UME Inventory 同步后的 ne_type。", + neTypesEmpty: "暂无设备类型,请先同步 Inventory", + neTypesAll: "全部", forwardOnClear: "告警清除时也推送", forwardOnClearHelp: "全局开关:对所有监控规则生效。默认只在告警新增/更新时推送;勾选后,UME 上报告警已清除时也会向 WhatsApp 发送清除通知。", add: "添加规则", adding: "添加中…", delete: "删除", + edit: "修改", + editTitle: "修改监控规则", + save: "保存", + saving: "保存中…", + cancel: "取消", + editOk: "规则已更新", pickFromAlarms: "加载当前告警参考", emptyRules: "尚未配置监控规则", colType: "类型", colMatch: "匹配内容", colLabel: "描述", + colNeTypes: "设备类型", colForwardClear: "清除推送", colPublished: "发布次数", colAttempts: "尝试次数", diff --git a/web/src/index.css b/web/src/index.css index aec801d..dcab946 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -362,6 +362,12 @@ button { margin-bottom: 8px; } +.pager input, +.pager select { + width: auto; + margin-bottom: 0; +} + .actions-row { margin-top: 8px; } @@ -864,7 +870,7 @@ pre { display: flex; align-items: center; gap: 6px; - flex-wrap: wrap; + flex-wrap: nowrap; } .pager__btn, diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index a2eb10e..d811796 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -10,6 +10,7 @@ import { establishUmeAlarmSubscription, fetchUmeAlarmSubscriptionStatus, fetchUmeAlarmKeywords, + fetchUmeInventoryNeTypes, fetchUmeKeyAlertMonitor, fetchUmeNotificationIds, fetchUmeSyncStatus, @@ -24,7 +25,7 @@ import { HelpHint } from "../components/HelpHint"; import { queryKeys } from "../constants/queryKeys"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; -import type { UmeAlarmSubscriptionStatus } from "../types"; +import type { UmeAlarmSubscriptionStatus, UmeKeyAlertRuleItem } from "../types"; import { pageCount, runtimeIntervalLabel } from "../utils/display"; import { formatSystemTime } from "../utils/time"; @@ -62,6 +63,9 @@ export function UmePage() { const [keyAlertFilterKeyword, setKeyAlertFilterKeyword] = useState(""); const [keyAlertFilterEnabled, setKeyAlertFilterEnabled] = useState<"" | "true" | "false">(""); const [keyAlertFilterMatchType, setKeyAlertFilterMatchType] = useState<"" | "notification_id" | "keyword">(""); + const [keyAlertNeTypes, setKeyAlertNeTypes] = useState([]); + const [keyAlertEditRule, setKeyAlertEditRule] = useState(null); + const [keyAlertEditNeTypes, setKeyAlertEditNeTypes] = useState([]); const syncMutation = useMutation({ mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }), @@ -69,6 +73,7 @@ export function UmePage() { await queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll }); await queryClient.invalidateQueries({ queryKey: queryKeys.umeNEAll }); await queryClient.invalidateQueries({ queryKey: queryKeys.umeCurrentAlarmsAll }); + await queryClient.invalidateQueries({ queryKey: queryKeys.umeInventoryNeTypes }); }, }); @@ -130,6 +135,11 @@ export function UmePage() { staleTime: 3000, refetchInterval: 5000, }); + const keyAlertNeTypesQuery = useQuery({ + queryKey: queryKeys.umeInventoryNeTypes, + queryFn: () => fetchUmeInventoryNeTypes(), + staleTime: 60_000, + }); const confirmClearLocalSubscription = (hint?: string) => window.confirm(hint || t("ume.subscription.confirmClearDefault")); @@ -319,11 +329,13 @@ export function UmePage() { match_value: keyAlertMatchValue.trim(), label: keyAlertLabel.trim(), enabled: true, + ne_types: keyAlertNeTypes, }), onMutate: () => setKeyAlertOpError(""), onSuccess: async () => { setKeyAlertMatchValue(""); setKeyAlertLabel(""); + setKeyAlertNeTypes([]); showOk(t("ume.keyAlert.addOk")); await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll }); await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus }); @@ -364,6 +376,22 @@ export function UmePage() { }, }); + const keyAlertEditMutation = useMutation({ + mutationFn: (vars: { ruleKey: string; ne_types: string[] }) => + patchUmeKeyAlertRule(vars.ruleKey, { ne_types: vars.ne_types }), + onMutate: () => setKeyAlertOpError(""), + onSuccess: async () => { + setKeyAlertEditRule(null); + showOk(t("ume.keyAlert.editOk")); + await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll }); + }, + onError: (err) => { + const msg = String(err); + setKeyAlertOpError(msg); + showError(msg); + }, + }); + const keyAlertConfigMutation = useMutation({ mutationFn: (forwardOnClear: boolean) => updateUmeKeyAlertMonitorConfig({ forward_on_clear: forwardOnClear }), onMutate: () => setKeyAlertOpError(""), @@ -741,6 +769,53 @@ export function UmePage() { {keyAlertAddMutation.isPending ? t("ume.keyAlert.adding") : t("ume.keyAlert.add")} +
+
+ {t("ume.keyAlert.neTypesLabel")} +
+ {keyAlertNeTypesQuery.isLoading ? ( + {t("common.refreshing")} + ) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? ( + {t("ume.keyAlert.neTypesEmpty")} + ) : ( +
+ {(keyAlertNeTypesQuery.data?.items || []).map((item) => ( + + ))} +
+ )} +
+ {t("ume.keyAlert.neTypesHint")} +
+
{t("ume.keyAlert.colType")} {t("ume.keyAlert.colMatch")} {t("ume.keyAlert.colLabel")} + {t("ume.keyAlert.colNeTypes")} {t("ume.keyAlert.colPublished")} {t("ume.keyAlert.colAttempts")} {t("ume.keyAlert.colLast")} @@ -847,6 +923,11 @@ export function UmePage() { {rule.match_value || rule.notification_id} {rule.label || t("common.empty")} + + {rule.ne_types?.length + ? rule.ne_types.join(", ") + : t("ume.keyAlert.neTypesAll")} + {Number(rule.forward_stats?.published_ok || 0)} {Number(rule.forward_stats?.attempts || 0)} @@ -855,23 +936,35 @@ export function UmePage() { : t("common.empty")} - +
+ + +
))} {!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? ( - {t("ume.keyAlert.emptyRules")} + {t("ume.keyAlert.emptyRules")} ) : null} @@ -1324,6 +1417,83 @@ export function UmePage() { ) : null} + + {keyAlertEditRule ? ( +
setKeyAlertEditRule(null)}> +
e.stopPropagation()}> +

{t("ume.keyAlert.editTitle")}

+

+ {keyAlertEditRule.match_type === "keyword" + ? t("ume.keyAlert.matchKeyword") + : t("ume.keyAlert.matchNotificationId")} + : {keyAlertEditRule.match_value || keyAlertEditRule.notification_id} + {keyAlertEditRule.label ? ` · ${keyAlertEditRule.label}` : ""} +

+
+ {t("ume.keyAlert.neTypesLabel")} +
+ {keyAlertNeTypesQuery.isLoading ? ( + {t("common.refreshing")} + ) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? ( + {t("ume.keyAlert.neTypesEmpty")} + ) : ( +
+ {(keyAlertNeTypesQuery.data?.items || []).map((item) => ( + + ))} +
+ )} +
+ {t("ume.keyAlert.neTypesHint")} +
+
+ + +
+
+
+ ) : null} ); } diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 2ca7889..3335a19 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -104,6 +104,7 @@ export const upsertUmeKeyAlertRule = (payload: { match_value: string; label: string; enabled?: boolean; + ne_types?: string[]; }) => apiPost<{ ok: boolean; item?: unknown }>("/v1/ume/key-alert-rules", payload); export const updateUmeKeyAlertMonitorConfig = (payload: { forward_on_clear: boolean }) => @@ -112,7 +113,10 @@ export const updateUmeKeyAlertMonitorConfig = (payload: { forward_on_clear: bool export const deleteUmeKeyAlertRule = (ruleKey: string) => apiDelete<{ ok: boolean }>(`/v1/ume/key-alert-rules/${encodeURIComponent(ruleKey)}`); -export const patchUmeKeyAlertRule = (ruleKey: string, payload: { enabled: boolean }) => +export const patchUmeKeyAlertRule = ( + ruleKey: string, + payload: { enabled?: boolean; ne_types?: string[] }, +) => apiPatch<{ ok: boolean; item?: unknown }>( `/v1/ume/key-alert-rules/${encodeURIComponent(ruleKey)}`, payload, @@ -128,6 +132,11 @@ export const fetchUmeAlarmKeywords = (limit = 200) => `/v1/ume/alarm-keywords?limit=${encodeURIComponent(String(limit))}`, ); +export const fetchUmeInventoryNeTypes = (limit = 500) => + apiGet<{ items: Array<{ ne_type: string; ne_count: number }>; total: number }>( + `/v1/ume/inventory/ne-types?limit=${encodeURIComponent(String(limit))}`, + ); + export const fetchUmeSyncStatus = (params: { page: number; pageSize: number }) => { const p = new URLSearchParams(); p.set("page", String(Math.max(1, Number(params.page || 1)))); diff --git a/web/src/types.ts b/web/src/types.ts index 93a61b3..3a6b5b1 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -22,6 +22,7 @@ export type UmeKeyAlertRuleItem = { match_value: string; enabled: boolean; label: string; + ne_types?: string[]; created_at: string; updated_at: string; forward_stats?: { @@ -31,6 +32,11 @@ export type UmeKeyAlertRuleItem = { }; }; +export type UmeInventoryNeTypeItem = { + ne_type: string; + ne_count: number; +}; + export type UmeKeyAlertForwarderStatus = { enabled: boolean; connected: boolean;