fix(ume): forward stats by rule_key, rule toggle, and list filters

Aggregate push counts per monitor rule, add enabled PATCH and paginated filtered list API/UI so keyword rules show correct stats without deleting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-06-23 14:36:13 +08:00
parent 300fd74565
commit 17270a26b6
10 changed files with 286 additions and 21 deletions

View file

@ -4,7 +4,14 @@ export const queryKeys = {
integrationsStatus: ["integrationsStatus"] as const,
umeTokenStatus: ["umeTokenStatus"] as const,
umeAlarmSubscription: ["umeAlarmSubscription"] as const,
umeKeyAlertMonitor: ["umeKeyAlertMonitor"] as const,
umeKeyAlertMonitorAll: ["umeKeyAlertMonitor"] as const,
umeKeyAlertMonitor: (
page: number,
pageSize: number,
keyword: string,
enabled: string,
matchType: string,
) => ["umeKeyAlertMonitor", page, pageSize, keyword, enabled, matchType] as const,
/** Prefix keys for invalidating all pages/variants of a domain. */
umeSyncStatusAll: ["umeSyncStatus"] as const,
umeNEAll: ["umeNE"] as const,

View file

@ -367,6 +367,14 @@ const en = {
colAttempts: "attempts",
colLast: "last push",
colActions: "actions",
colMonitor: "monitor",
monitorOn: "on",
monitorOff: "off",
filterKeywordPh: "Filter match / label",
filterMatchAll: "All types",
filterEnabledAll: "All states",
filterEnabledOn: "Monitoring on",
filterEnabledOff: "Monitoring off",
addOk: "Monitor rule added",
deleteOk: "Deleted",
confirmDelete: "Delete this monitor rule?",

View file

@ -365,6 +365,14 @@ const zh = {
colAttempts: "尝试次数",
colLast: "最近推送",
colActions: "操作",
colMonitor: "是否监控",
monitorOn: "开启",
monitorOff: "关闭",
filterKeywordPh: "筛选匹配内容 / 描述",
filterMatchAll: "全部类型",
filterEnabledAll: "全部状态",
filterEnabledOn: "监控中",
filterEnabledOff: "已暂停",
addOk: "已添加监控规则",
deleteOk: "已删除",
confirmDelete: "确定删除该监控规则?",

View file

@ -17,6 +17,7 @@ import {
refreshUmeToken,
upsertUmeKeyAlertRule,
deleteUmeKeyAlertRule,
patchUmeKeyAlertRule,
updateUmeKeyAlertMonitorConfig,
} from "../services/api";
import { HelpHint } from "../components/HelpHint";
@ -56,6 +57,11 @@ export function UmePage() {
const [keyAlertIdHints, setKeyAlertIdHints] = useState<
Array<{ notification_id: string; native_probable_cause_sample: string }>
>([]);
const [keyAlertPage, setKeyAlertPage] = useState(1);
const [keyAlertPageSize, setKeyAlertPageSize] = useState(20);
const [keyAlertFilterKeyword, setKeyAlertFilterKeyword] = useState("");
const [keyAlertFilterEnabled, setKeyAlertFilterEnabled] = useState<"" | "true" | "false">("");
const [keyAlertFilterMatchType, setKeyAlertFilterMatchType] = useState<"" | "notification_id" | "keyword">("");
const syncMutation = useMutation({
mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }),
@ -106,8 +112,21 @@ export function UmePage() {
refetchInterval: 5000,
});
const keyAlertMonitorQuery = useQuery({
queryKey: queryKeys.umeKeyAlertMonitor,
queryFn: fetchUmeKeyAlertMonitor,
queryKey: queryKeys.umeKeyAlertMonitor(
keyAlertPage,
keyAlertPageSize,
keyAlertFilterKeyword,
keyAlertFilterEnabled,
keyAlertFilterMatchType,
),
queryFn: () =>
fetchUmeKeyAlertMonitor({
page: keyAlertPage,
pageSize: keyAlertPageSize,
keyword: keyAlertFilterKeyword,
enabled: keyAlertFilterEnabled,
matchType: keyAlertFilterMatchType,
}),
staleTime: 3000,
refetchInterval: 5000,
});
@ -306,7 +325,7 @@ export function UmePage() {
setKeyAlertMatchValue("");
setKeyAlertLabel("");
showOk(t("ume.keyAlert.addOk"));
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor });
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll });
await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus });
},
onError: (err) => {
@ -321,7 +340,7 @@ export function UmePage() {
onMutate: () => setKeyAlertOpError(""),
onSuccess: async () => {
showOk(t("ume.keyAlert.deleteOk"));
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor });
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll });
await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus });
},
onError: (err) => {
@ -331,11 +350,25 @@ export function UmePage() {
},
});
const keyAlertToggleMutation = useMutation({
mutationFn: (vars: { ruleKey: string; enabled: boolean }) =>
patchUmeKeyAlertRule(vars.ruleKey, { enabled: vars.enabled }),
onMutate: () => setKeyAlertOpError(""),
onSuccess: async () => {
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(""),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor });
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll });
},
onError: (err) => {
const msg = String(err);
@ -353,6 +386,8 @@ export function UmePage() {
const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder;
const keyAlertRules = keyAlertMonitorQuery.data?.rules || [];
const keyAlertTotal = Number(keyAlertMonitorQuery.data?.total || keyAlertRules.length);
const keyAlertPages = pageCount(keyAlertTotal, keyAlertPageSize);
const keyAlertForwardOnClear = Boolean(keyAlertMonitorQuery.data?.config?.forward_on_clear);
const oclawWsPill =
!keyAlertForwarder?.enabled
@ -740,9 +775,43 @@ export function UmePage() {
{t("common.opFailed")}: {keyAlertOpError}
</div>
) : null}
<div className="actions-row actions-row--inline" style={{ marginTop: 12, flexWrap: "wrap", gap: 8 }}>
<input
type="search"
placeholder={t("ume.keyAlert.filterKeywordPh")}
value={keyAlertFilterKeyword}
onChange={(e) => {
setKeyAlertFilterKeyword(e.target.value);
setKeyAlertPage(1);
}}
/>
<select
value={keyAlertFilterMatchType}
onChange={(e) => {
setKeyAlertFilterMatchType(e.target.value as "" | "notification_id" | "keyword");
setKeyAlertPage(1);
}}
>
<option value="">{t("ume.keyAlert.filterMatchAll")}</option>
<option value="notification_id">{t("ume.keyAlert.matchNotificationId")}</option>
<option value="keyword">{t("ume.keyAlert.matchKeyword")}</option>
</select>
<select
value={keyAlertFilterEnabled}
onChange={(e) => {
setKeyAlertFilterEnabled(e.target.value as "" | "true" | "false");
setKeyAlertPage(1);
}}
>
<option value="">{t("ume.keyAlert.filterEnabledAll")}</option>
<option value="true">{t("ume.keyAlert.filterEnabledOn")}</option>
<option value="false">{t("ume.keyAlert.filterEnabledOff")}</option>
</select>
</div>
<table style={{ marginTop: 12 }}>
<thead>
<tr>
<th>{t("ume.keyAlert.colMonitor")}</th>
<th>{t("ume.keyAlert.colType")}</th>
<th>{t("ume.keyAlert.colMatch")}</th>
<th>{t("ume.keyAlert.colLabel")}</th>
@ -754,7 +823,23 @@ export function UmePage() {
</thead>
<tbody>
{keyAlertRules.map((rule) => (
<tr key={rule.notification_id}>
<tr key={rule.notification_id} className={rule.enabled ? undefined : "row--muted"}>
<td>
<label className="checkbox-inline">
<input
type="checkbox"
checked={Boolean(rule.enabled)}
disabled={keyAlertToggleMutation.isPending}
onChange={(e) =>
keyAlertToggleMutation.mutate({
ruleKey: rule.notification_id,
enabled: e.target.checked,
})
}
/>
{rule.enabled ? t("ume.keyAlert.monitorOn") : t("ume.keyAlert.monitorOff")}
</label>
</td>
<td>
{rule.match_type === "keyword"
? t("ume.keyAlert.matchKeyword")
@ -786,11 +871,44 @@ export function UmePage() {
))}
{!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? (
<tr>
<td colSpan={7}>{t("ume.keyAlert.emptyRules")}</td>
<td colSpan={8}>{t("ume.keyAlert.emptyRules")}</td>
</tr>
) : null}
</tbody>
</table>
<div className="pager">
<div className="pager__meta">
{t("common.pagerMeta", { total: keyAlertTotal, page: keyAlertPage, pages: keyAlertPages })}
</div>
<div className="pager__controls">
<button
className="pager__btn"
onClick={() => setKeyAlertPage(Math.max(1, keyAlertPage - 1))}
disabled={keyAlertPage <= 1}
>
{t("common.prevPage")}
</button>
<button
className="pager__btn"
onClick={() => setKeyAlertPage(keyAlertPage + 1)}
disabled={keyAlertPage >= keyAlertPages}
>
{t("common.nextPage")}
</button>
<select
className="pager__size"
value={String(keyAlertPageSize)}
onChange={(e) => {
setKeyAlertPageSize(Number(e.target.value) || 20);
setKeyAlertPage(1);
}}
>
<option value="10">10 / page</option>
<option value="20">20 / page</option>
<option value="50">50 / page</option>
</select>
</div>
</div>
</article>
<article className="card card--full">

View file

@ -80,7 +80,24 @@ export const cancelUmeAlarmSubscription = (opts?: { forceClearLocal?: boolean })
export const clearLocalUmeAlarmSubscription = () =>
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/clear-local", {});
export const fetchUmeKeyAlertMonitor = () => apiGet<UmeKeyAlertMonitorResponse>("/v1/ume/key-alert-monitor");
export const fetchUmeKeyAlertMonitor = (params?: {
page?: number;
pageSize?: number;
keyword?: string;
enabled?: "" | "true" | "false";
matchType?: "" | "notification_id" | "keyword";
}) => {
const p = new URLSearchParams();
p.set("page", String(Math.max(1, Number(params?.page || 1))));
p.set("page_size", String(Math.max(1, Math.min(200, Number(params?.pageSize || 50)))));
const kw = String(params?.keyword || "").trim();
if (kw) p.set("keyword", kw);
const en = String(params?.enabled || "").trim();
if (en) p.set("enabled", en);
const mt = String(params?.matchType || "").trim();
if (mt) p.set("match_type", mt);
return apiGet<UmeKeyAlertMonitorResponse>(`/v1/ume/key-alert-monitor?${p.toString()}`);
};
export const upsertUmeKeyAlertRule = (payload: {
match_type: "notification_id" | "keyword";
@ -95,6 +112,12 @@ 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 }) =>
apiPatch<{ ok: boolean; item?: unknown }>(
`/v1/ume/key-alert-rules/${encodeURIComponent(ruleKey)}`,
payload,
);
export const fetchUmeNotificationIds = (limit = 200) =>
apiGet<{ items: Array<{ notification_id: string; native_probable_cause_sample: string }> }>(
`/v1/ume/notification-ids?limit=${encodeURIComponent(String(limit))}`,

View file

@ -44,6 +44,9 @@ export type UmeKeyAlertForwarderStatus = {
export type UmeKeyAlertMonitorResponse = {
ok: boolean;
rules: UmeKeyAlertRuleItem[];
total?: number;
page?: number;
page_size?: number;
config?: {
forward_on_clear: boolean;
};