From 34db62c8d2a9282fecfd311ca3da8f1c70a802b2 Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 22 Jun 2026 16:48:01 +0800 Subject: [PATCH] feat(ume): keyword key-alert rules with label and case-insensitive match Add description keyword matching alongside notificationId rules, require per-rule labels, and improve the AI monitor form layout and clear-on-push UX. Co-authored-by: Cursor --- netx_api/key_alert_matcher.py | 121 ++++++++++++++++---- netx_api/main.py | 107 ++++++++++++++---- netx_api/models.py | 4 +- tests/test_key_alert_matcher.py | 63 ++++++++++- web/src/components/HelpHint.tsx | 8 +- web/src/i18n/en.ts | 34 +++--- web/src/i18n/zh.ts | 28 +++-- web/src/index.css | 6 + web/src/pages/UmePage.tsx | 194 ++++++++++++++++++++++---------- web/src/services/api.ts | 16 ++- web/src/types.ts | 2 + 11 files changed, 449 insertions(+), 134 deletions(-) diff --git a/netx_api/key_alert_matcher.py b/netx_api/key_alert_matcher.py index 130cd25..9bcb120 100644 --- a/netx_api/key_alert_matcher.py +++ b/netx_api/key_alert_matcher.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import threading import time from typing import Any @@ -7,13 +8,72 @@ from typing import Any from sqlalchemy.orm import Session from .models import UmeKeyAlertRule -from .ume_sync_service import _is_alarm_cleared, notification_id_from_norm +from .ume_sync_service import _is_alarm_cleared, _pick, _s, notification_id_from_norm _RULE_CACHE_LOCK = threading.Lock() -_RULE_CACHE: dict[str, UmeKeyAlertRule] = {} +_RULE_CACHE: list[UmeKeyAlertRule] = [] _RULE_CACHE_LOADED_AT = 0.0 _RULE_CACHE_TTL_S = 30.0 +_ITEM_SEP_RE = re.compile(r"[,,;;|\n]+") + + +def _fold(text: str) -> str: + """Case-insensitive compare key for keyword matching.""" + return str(text or "").strip().casefold() + + +def parse_rule_items(text: str) -> list[str]: + """Split batch rule input (comma/semicolon/newline separated).""" + raw = str(text or "").strip() + if not raw: + return [] + parts = [p.strip() for p in _ITEM_SEP_RE.split(raw) if p.strip()] + seen: set[str] = set() + out: list[str] = [] + for part in parts: + key = _fold(part) + if key in seen: + continue + seen.add(key) + out.append(part) + return out + + +def normalize_match_type(value: str) -> str: + mt = str(value or "").strip().lower() + if mt in {"keyword", "keywords", "desc", "description", "cause"}: + return "keyword" + return "notification_id" + + +def rule_storage_key(*, match_type: str, value: str) -> str: + mt = normalize_match_type(match_type) + v = str(value or "").strip() + if not v: + raise ValueError("match value is required") + if mt == "keyword": + return f"kw:{_fold(v)[:120]}" + return v[:128] + + +def rule_match_value(row: UmeKeyAlertRule) -> str: + mv = str(getattr(row, "match_value", "") or "").strip() + if mv: + return mv + pk = str(row.notification_id or "").strip() + if pk.startswith("kw:"): + return pk[3:] + return pk + + +def rule_match_type(row: UmeKeyAlertRule) -> str: + mt = str(getattr(row, "match_type", "") or "").strip().lower() + if mt in {"keyword", "notification_id"}: + return mt + pk = str(row.notification_id or "").strip() + return "keyword" if pk.startswith("kw:") else "notification_id" + def invalidate_key_alert_rule_cache() -> None: global _RULE_CACHE_LOADED_AT @@ -22,23 +82,48 @@ def invalidate_key_alert_rule_cache() -> None: _RULE_CACHE_LOADED_AT = 0.0 -def _load_enabled_rules(db: Session) -> dict[str, UmeKeyAlertRule]: +def _load_enabled_rules(db: Session) -> list[UmeKeyAlertRule]: global _RULE_CACHE_LOADED_AT now = time.time() with _RULE_CACHE_LOCK: if _RULE_CACHE and (now - _RULE_CACHE_LOADED_AT) < _RULE_CACHE_TTL_S: - return dict(_RULE_CACHE) + return list(_RULE_CACHE) rows = ( db.query(UmeKeyAlertRule) .filter(UmeKeyAlertRule.enabled == 1) .all() ) - loaded = {str(row.notification_id or "").strip(): row for row in rows if str(row.notification_id or "").strip()} with _RULE_CACHE_LOCK: _RULE_CACHE.clear() - _RULE_CACHE.update(loaded) + _RULE_CACHE.extend(rows) _RULE_CACHE_LOADED_AT = now - return dict(loaded) + return list(rows) + + +def _alarm_search_text(norm: dict[str, Any]) -> str: + parts = [ + notification_id_from_norm(norm), + _s(_pick(norm, "nativeProbableCause", "native-probable-cause")), + _s(_pick(norm, "objectName", "object-name")), + _s(_pick(norm, "eventType", "event-type")), + ] + return " ".join(p for p in parts if p).casefold() + + +def _keyword_matches(norm: dict[str, Any], keyword: str) -> bool: + kw = _fold(keyword) + if not kw: + return False + return kw in _alarm_search_text(norm) + + +def _rule_matches_norm(rule: UmeKeyAlertRule, norm: dict[str, Any]) -> bool: + mt = rule_match_type(rule) + mv = rule_match_value(rule) + if mt == "keyword": + return _keyword_matches(norm, mv) + nid = notification_id_from_norm(norm) + return bool(nid) and nid == mv def match_key_alert_rule( @@ -47,19 +132,15 @@ def match_key_alert_rule( norm: dict[str, Any], action: str, ) -> UmeKeyAlertRule | None: - notification_id = notification_id_from_norm(norm) - if not notification_id: - return None - rules = _load_enabled_rules(db) - rule = rules.get(notification_id) - if rule is None: - return None act = str(action or "").strip().lower() - if act in {"inserted", "updated"}: - if _is_alarm_cleared(norm): - return None - return rule - if act == "deleted": - if int(getattr(rule, "forward_on_clear", 0) or 0) == 1: + for rule in _load_enabled_rules(db): + if not _rule_matches_norm(rule, norm): + continue + if act in {"inserted", "updated"}: + if _is_alarm_cleared(norm): + continue return rule + if act == "deleted": + if int(getattr(rule, "forward_on_clear", 0) or 0) == 1: + return rule return None diff --git a/netx_api/main.py b/netx_api/main.py index 20d9b8c..a18a14e 100644 --- a/netx_api/main.py +++ b/netx_api/main.py @@ -60,7 +60,13 @@ from .ume_alarm_ws import ( start_ume_alarm_ws_consumer, ) from .ume_sync_service import sync_alarms_current, sync_alarms_history_full, sync_inventory_full -from .key_alert_matcher import invalidate_key_alert_rule_cache +from .key_alert_matcher import ( + invalidate_key_alert_rule_cache, + normalize_match_type, + rule_match_type, + rule_match_value, + rule_storage_key, +) from .oclaw_alarm_forwarder import forwarder_status, shutdown_oclaw_alarm_forwarder, start_oclaw_alarm_forwarder from .ume_token_store import ( clear_shared_token, @@ -771,6 +777,20 @@ def on_startup() -> None: conn.exec_driver_sql( "CREATE INDEX IF NOT EXISTS ix_ume_alarms_history_notification_id ON ume_alarms_history (notification_id)" ) + conn.exec_driver_sql( + "ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS match_type VARCHAR(32) DEFAULT 'notification_id'" + ) + conn.exec_driver_sql( + "ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS match_value VARCHAR(256) DEFAULT ''" + ) + conn.exec_driver_sql( + "UPDATE ume_key_alert_rule SET match_value = notification_id " + "WHERE (match_value IS NULL OR match_value = '') AND notification_id NOT LIKE 'kw:%'" + ) + conn.exec_driver_sql( + "UPDATE ume_key_alert_rule SET match_type = 'keyword', match_value = SUBSTRING(notification_id FROM 4) " + "WHERE notification_id LIKE 'kw:%' AND (match_type IS NULL OR match_type = '' OR match_type = 'notification_id')" + ) conn.exec_driver_sql( "CREATE INDEX IF NOT EXISTS ix_ume_alarms_current_host_name ON ume_alarms_current (host_name)" ) @@ -1227,6 +1247,8 @@ def ume_list_key_alert_rules(db: Session = Depends(get_db)) -> dict[str, Any]: items = [ { "notification_id": str(row.notification_id or ""), + "match_type": rule_match_type(row), + "match_value": rule_match_value(row), "enabled": bool(int(row.enabled or 0)), "forward_on_clear": bool(int(row.forward_on_clear or 0)), "label": str(row.label or ""), @@ -1256,39 +1278,82 @@ def ume_key_alert_monitor(db: Session = Depends(get_db)) -> dict[str, Any]: @app.post("/v1/ume/key-alert-rules") def ume_upsert_key_alert_rule(payload: dict[str, Any], db: Session = Depends(get_db)) -> dict[str, Any]: - notification_id = str(payload.get("notification_id") or "").strip() - if not notification_id: - raise HTTPException(status_code=400, detail="notification_id_required") + match_type = normalize_match_type(str(payload.get("match_type") or "notification_id")) + match_value = str(payload.get("match_value") or payload.get("notification_id") or "").strip() + if not match_value: + raise HTTPException(status_code=400, detail="match_value_required") + label = str(payload.get("label") or "").strip() + if not label: + raise HTTPException(status_code=400, detail="label_required") + forward_on_clear = 1 if bool(payload.get("forward_on_clear", False)) else 0 + enabled = 1 if bool(payload.get("enabled", True)) else 0 now = datetime.now(timezone.utc).replace(tzinfo=None) - row = db.get(UmeKeyAlertRule, notification_id) + try: + storage_key = rule_storage_key(match_type=match_type, value=match_value) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + row = db.get(UmeKeyAlertRule, storage_key) if row is None: - row = UmeKeyAlertRule(notification_id=notification_id, created_at=now, updated_at=now) + row = UmeKeyAlertRule(notification_id=storage_key, created_at=now, updated_at=now) db.add(row) - row.enabled = 1 if bool(payload.get("enabled", True)) else 0 - row.forward_on_clear = 1 if bool(payload.get("forward_on_clear", False)) else 0 - row.label = str(payload.get("label") or "").strip() + row.match_type = match_type + row.match_value = match_value + row.enabled = enabled + row.forward_on_clear = forward_on_clear + row.label = label row.updated_at = now + saved = { + "notification_id": storage_key, + "match_type": match_type, + "match_value": match_value, + "enabled": bool(enabled), + "forward_on_clear": bool(forward_on_clear), + "label": label, + } db.commit() invalidate_key_alert_rule_cache() - return { - "ok": True, - "notification_id": notification_id, - "enabled": bool(row.enabled), - "forward_on_clear": bool(row.forward_on_clear), - "label": row.label, - } + return {"ok": True, "item": saved} -@app.delete("/v1/ume/key-alert-rules/{notification_id}") -def ume_delete_key_alert_rule(notification_id: str, db: Session = Depends(get_db)) -> dict[str, Any]: - nid = str(notification_id or "").strip() - row = db.get(UmeKeyAlertRule, nid) +@app.delete("/v1/ume/key-alert-rules/{rule_key:path}") +def ume_delete_key_alert_rule(rule_key: str, db: Session = Depends(get_db)) -> dict[str, Any]: + key = str(rule_key or "").strip() + row = db.get(UmeKeyAlertRule, key) if row is None: raise HTTPException(status_code=404, detail="rule_not_found") db.delete(row) db.commit() invalidate_key_alert_rule_cache() - return {"ok": True, "deleted": nid} + return {"ok": True, "deleted": key} + + +@app.get("/v1/ume/alarm-keywords") +def ume_list_alarm_keywords( + limit: int = Query(default=200, ge=1, le=2000), + db: Session = Depends(get_db), +) -> dict[str, Any]: + from sqlalchemy import func + + rows = ( + db.query( + UmeAlarmCurrent.native_probable_cause, + func.count(UmeAlarmCurrent.alarm_key).label("cnt"), + ) + .filter(UmeAlarmCurrent.native_probable_cause != "") + .group_by(UmeAlarmCurrent.native_probable_cause) + .order_by(func.count(UmeAlarmCurrent.alarm_key).desc(), UmeAlarmCurrent.native_probable_cause.asc()) + .limit(limit) + .all() + ) + items = [ + { + "keyword": str(cause or ""), + "alarm_count": int(cnt or 0), + } + for cause, cnt in rows + if str(cause or "").strip() + ] + return {"items": items, "total": len(items)} @app.get("/v1/ume/notification-ids") diff --git a/netx_api/models.py b/netx_api/models.py index 3f38424..5309aad 100644 --- a/netx_api/models.py +++ b/netx_api/models.py @@ -202,11 +202,13 @@ class UmeAlarmHistory(Base): class UmeKeyAlertRule(Base): - """Key alert rule matched by UME notificationId.""" + """Key alert rule matched by UME notificationId or alarm description keyword.""" __tablename__ = "ume_key_alert_rule" notification_id: Mapped[str] = mapped_column(String(128), primary_key=True) + match_type: Mapped[str] = mapped_column(String(32), default="notification_id", index=True) + match_value: Mapped[str] = mapped_column(String(256), default="", index=True) 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="") diff --git a/tests/test_key_alert_matcher.py b/tests/test_key_alert_matcher.py index fae789f..61a0f52 100644 --- a/tests/test_key_alert_matcher.py +++ b/tests/test_key_alert_matcher.py @@ -7,7 +7,12 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from netx_api.db import Base -from netx_api.key_alert_matcher import invalidate_key_alert_rule_cache, match_key_alert_rule +from netx_api.key_alert_matcher import ( + invalidate_key_alert_rule_cache, + match_key_alert_rule, + parse_rule_items, + rule_storage_key, +) from netx_api.models import UmeKeyAlertRule from netx_api.ume_sync_service import apply_alarm_to_current, notification_id_from_norm @@ -70,6 +75,62 @@ class KeyAlertMatcherTests(unittest.TestCase): invalidate_key_alert_rule_cache() self.assertIsNotNone(match_key_alert_rule(self.db, norm=cleared, action="deleted")) + def test_parse_rule_items(self) -> None: + self.assertEqual(parse_rule_items("a, b;c\n d"), ["a", "b", "c", "d"]) + self.assertEqual(parse_rule_items("A, a"), ["A"]) + self.assertEqual(parse_rule_items(""), []) + + def test_rule_storage_key_keyword(self) -> None: + self.assertEqual(rule_storage_key(match_type="keyword", value="BGP Down"), "kw:bgp down") + + def test_keyword_match_insert(self) -> None: + pk = rule_storage_key(match_type="keyword", value="链路中断") + self.db.add( + UmeKeyAlertRule( + notification_id=pk, + match_type="keyword", + match_value="链路中断", + enabled=1, + forward_on_clear=0, + label="链路", + ) + ) + self.db.commit() + invalidate_key_alert_rule_cache() + norm = { + "notificationId": "NID-999", + "nativeProbableCause": "核心链路中断告警", + "is-cleared": False, + } + rule = match_key_alert_rule(self.db, norm=norm, action="inserted") + self.assertIsNotNone(rule) + assert rule is not None + self.assertEqual(rule.match_value, "链路中断") + + norm_miss = {"notificationId": "NID-1", "nativeProbableCause": "CPU high", "is-cleared": False} + self.assertIsNone(match_key_alert_rule(self.db, norm=norm_miss, action="inserted")) + + def test_keyword_match_case_insensitive(self) -> None: + pk = rule_storage_key(match_type="keyword", value="bgp down") + self.db.add( + UmeKeyAlertRule( + notification_id=pk, + match_type="keyword", + match_value="BGP DOWN", + enabled=1, + forward_on_clear=0, + label="BGP", + ) + ) + self.db.commit() + invalidate_key_alert_rule_cache() + for cause in ("BGP DOWN alarm", "bgp down on port", "Link BGP DOWN detected"): + norm = {"notificationId": "NID-x", "nativeProbableCause": cause, "is-cleared": False} + self.assertIsNotNone( + match_key_alert_rule(self.db, norm=norm, action="inserted"), + msg=f"expected match for cause={cause!r}", + ) + if __name__ == "__main__": unittest.main() diff --git a/web/src/components/HelpHint.tsx b/web/src/components/HelpHint.tsx index 211e328..9e9b203 100644 --- a/web/src/components/HelpHint.tsx +++ b/web/src/components/HelpHint.tsx @@ -4,9 +4,10 @@ type Props = { text: string; ariaLabel: string; align?: "start" | "end"; + nowrap?: boolean; }; -export function HelpHint({ text, ariaLabel, align = "start" }: Props) { +export function HelpHint({ text, ariaLabel, align = "start", nowrap = false }: Props) { const [open, setOpen] = useState(false); const rootRef = useRef(null); @@ -38,7 +39,10 @@ export function HelpHint({ text, ariaLabel, align = "start" }: Props) { ? {open ? ( -
+
{text}
) : null} diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index fecc1d2..350e24f 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: "Match UME key alarms by notificationId and push via OClaw WebSocket. Auth shares NETX_OCLAW_ANALYZE_TOKEN / OCLAW_OPS_AI_SHARED_TOKEN; set NETX_OCLAW_ALARM_WS_URL separately.", + help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.), pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.", ws: "OClaw WSS", wsConnected: "connected", wsDisconnected: "disconnected", @@ -345,25 +345,33 @@ const en = { publishedOk: "published ok", publishedFail: "published fail", queue: "queue", - notificationId: "notificationId", - notificationIdPh: "Enter or pick notificationId", - label: "label", - labelPh: "optional, e.g. BGP Down", - forwardOnClear: "forward clear events", - add: "Add monitor ID", + matchType: "Match by", + matchNotificationId: "notificationId", + matchKeyword: "description keyword", + matchValuePhNotificationId: "Enter notificationId", + matchValuePhKeyword: "Enter keyword (case-insensitive)", + label: "Rule description", + labelPh: "Required, e.g. BGP Down", + forwardOnClear: "Push on alarm clear", + forwardOnClearHelp: "By default only new/updated alarms are pushed. When enabled, a clear notification is also sent when UME reports the alarm cleared.", + add: "Add rules", adding: "Adding…", delete: "Delete", - pickFromAlarms: "Pick from current alarms", - emptyRules: "No notificationId rules yet", - colId: "notificationId", - colLabel: "label", + pickFromAlarms: "Load alarm hints", + emptyRules: "No monitor rules yet", + colType: "type", + colMatch: "match", + colLabel: "description", + colForwardClear: "on clear", colPublished: "published", colAttempts: "attempts", colLast: "last push", colActions: "actions", - addOk: "Rule added", + addOk: "Monitor rule added", deleteOk: "Deleted", - confirmDelete: "Delete this notificationId monitor rule?", + confirmDelete: "Delete this monitor rule?", + yes: "yes", + no: "no", }, sync: { title: "UME sync", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 1dd61fb..de34a3a 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 匹配 UME 关键告警,经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN / OCLAW_OPS_AI_SHARED_TOKEN 共用;另需配置 NETX_OCLAW_ALARM_WS_URL。", + help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段),经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。", ws: "OClaw WSS", wsConnected: "已连接", wsDisconnected: "未连接", @@ -343,25 +343,33 @@ const zh = { publishedOk: "发布成功", publishedFail: "发布失败", queue: "待发队列", - notificationId: "notificationId", - notificationIdPh: "输入或从下方选择 notificationId", + matchType: "匹配方式", + matchNotificationId: "告警码 (notificationId)", + matchKeyword: "告警描述关键字", + matchValuePhNotificationId: "输入 notificationId", + matchValuePhKeyword: "输入匹配关键字(不区分大小写)", label: "规则描述", - labelPh: "可选,如 BGP Down", - forwardOnClear: "转发清除告警", - add: "添加监控 ID", + labelPh: "必填,如 BGP Down", + forwardOnClear: "告警清除时也推送", + forwardOnClearHelp: "默认只在告警新增/更新时推送。勾选后,当 UME 上报告警已清除(从当前告警表删除)时,也会向 WhatsApp 发送一条清除通知。", + add: "添加规则", adding: "添加中…", delete: "删除", - pickFromAlarms: "从当前告警选择", - emptyRules: "尚未配置监控 notificationId", - colId: "notificationId", + pickFromAlarms: "加载当前告警参考", + emptyRules: "尚未配置监控规则", + colType: "类型", + colMatch: "匹配内容", colLabel: "描述", + colForwardClear: "清除推送", colPublished: "发布次数", colAttempts: "尝试次数", colLast: "最近推送", colActions: "操作", addOk: "已添加监控规则", deleteOk: "已删除", - confirmDelete: "确定删除该 notificationId 监控规则?", + confirmDelete: "确定删除该监控规则?", + yes: "是", + no: "否", }, sync: { title: "UME 同步", diff --git a/web/src/index.css b/web/src/index.css index 27af4c4..bffc9ba 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -708,6 +708,12 @@ pre { right: 0; } +.help-hint__popover--nowrap { + width: max-content; + max-width: min(90vw, 720px); + white-space: nowrap; +} + .panel__toolbar-end { margin-left: auto; display: flex; diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 5aed565..2fddcdd 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -9,6 +9,7 @@ import { clearLocalUmeAlarmSubscription, establishUmeAlarmSubscription, fetchUmeAlarmSubscriptionStatus, + fetchUmeAlarmKeywords, fetchUmeKeyAlertMonitor, fetchUmeNotificationIds, fetchUmeSyncStatus, @@ -46,10 +47,15 @@ export function UmePage() { const [curPage, setCurPage] = useState(1); const [curPageSize, setCurPageSize] = useState(50); const [alarmsPanelOpen, setAlarmsPanelOpen] = useState(false); - const [keyAlertNotificationId, setKeyAlertNotificationId] = useState(""); + const [keyAlertMatchType, setKeyAlertMatchType] = useState<"notification_id" | "keyword">("keyword"); + const [keyAlertMatchValue, setKeyAlertMatchValue] = useState(""); const [keyAlertLabel, setKeyAlertLabel] = useState(""); const [keyAlertForwardOnClear, setKeyAlertForwardOnClear] = useState(false); const [keyAlertOpError, setKeyAlertOpError] = useState(""); + const [keyAlertKeywordHints, setKeyAlertKeywordHints] = useState([]); + const [keyAlertIdHints, setKeyAlertIdHints] = useState< + Array<{ notification_id: string; native_probable_cause_sample: string }> + >([]); const syncMutation = useMutation({ mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }), @@ -105,12 +111,6 @@ export function UmePage() { staleTime: 3000, refetchInterval: 5000, }); - const notificationIdsQuery = useQuery({ - queryKey: ["umeNotificationIds"], - queryFn: () => fetchUmeNotificationIds(200), - staleTime: 10000, - enabled: false, - }); const confirmClearLocalSubscription = (hint?: string) => window.confirm(hint || t("ume.subscription.confirmClearDefault")); @@ -296,14 +296,15 @@ export function UmePage() { const keyAlertAddMutation = useMutation({ mutationFn: () => upsertUmeKeyAlertRule({ - notification_id: keyAlertNotificationId.trim(), + match_type: keyAlertMatchType, + match_value: keyAlertMatchValue.trim(), label: keyAlertLabel.trim(), enabled: true, forward_on_clear: keyAlertForwardOnClear, }), onMutate: () => setKeyAlertOpError(""), onSuccess: async () => { - setKeyAlertNotificationId(""); + setKeyAlertMatchValue(""); setKeyAlertLabel(""); showOk(t("ume.keyAlert.addOk")); await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor }); @@ -606,58 +607,121 @@ export function UmePage() { ) : null}
-
- setKeyAlertNotificationId(e.target.value)} - list="ume-notification-id-options" - /> - - {(notificationIdsQuery.data?.items || []).map((x) => ( - - ))} - - setKeyAlertLabel(e.target.value)} - /> -