mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
feat(UME): 增加RESTCONF对接与可视化运维页
- 支持 token 获取/续约/断开、DB共享缓存与跨进程单飞锁、keepalive 保活 - 新增 inventory/current/history 告警同步入库与查询接口,前端增加 UME 对接页面与分页 - 修复 stop_netx.ps1 在部分 PowerShell 版本下 Stop-Job -Force 报错 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ab1b76dd47
commit
7f2e4ec393
15 changed files with 2577 additions and 15 deletions
|
|
@ -5,8 +5,8 @@ import { AppLayout } from "./layout/AppLayout";
|
|||
import { AlarmsPage } from "./pages/AlarmsPage";
|
||||
import { DiagnosticsPage } from "./pages/DiagnosticsPage";
|
||||
import { AiAnalysisPage } from "./pages/AiAnalysisPage";
|
||||
import { JobsPage } from "./pages/JobsPage";
|
||||
import { IngestPage } from "./pages/IngestPage";
|
||||
import { UmePage } from "./pages/UmePage";
|
||||
import type { Alarm, Batch, Diagnostics, ImportHistoryItem } from "./types";
|
||||
import { apiDelete, apiPost, apiPostForm, fetchAlarms, fetchBatches, fetchDiagnostics, fetchIntegrationStatus } from "./services/api";
|
||||
import { fetchJobs } from "./services/api";
|
||||
|
|
@ -17,7 +17,6 @@ function App() {
|
|||
alarms: (import.meta.env.VITE_FEATURE_ALARMS ?? "1") !== "0",
|
||||
diagnostics: (import.meta.env.VITE_FEATURE_DIAGNOSTICS ?? "1") !== "0",
|
||||
aiAnalysis: (import.meta.env.VITE_FEATURE_AI_ANALYSIS ?? "1") !== "0",
|
||||
jobs: (import.meta.env.VITE_FEATURE_JOBS ?? "0") === "1",
|
||||
};
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
|
@ -336,7 +335,7 @@ function App() {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/jobs" element={<JobsPage />} />
|
||||
<Route path="/ume" element={<UmePage />} />
|
||||
</Routes>
|
||||
{toast && <div className={`toast toast--${toast.type}`}>{toast.text}</div>}
|
||||
</AppLayout>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ type Props = {
|
|||
alarms: boolean;
|
||||
diagnostics: boolean;
|
||||
aiAnalysis: boolean;
|
||||
jobs: boolean;
|
||||
};
|
||||
children: ReactNode;
|
||||
};
|
||||
|
|
@ -52,11 +51,8 @@ export function AppLayout({ status, connections, onRefreshBatches, onRefreshAlar
|
|||
>
|
||||
AI 分析
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/jobs"
|
||||
className={({ isActive }) => `nav-item${isActive ? " active" : ""}${!capabilities.jobs ? " disabled" : ""}`}
|
||||
>
|
||||
作业中心
|
||||
<NavLink to="/ume" className={({ isActive }) => `nav-item${isActive ? " active" : ""}`}>
|
||||
UME 对接
|
||||
</NavLink>
|
||||
</aside>
|
||||
|
||||
|
|
|
|||
486
web/src/pages/UmePage.tsx
Normal file
486
web/src/pages/UmePage.tsx
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
apiPost,
|
||||
disconnectUmeToken,
|
||||
fetchUmeCurrentAlarms,
|
||||
fetchUmeHistoryAlarms,
|
||||
fetchUmeNe,
|
||||
fetchUmeSyncStatus,
|
||||
fetchUmeTokenStatus,
|
||||
refreshUmeToken,
|
||||
} from "../services/api";
|
||||
import { formatSystemTime } from "../utils/time";
|
||||
|
||||
function toIsoNoMs(d: Date): string {
|
||||
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||
}
|
||||
|
||||
export function UmePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [syncPage, setSyncPage] = useState(1);
|
||||
const [syncPageSize, setSyncPageSize] = useState(20);
|
||||
const [neKeyword, setNeKeyword] = useState("");
|
||||
const [nePage, setNePage] = useState(1);
|
||||
const [nePageSize, setNePageSize] = useState(50);
|
||||
|
||||
const [curSeverity, setCurSeverity] = useState("");
|
||||
const [curCleared, setCurCleared] = useState("");
|
||||
const [curNeId, setCurNeId] = useState("");
|
||||
const [curKeyword, setCurKeyword] = useState("");
|
||||
const [curPage, setCurPage] = useState(1);
|
||||
const [curPageSize, setCurPageSize] = useState(50);
|
||||
|
||||
const [hisSeverity, setHisSeverity] = useState("");
|
||||
const [hisNeId, setHisNeId] = useState("");
|
||||
const [hisKeyword, setHisKeyword] = useState("");
|
||||
const [hisTimeFrom, setHisTimeFrom] = useState("");
|
||||
const [hisTimeTo, setHisTimeTo] = useState("");
|
||||
const [hisPage, setHisPage] = useState(1);
|
||||
const [hisPageSize, setHisPageSize] = useState(50);
|
||||
|
||||
const applyQuickRange = (hours: number) => {
|
||||
const now = new Date();
|
||||
const from = new Date(now.getTime() - hours * 3600 * 1000);
|
||||
setHisTimeFrom(toIsoNoMs(from));
|
||||
setHisTimeTo(toIsoNoMs(now));
|
||||
setHisPage(1);
|
||||
};
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeNE"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeCurrentAlarms"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeHistoryAlarms"] });
|
||||
},
|
||||
});
|
||||
|
||||
const syncStatusQuery = useQuery({
|
||||
queryKey: ["umeSyncStatus", syncPage, syncPageSize],
|
||||
queryFn: () => fetchUmeSyncStatus({ page: syncPage, pageSize: syncPageSize }),
|
||||
staleTime: 5000,
|
||||
});
|
||||
const tokenStatusQuery = useQuery({
|
||||
queryKey: ["umeTokenStatus"],
|
||||
queryFn: fetchUmeTokenStatus,
|
||||
staleTime: 3000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const tokenRefreshMutation = useMutation({
|
||||
mutationFn: refreshUmeToken,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
||||
},
|
||||
});
|
||||
const tokenDisconnectMutation = useMutation({
|
||||
mutationFn: disconnectUmeToken,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] });
|
||||
},
|
||||
});
|
||||
|
||||
const tokenExpiresIn = Number(tokenStatusQuery.data?.expires_in_s || 0);
|
||||
const tokenLevel = !tokenStatusQuery.data?.has_token
|
||||
? "down"
|
||||
: tokenExpiresIn < 15
|
||||
? "down"
|
||||
: tokenExpiresIn < 60
|
||||
? "unknown"
|
||||
: "up";
|
||||
const neQuery = useQuery({
|
||||
queryKey: ["umeNE", neKeyword, nePage, nePageSize],
|
||||
queryFn: () => fetchUmeNe({ keyword: neKeyword, page: nePage, pageSize: nePageSize }),
|
||||
staleTime: 5000,
|
||||
});
|
||||
const currentQuery = useQuery({
|
||||
queryKey: ["umeCurrentAlarms", curSeverity, curCleared, curNeId, curKeyword, curPage, curPageSize],
|
||||
queryFn: () =>
|
||||
fetchUmeCurrentAlarms({
|
||||
severity: curSeverity,
|
||||
isCleared: curCleared,
|
||||
neId: curNeId,
|
||||
keyword: curKeyword,
|
||||
page: curPage,
|
||||
pageSize: curPageSize,
|
||||
}),
|
||||
staleTime: 5000,
|
||||
});
|
||||
const historyQuery = useQuery({
|
||||
queryKey: ["umeHistoryAlarms", hisSeverity, hisNeId, hisKeyword, hisTimeFrom, hisTimeTo, hisPage, hisPageSize],
|
||||
queryFn: () =>
|
||||
fetchUmeHistoryAlarms({
|
||||
severity: hisSeverity,
|
||||
neId: hisNeId,
|
||||
keyword: hisKeyword,
|
||||
timeFrom: hisTimeFrom,
|
||||
timeTo: hisTimeTo,
|
||||
page: hisPage,
|
||||
pageSize: hisPageSize,
|
||||
}),
|
||||
staleTime: 5000,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="cards">
|
||||
<article className="card card--full">
|
||||
<h3>UME Token 状态</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<span className={`conn-pill conn-pill--${tokenStatusQuery.data?.has_token ? "up" : "down"}`}>
|
||||
token: {tokenStatusQuery.data?.has_token ? "connected" : "disconnected"}
|
||||
</span>
|
||||
<span className={`conn-pill conn-pill--${tokenLevel}`}>
|
||||
expires_in: {typeof tokenStatusQuery.data?.expires_in_s === "number" ? `${tokenStatusQuery.data.expires_in_s}s` : "-"}
|
||||
</span>
|
||||
{tokenStatusQuery.data?.token_preview ? <span className="conn-pill">preview: {tokenStatusQuery.data.token_preview}</span> : null}
|
||||
</div>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<button
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: ["umeTokenStatus"] })}
|
||||
disabled={tokenStatusQuery.isFetching}
|
||||
>
|
||||
刷新状态
|
||||
</button>
|
||||
<button onClick={() => tokenRefreshMutation.mutate()} disabled={tokenRefreshMutation.isPending}>
|
||||
手动续期/登录
|
||||
</button>
|
||||
<button onClick={() => tokenDisconnectMutation.mutate()} disabled={tokenDisconnectMutation.isPending}>
|
||||
断开 token
|
||||
</button>
|
||||
</div>
|
||||
{(tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
||||
<div className="pill pill--high">
|
||||
操作失败: {String(tokenRefreshMutation.error || tokenDisconnectMutation.error)}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
<article className="card card--full">
|
||||
<h3>UME 同步</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<button onClick={() => syncMutation.mutate(["inventory"])} disabled={syncMutation.isPending}>
|
||||
同步 Inventory
|
||||
</button>
|
||||
<button onClick={() => syncMutation.mutate(["alarms_current"])} disabled={syncMutation.isPending}>
|
||||
同步当前告警
|
||||
</button>
|
||||
<button onClick={() => syncMutation.mutate(["alarms_history"])} disabled={syncMutation.isPending}>
|
||||
同步历史告警
|
||||
</button>
|
||||
<button onClick={() => syncMutation.mutate(["inventory", "alarms_current", "alarms_history"])} disabled={syncMutation.isPending}>
|
||||
全量同步
|
||||
</button>
|
||||
</div>
|
||||
{syncMutation.error && <div className="pill pill--high">同步失败: {String(syncMutation.error)}</div>}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>同步状态</h2>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<button onClick={() => queryClient.invalidateQueries({ queryKey: ["umeSyncStatus"] })} disabled={syncStatusQuery.isFetching}>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>domain</th>
|
||||
<th>status</th>
|
||||
<th>pulled</th>
|
||||
<th>inserted</th>
|
||||
<th>updated</th>
|
||||
<th>started_at</th>
|
||||
<th>ended_at</th>
|
||||
<th>error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(syncStatusQuery.data?.items || []).map((x) => (
|
||||
<tr key={x.id}>
|
||||
<td>{x.id}</td>
|
||||
<td>{x.domain}</td>
|
||||
<td>{x.status}</td>
|
||||
<td>{x.pulled_count}</td>
|
||||
<td>{x.inserted_count}</td>
|
||||
<td>{x.updated_count}</td>
|
||||
<td>{formatSystemTime(x.started_at)}</td>
|
||||
<td>{x.ended_at ? formatSystemTime(x.ended_at) : "-"}</td>
|
||||
<td>{x.error_message || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!syncStatusQuery.isLoading && (syncStatusQuery.data?.items || []).length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={9}>暂无同步记录</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<div className="pager__meta">
|
||||
共 {syncStatusQuery.data?.total || 0} 条 · 第 {syncPage}/
|
||||
{Math.max(1, Math.ceil(Math.max(0, Number(syncStatusQuery.data?.total || 0)) / Math.max(1, syncPageSize)))} 页
|
||||
</div>
|
||||
<div className="pager__controls">
|
||||
<button className="pager__btn" onClick={() => setSyncPage(Math.max(1, syncPage - 1))} disabled={syncPage <= 1}>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
className="pager__btn"
|
||||
onClick={() => setSyncPage(syncPage + 1)}
|
||||
disabled={syncPage >= Math.max(1, Math.ceil(Math.max(0, Number(syncStatusQuery.data?.total || 0)) / Math.max(1, syncPageSize)))}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
<select
|
||||
className="pager__size"
|
||||
value={String(syncPageSize)}
|
||||
onChange={(e) => {
|
||||
setSyncPageSize(Number(e.target.value) || 20);
|
||||
setSyncPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="20">20/页</option>
|
||||
<option value="50">50/页</option>
|
||||
<option value="100">100/页</option>
|
||||
<option value="200">200/页</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>网元清单</h2>
|
||||
<div className="filter-inline">
|
||||
<input value={neKeyword} placeholder="keyword(ne_id/ne_name/user_label/ip)" onChange={(e) => setNeKeyword(e.target.value)} />
|
||||
<button onClick={() => queryClient.invalidateQueries({ queryKey: ["umeNE"] })}>查询</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ne_id</th>
|
||||
<th>user_label</th>
|
||||
<th>ne_name</th>
|
||||
<th>ip</th>
|
||||
<th>type</th>
|
||||
<th>last_seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(neQuery.data?.items || []).map((x) => (
|
||||
<tr key={x.ne_id}>
|
||||
<td>{x.ne_id}</td>
|
||||
<td>{x.user_label}</td>
|
||||
<td>{x.ne_name}</td>
|
||||
<td>{x.ip_address}</td>
|
||||
<td>{x.ne_type}</td>
|
||||
<td>{x.last_seen_at ? formatSystemTime(x.last_seen_at) : "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<div className="pager__meta">
|
||||
共 {neQuery.data?.total || 0} 条 · 第 {nePage}/
|
||||
{Math.max(1, Math.ceil(Math.max(0, Number(neQuery.data?.total || 0)) / Math.max(1, nePageSize)))} 页
|
||||
</div>
|
||||
<div className="pager__controls">
|
||||
<button className="pager__btn" onClick={() => setNePage(Math.max(1, nePage - 1))} disabled={nePage <= 1}>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
className="pager__btn"
|
||||
onClick={() => setNePage(nePage + 1)}
|
||||
disabled={nePage >= Math.max(1, Math.ceil(Math.max(0, Number(neQuery.data?.total || 0)) / Math.max(1, nePageSize)))}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
<select
|
||||
className="pager__size"
|
||||
value={String(nePageSize)}
|
||||
onChange={(e) => {
|
||||
setNePageSize(Number(e.target.value) || 50);
|
||||
setNePage(1);
|
||||
}}
|
||||
>
|
||||
<option value="20">20/页</option>
|
||||
<option value="50">50/页</option>
|
||||
<option value="100">100/页</option>
|
||||
<option value="200">200/页</option>
|
||||
<option value="500">500/页</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>当前告警</h2>
|
||||
<div className="filter-inline">
|
||||
<input value={curKeyword} placeholder="keyword" onChange={(e) => setCurKeyword(e.target.value)} />
|
||||
<input value={curNeId} placeholder="ne_id" onChange={(e) => setCurNeId(e.target.value)} />
|
||||
<select value={curSeverity} onChange={(e) => setCurSeverity(e.target.value)}>
|
||||
<option value="">全部级别</option>
|
||||
<option value="critical">critical</option>
|
||||
<option value="major">major</option>
|
||||
<option value="minor">minor</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="info">info</option>
|
||||
</select>
|
||||
<select value={curCleared} onChange={(e) => setCurCleared(e.target.value)}>
|
||||
<option value="">is_cleared: all</option>
|
||||
<option value="true">true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>time_created</th>
|
||||
<th>severity</th>
|
||||
<th>user_label</th>
|
||||
<th>object_name</th>
|
||||
<th>cause</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(currentQuery.data?.items || []).map((x) => (
|
||||
<tr key={x.alarm_key}>
|
||||
<td>{x.time_created}</td>
|
||||
<td>{x.perceived_severity}</td>
|
||||
<td>
|
||||
<button
|
||||
className="link-btn"
|
||||
onClick={() => {
|
||||
setCurNeId(x.ne_id || "");
|
||||
setCurKeyword(x.user_label || x.ne_name || x.ne_id || "");
|
||||
setCurPage(1);
|
||||
}}
|
||||
>
|
||||
{x.user_label || x.ne_name || x.ne_id}
|
||||
</button>
|
||||
</td>
|
||||
<td>{x.object_name}</td>
|
||||
<td>{x.native_probable_cause}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<div className="pager__meta">共 {currentQuery.data?.total || 0} 条 · 第 {curPage} 页</div>
|
||||
<div className="pager__controls">
|
||||
<button className="pager__btn" onClick={() => setCurPage(Math.max(1, curPage - 1))} disabled={curPage <= 1}>
|
||||
上一页
|
||||
</button>
|
||||
<button className="pager__btn" onClick={() => setCurPage(curPage + 1)} disabled={(currentQuery.data?.items || []).length < curPageSize}>
|
||||
下一页
|
||||
</button>
|
||||
<select
|
||||
className="pager__size"
|
||||
value={String(curPageSize)}
|
||||
onChange={(e) => {
|
||||
setCurPageSize(Number(e.target.value) || 50);
|
||||
setCurPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="50">50/页</option>
|
||||
<option value="100">100/页</option>
|
||||
<option value="200">200/页</option>
|
||||
<option value="500">500/页</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h2>历史告警</h2>
|
||||
<div className="filter-inline">
|
||||
<input value={hisKeyword} placeholder="keyword" onChange={(e) => setHisKeyword(e.target.value)} />
|
||||
<input value={hisNeId} placeholder="ne_id" onChange={(e) => setHisNeId(e.target.value)} />
|
||||
<input value={hisTimeFrom} placeholder="time_from(ISO8601)" onChange={(e) => setHisTimeFrom(e.target.value)} />
|
||||
<input value={hisTimeTo} placeholder="time_to(ISO8601)" onChange={(e) => setHisTimeTo(e.target.value)} />
|
||||
<select value={hisSeverity} onChange={(e) => setHisSeverity(e.target.value)}>
|
||||
<option value="">全部级别</option>
|
||||
<option value="critical">critical</option>
|
||||
<option value="major">major</option>
|
||||
<option value="minor">minor</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="info">info</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<button onClick={() => applyQuickRange(1)}>最近1小时</button>
|
||||
<button onClick={() => applyQuickRange(24)}>最近24小时</button>
|
||||
<button onClick={() => applyQuickRange(24 * 7)}>最近7天</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHisTimeFrom("");
|
||||
setHisTimeTo("");
|
||||
setHisPage(1);
|
||||
}}
|
||||
>
|
||||
清空时间
|
||||
</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>time_created</th>
|
||||
<th>severity</th>
|
||||
<th>user_label</th>
|
||||
<th>object_name</th>
|
||||
<th>cause</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(historyQuery.data?.items || []).map((x) => (
|
||||
<tr key={x.alarm_key}>
|
||||
<td>{x.time_created}</td>
|
||||
<td>{x.perceived_severity}</td>
|
||||
<td>
|
||||
<button
|
||||
className="link-btn"
|
||||
onClick={() => {
|
||||
setCurNeId(x.ne_id || "");
|
||||
setCurKeyword(x.user_label || x.ne_name || x.ne_id || "");
|
||||
setCurPage(1);
|
||||
}}
|
||||
>
|
||||
{x.user_label || x.ne_name || x.ne_id}
|
||||
</button>
|
||||
</td>
|
||||
<td>{x.object_name}</td>
|
||||
<td>{x.native_probable_cause}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<div className="pager__meta">共 {historyQuery.data?.total || 0} 条 · 第 {hisPage} 页</div>
|
||||
<div className="pager__controls">
|
||||
<button className="pager__btn" onClick={() => setHisPage(Math.max(1, hisPage - 1))} disabled={hisPage <= 1}>
|
||||
上一页
|
||||
</button>
|
||||
<button className="pager__btn" onClick={() => setHisPage(hisPage + 1)} disabled={(historyQuery.data?.items || []).length < hisPageSize}>
|
||||
下一页
|
||||
</button>
|
||||
<select
|
||||
className="pager__size"
|
||||
value={String(hisPageSize)}
|
||||
onChange={(e) => {
|
||||
setHisPageSize(Number(e.target.value) || 50);
|
||||
setHisPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="50">50/页</option>
|
||||
<option value="100">100/页</option>
|
||||
<option value="200">200/页</option>
|
||||
<option value="500">500/页</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,15 @@
|
|||
import type { AiAnalyzeHistoryResponse, AlarmQueryResponse, Batch, Diagnostics, ImportHistoryItem, IntegrationStatus } from "../types";
|
||||
import type {
|
||||
AiAnalyzeHistoryResponse,
|
||||
AlarmQueryResponse,
|
||||
Batch,
|
||||
Diagnostics,
|
||||
ImportHistoryItem,
|
||||
IntegrationStatus,
|
||||
UmeAlarmItem,
|
||||
UmeNeItem,
|
||||
UmeSyncStatusResponse,
|
||||
UmeTokenStatus,
|
||||
} from "../types";
|
||||
|
||||
export const apiGet = async <T,>(path: string): Promise<T> => {
|
||||
const res = await fetch(path, { headers: { accept: "application/json" } });
|
||||
|
|
@ -89,3 +100,61 @@ export const fetchAlarms = (params: {
|
|||
return apiGet<AlarmQueryResponse>(`/v1/alarms?${p.toString()}`);
|
||||
};
|
||||
|
||||
export const fetchUmeSyncStatus = (params: { page: number; pageSize: number }) => {
|
||||
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 || 20)))));
|
||||
return apiGet<UmeSyncStatusResponse>(`/v1/ume/sync/status?${p.toString()}`);
|
||||
};
|
||||
export const fetchUmeTokenStatus = () => apiGet<UmeTokenStatus>("/v1/ume/token/status");
|
||||
export const refreshUmeToken = () => apiPost<UmeTokenStatus>("/v1/ume/token/refresh", {});
|
||||
export const disconnectUmeToken = () => apiPost<UmeTokenStatus>("/v1/ume/token/disconnect", {});
|
||||
|
||||
export const fetchUmeNe = (params: { keyword: string; page: number; pageSize: number }) => {
|
||||
const p = new URLSearchParams();
|
||||
if (params.keyword) p.set("keyword", params.keyword);
|
||||
p.set("page", String(Math.max(1, Number(params.page || 1))));
|
||||
p.set("page_size", String(Math.max(1, Math.min(500, Number(params.pageSize || 50)))));
|
||||
return apiGet<{ total: number; page: number; page_size: number; items: UmeNeItem[] }>(`/v1/ume/inventory/ne?${p.toString()}`);
|
||||
};
|
||||
|
||||
export const fetchUmeCurrentAlarms = (params: {
|
||||
severity: string;
|
||||
isCleared: string;
|
||||
neId: string;
|
||||
keyword: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
const p = new URLSearchParams();
|
||||
if (params.severity) p.set("severity", params.severity);
|
||||
if (params.isCleared) p.set("is_cleared", params.isCleared);
|
||||
if (params.neId) p.set("ne_id", params.neId);
|
||||
if (params.keyword) p.set("keyword", params.keyword);
|
||||
p.set("page", String(Math.max(1, Number(params.page || 1))));
|
||||
p.set("page_size", String(Math.max(1, Math.min(500, Number(params.pageSize || 50)))));
|
||||
return apiGet<{ total: number; page: number; page_size: number; items: UmeAlarmItem[] }>(`/v1/ume/alarms?${p.toString()}`);
|
||||
};
|
||||
|
||||
export const fetchUmeHistoryAlarms = (params: {
|
||||
severity: string;
|
||||
neId: string;
|
||||
keyword: string;
|
||||
timeFrom: string;
|
||||
timeTo: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
const p = new URLSearchParams();
|
||||
if (params.severity) p.set("severity", params.severity);
|
||||
if (params.neId) p.set("ne_id", params.neId);
|
||||
if (params.keyword) p.set("keyword", params.keyword);
|
||||
if (params.timeFrom) p.set("time_from", params.timeFrom);
|
||||
if (params.timeTo) p.set("time_to", params.timeTo);
|
||||
p.set("page", String(Math.max(1, Number(params.page || 1))));
|
||||
p.set("page_size", String(Math.max(1, Math.min(500, Number(params.pageSize || 50)))));
|
||||
return apiGet<{ total: number; page: number; page_size: number; items: UmeAlarmItem[] }>(
|
||||
`/v1/ume/alarms/history?${p.toString()}`,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -73,3 +73,58 @@ export type AiAnalyzeHistoryResponse = {
|
|||
page_size: number;
|
||||
items: AiAnalyzeHistoryItem[];
|
||||
};
|
||||
|
||||
export type UmeSyncJobItem = {
|
||||
id: number;
|
||||
domain: string;
|
||||
status: string;
|
||||
trigger_mode: string;
|
||||
pulled_count: number;
|
||||
inserted_count: number;
|
||||
updated_count: number;
|
||||
error_message?: string;
|
||||
started_at: string;
|
||||
ended_at?: string | null;
|
||||
};
|
||||
|
||||
export type UmeSyncStatusResponse = {
|
||||
total?: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
items: UmeSyncJobItem[];
|
||||
latest_by_domain?: Record<string, UmeSyncJobItem>;
|
||||
};
|
||||
|
||||
export type UmeNeItem = {
|
||||
ne_id: string;
|
||||
ne_name: string;
|
||||
user_label: string;
|
||||
ip_address: string;
|
||||
ne_type: string;
|
||||
last_seen_at?: string;
|
||||
};
|
||||
|
||||
export type UmeAlarmItem = {
|
||||
alarm_key: string;
|
||||
ne_id: string;
|
||||
ne_name: string;
|
||||
user_label: string;
|
||||
object_name: string;
|
||||
event_type: string;
|
||||
native_probable_cause: string;
|
||||
perceived_severity: string;
|
||||
is_cleared: string;
|
||||
time_created: string;
|
||||
last_seen_at?: string;
|
||||
};
|
||||
|
||||
export type UmeTokenStatus = {
|
||||
ok: boolean;
|
||||
has_token: boolean;
|
||||
expires_in_s: number;
|
||||
expires_at_epoch_s: number;
|
||||
auth_header: string;
|
||||
token_preview?: string;
|
||||
error_kind?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue