mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
feat(ume): real-time current alarms via WebSocket subscription
Add WS consumer, persisted subscription store, manual subscribe/cancel APIs, and UI controls; extend sync and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
21f7cfba41
commit
4f8735a83f
13 changed files with 1171 additions and 22 deletions
|
|
@ -5,6 +5,9 @@ import {
|
|||
disconnectUmeToken,
|
||||
fetchUmeCurrentAlarms,
|
||||
fetchUmeNe,
|
||||
cancelUmeAlarmSubscription,
|
||||
establishUmeAlarmSubscription,
|
||||
fetchUmeAlarmSubscriptionStatus,
|
||||
fetchUmeSyncStatus,
|
||||
fetchUmeTokenStatus,
|
||||
refreshUmeToken,
|
||||
|
|
@ -21,6 +24,7 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
|||
const queryClient = useQueryClient();
|
||||
const [tokenOpError, setTokenOpError] = useState("");
|
||||
const [runtimeTaskError, setRuntimeTaskError] = useState("");
|
||||
const [subscriptionOpError, setSubscriptionOpError] = useState("");
|
||||
const [syncPage, setSyncPage] = useState(1);
|
||||
const [syncPageSize, setSyncPageSize] = useState(20);
|
||||
const [neKeyword, setNeKeyword] = useState("");
|
||||
|
|
@ -79,6 +83,53 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
|||
toastError?.(msg);
|
||||
},
|
||||
});
|
||||
const subscriptionStatusQuery = useQuery({
|
||||
queryKey: ["umeAlarmSubscription"],
|
||||
queryFn: fetchUmeAlarmSubscriptionStatus,
|
||||
staleTime: 3000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const subscriptionEstablishMutation = useMutation({
|
||||
mutationFn: establishUmeAlarmSubscription,
|
||||
onMutate: () => setSubscriptionOpError(""),
|
||||
onSuccess: async (res) => {
|
||||
if (!res?.active) {
|
||||
const msg = "建立订阅未返回有效 id/uri";
|
||||
setSubscriptionOpError(msg);
|
||||
toastError?.(msg);
|
||||
} else {
|
||||
setSubscriptionOpError("");
|
||||
toastOk?.(
|
||||
res.already_exists
|
||||
? "订阅已存在,未重复建立(WSS 将保持/恢复连接)"
|
||||
: "告警订阅已建立,WSS 将自动连接",
|
||||
);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = String(err);
|
||||
setSubscriptionOpError(msg);
|
||||
toastError?.(msg);
|
||||
},
|
||||
});
|
||||
const subscriptionCancelMutation = useMutation({
|
||||
mutationFn: cancelUmeAlarmSubscription,
|
||||
onMutate: () => setSubscriptionOpError(""),
|
||||
onSuccess: async () => {
|
||||
setSubscriptionOpError("");
|
||||
toastOk?.("告警订阅已取消");
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = String(err);
|
||||
setSubscriptionOpError(msg);
|
||||
toastError?.(msg);
|
||||
},
|
||||
});
|
||||
|
||||
const tokenDisconnectMutation = useMutation({
|
||||
mutationFn: disconnectUmeToken,
|
||||
onMutate: () => {
|
||||
|
|
@ -135,6 +186,14 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
|||
});
|
||||
const runningTasks = (syncStatusQuery.data?.items || []).filter((x) => String(x.status || "").toLowerCase() === "running");
|
||||
const runtimeTasks = syncStatusQuery.data?.runtime_tasks || [];
|
||||
const alarmSub =
|
||||
subscriptionStatusQuery.data ??
|
||||
syncStatusQuery.data?.alarm_subscription ??
|
||||
({ active: false } as const);
|
||||
const wsConsumer = runtimeTasks.find((t) => t.task === "alarms_current_ws_consumer");
|
||||
const subscriptionActive = Boolean(alarmSub.active);
|
||||
const subPending =
|
||||
subscriptionEstablishMutation.isPending || subscriptionCancelMutation.isPending;
|
||||
|
||||
const runtimeTaskMutation = useMutation({
|
||||
mutationFn: async (vars: { task: string; action: "pause" | "resume" }) =>
|
||||
|
|
@ -228,6 +287,78 @@ export function UmePage({ toastOk, toastError }: UmePageProps) {
|
|||
</div>
|
||||
)}
|
||||
</article>
|
||||
<article className="card card--full">
|
||||
<h3>UME 告警订阅(WebSocket)</h3>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
订阅需手动建立/取消;建立后(或重启后若库中仍有有效订阅)后台会自动连接 WSS 接收实时告警。
|
||||
</p>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<span className={`conn-pill conn-pill--${subscriptionActive ? "up" : "down"}`}>
|
||||
订阅: {subscriptionActive ? "已建立" : "未建立"}
|
||||
</span>
|
||||
{subscriptionActive && alarmSub.subscription_id ? (
|
||||
<span className="conn-pill" title={alarmSub.wss_uri || ""}>
|
||||
id: {String(alarmSub.subscription_id).slice(0, 12)}…
|
||||
</span>
|
||||
) : null}
|
||||
{wsConsumer ? (
|
||||
<span
|
||||
className={`conn-pill conn-pill--${
|
||||
String(wsConsumer.last_error || "").includes("connected") ||
|
||||
wsConsumer.status === "running"
|
||||
? "up"
|
||||
: String(wsConsumer.last_error || "").includes("no_subscription")
|
||||
? "down"
|
||||
: "unknown"
|
||||
}`}
|
||||
title={wsConsumer.last_error || ""}
|
||||
>
|
||||
WSS: {wsConsumer.last_error || wsConsumer.status || "-"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => subscriptionEstablishMutation.mutate()}
|
||||
disabled={subPending || subscriptionActive || !hasToken}
|
||||
title={!hasToken ? "请先登录 UME token" : undefined}
|
||||
>
|
||||
{subscriptionEstablishMutation.isPending ? (
|
||||
<>
|
||||
<span className="inline-spinner" aria-hidden />
|
||||
建立订阅…
|
||||
</>
|
||||
) : (
|
||||
"建立告警订阅"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => subscriptionCancelMutation.mutate()}
|
||||
disabled={subPending || !subscriptionActive}
|
||||
>
|
||||
{subscriptionCancelMutation.isPending ? (
|
||||
<>
|
||||
<span className="inline-spinner" aria-hidden />
|
||||
取消订阅…
|
||||
</>
|
||||
) : (
|
||||
"取消告警订阅"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: ["umeAlarmSubscription"] })}
|
||||
disabled={subscriptionStatusQuery.isFetching}
|
||||
>
|
||||
刷新订阅状态
|
||||
</button>
|
||||
</div>
|
||||
{subscriptionOpError ? (
|
||||
<div className="pill pill--high">订阅操作失败: {subscriptionOpError}</div>
|
||||
) : null}
|
||||
</article>
|
||||
<article className="card card--full">
|
||||
<h3>UME 同步</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
ImportHistoryItem,
|
||||
IntegrationStatus,
|
||||
UmeAlarmItem,
|
||||
UmeAlarmSubscriptionStatus,
|
||||
UmeNeItem,
|
||||
UmeSyncStatusResponse,
|
||||
UmeTokenStatus,
|
||||
|
|
@ -100,6 +101,15 @@ export const fetchAlarms = (params: {
|
|||
return apiGet<AlarmQueryResponse>(`/v1/alarms?${p.toString()}`);
|
||||
};
|
||||
|
||||
export const fetchUmeAlarmSubscriptionStatus = () =>
|
||||
apiGet<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/status");
|
||||
|
||||
export const establishUmeAlarmSubscription = () =>
|
||||
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/establish", {});
|
||||
|
||||
export const cancelUmeAlarmSubscription = () =>
|
||||
apiPost<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/cancel", {});
|
||||
|
||||
export const fetchUmeSyncStatus = (params: { page: number; pageSize: number }) => {
|
||||
const p = new URLSearchParams();
|
||||
p.set("page", String(Math.max(1, Number(params.page || 1))));
|
||||
|
|
|
|||
|
|
@ -89,12 +89,26 @@ export type UmeSyncJobItem = {
|
|||
ended_at?: string | null;
|
||||
};
|
||||
|
||||
export type UmeAlarmSubscriptionStatus = {
|
||||
ok?: boolean;
|
||||
created?: boolean;
|
||||
already_exists?: boolean;
|
||||
active: boolean;
|
||||
subscription_id?: string;
|
||||
wss_uri?: string;
|
||||
topic?: string;
|
||||
ws_consumer_status?: string;
|
||||
ws_consumer_last_error?: string;
|
||||
ws_consumer_last_run_at?: string | null;
|
||||
};
|
||||
|
||||
export type UmeSyncStatusResponse = {
|
||||
total?: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
items: UmeSyncJobItem[];
|
||||
latest_by_domain?: Record<string, UmeSyncJobItem>;
|
||||
alarm_subscription?: UmeAlarmSubscriptionStatus;
|
||||
runtime_tasks?: Array<{
|
||||
task: string;
|
||||
status: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue