mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 07:53:15 +08:00
Add ops task overview, chart ahead window, and WebCRT lifecycle status.
Unify live runners in /audit/tasks (including UME sync and connect tests), extend port-traffic compare past now via ahead_hours, and distinguish WebCRT connecting/ready/detached. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3f0787da11
commit
3908b78dbb
28 changed files with 1083 additions and 54 deletions
|
|
@ -20,6 +20,8 @@ import { PortTrafficBoardListPage } from "./pages/network/PortTrafficBoardListPa
|
|||
import { PortTrafficWallPage } from "./pages/network/PortTrafficWallPage";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { UsersPage } from "./pages/UsersPage";
|
||||
import { AuditLayout } from "./pages/audit/AuditLayout";
|
||||
import { TaskOverviewPage } from "./pages/audit/TaskOverviewPage";
|
||||
import { AuditPage } from "./pages/AuditPage";
|
||||
import { ApiTokensPage } from "./pages/ApiTokensPage";
|
||||
import { ForceChangePasswordPage } from "./pages/ForceChangePasswordPage";
|
||||
|
|
@ -117,7 +119,11 @@ function ProtectedApp() {
|
|||
</Route>
|
||||
<Route path="/collect" element={<Navigate to="/network/tasks/collect" replace />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/audit" element={<AuditPage />} />
|
||||
<Route path="/audit" element={<AuditLayout />}>
|
||||
<Route index element={<Navigate to="tasks" replace />} />
|
||||
<Route path="tasks" element={<TaskOverviewPage />} />
|
||||
<Route path="logs" element={<AuditPage />} />
|
||||
</Route>
|
||||
<Route path="/api-keys" element={<ApiTokensPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
|
|
|||
10
web/src/config/auditNav.ts
Normal file
10
web/src/config/auditNav.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type AuditNavItem = {
|
||||
id: string;
|
||||
path: string;
|
||||
labelKey: string;
|
||||
};
|
||||
|
||||
export const AUDIT_NAV: readonly AuditNavItem[] = [
|
||||
{ id: "tasks", path: "/audit/tasks", labelKey: "audit.nav.tasks" },
|
||||
{ id: "logs", path: "/audit/logs", labelKey: "audit.nav.logs" },
|
||||
] as const;
|
||||
|
|
@ -118,6 +118,8 @@ export function moduleIdFromPath(pathname: string): string | null {
|
|||
}
|
||||
|
||||
export function getPageTitleKey(pathname: string): string {
|
||||
if (pathname.startsWith("/audit/tasks")) return "audit.tasks.title";
|
||||
if (pathname.startsWith("/audit/logs") || pathname === "/audit") return "audit.logsTitle";
|
||||
const moduleId = moduleIdFromPath(pathname);
|
||||
if (moduleId) {
|
||||
const mod = getModuleById(moduleId);
|
||||
|
|
|
|||
|
|
@ -96,5 +96,7 @@ export const queryKeys = {
|
|||
baseline: string,
|
||||
offsetHours: number,
|
||||
baselineTargetId: string,
|
||||
) => ["portTrafficCompare", targetId, rangeHours, baseline, offsetHours, baselineTargetId] as const,
|
||||
aheadHours = 0,
|
||||
) =>
|
||||
["portTrafficCompare", targetId, rangeHours, baseline, offsetHours, baselineTargetId, aheadHours] as const,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const en = {
|
|||
users: "Users",
|
||||
usersDesc: "Admin: create, disable users and reset passwords",
|
||||
audit: "Audit",
|
||||
auditDesc: "View login and operation records",
|
||||
auditDesc: "Live task overview and operation logs",
|
||||
apiKeys: "API Keys",
|
||||
apiKeysDesc: "Issue MCP/script tokens per user with expiry",
|
||||
},
|
||||
|
|
@ -170,6 +170,11 @@ const en = {
|
|||
compareShift: "Previous window",
|
||||
compareCustom: "Custom offset",
|
||||
offsetHours: "Offset (hours)",
|
||||
aheadHours: "Extend past now (hours)",
|
||||
aheadHoursHint:
|
||||
"Push the chart end past now. With period compare enabled, the overlay can show trend after the current clock; 0 ends at now.",
|
||||
mapBaselineHint:
|
||||
"Mapped baseline can be combined with period compare; turn on Yesterday yourself when you need that contrast.",
|
||||
baseline: "Baseline",
|
||||
baselineAt: "Baseline time",
|
||||
baselineDevice: "Baseline device",
|
||||
|
|
@ -1474,6 +1479,55 @@ const en = {
|
|||
sliceStats: "{{maps}} map(s) planned, {{overlap}} NE(s) on multiple maps",
|
||||
nodeCount: "{{count}} NE(s)",
|
||||
},
|
||||
audit: {
|
||||
title: "Audit & ops",
|
||||
logsTitle: "Operation logs",
|
||||
nav: {
|
||||
tasks: "Task overview",
|
||||
logs: "Operation logs",
|
||||
},
|
||||
tasks: {
|
||||
title: "Task overview",
|
||||
hint: "Live view of running or pending background work. Actor prefers recent audit entries; scheduled work shows as scheduler/system.",
|
||||
empty: "No tasks to show right now.",
|
||||
open: "Open",
|
||||
kpiActive: "Active / in-flight",
|
||||
kpiTotal: "Total rows",
|
||||
kpiKinds: "By kind",
|
||||
kpiStatuses: "By status",
|
||||
colKind: "Kind",
|
||||
colTitle: "Task",
|
||||
colStatus: "Status",
|
||||
colActor: "Actor",
|
||||
colTrigger: "Trigger",
|
||||
colProgress: "Progress",
|
||||
colStarted: "Started",
|
||||
colUpdated: "Updated",
|
||||
colLink: "Open",
|
||||
kind: {
|
||||
port_traffic: "Port traffic",
|
||||
config_sync: "Config sync",
|
||||
ne_collect: "Batch collect",
|
||||
lldp_discover: "LLDP discover",
|
||||
ume_sync: "UME sync",
|
||||
ne_connect: "Connect test",
|
||||
ume_runtime: "UME runtime",
|
||||
webcrt: "WebCRT",
|
||||
},
|
||||
status: {
|
||||
collecting: "Collecting",
|
||||
running: "Running",
|
||||
testing: "Testing",
|
||||
connecting: "Logging in",
|
||||
ready: "Connected",
|
||||
detached: "Detached (grace)",
|
||||
pending: "Pending",
|
||||
paused: "Paused",
|
||||
error: "Error",
|
||||
failed: "Failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const zh = {
|
|||
users: "用户管理",
|
||||
usersDesc: "管理员添加、禁用用户并重置密码",
|
||||
audit: "操作审计",
|
||||
auditDesc: "查看登录与操作记录",
|
||||
auditDesc: "任务概览与操作日志",
|
||||
apiKeys: "API Key",
|
||||
apiKeysDesc: "为用户生成 MCP/脚本用 Token,可设有效期",
|
||||
},
|
||||
|
|
@ -170,6 +170,10 @@ const zh = {
|
|||
compareShift: "前一窗",
|
||||
compareCustom: "自定义偏移",
|
||||
offsetHours: "偏移(小时)",
|
||||
aheadHours: "向后延长(小时)",
|
||||
aheadHoursHint:
|
||||
"图表终点相对当前时刻再往后延长。开启周期对比时可看到对照曲线在「现在」之后的走势;0 表示截止到现在。",
|
||||
mapBaselineHint: "映射基线可与周期对比叠加使用;需要昨日对照时请自行打开「昨日同时刻」。",
|
||||
baseline: "基线",
|
||||
baselineAt: "基线原时刻",
|
||||
baselineDevice: "基线网元",
|
||||
|
|
@ -1466,6 +1470,55 @@ const zh = {
|
|||
sliceStats: "计划 {{maps}} 张图,重复上图网元 {{overlap}} 个",
|
||||
nodeCount: "{{count}} 个网元",
|
||||
},
|
||||
audit: {
|
||||
title: "操作审计",
|
||||
logsTitle: "操作日志",
|
||||
nav: {
|
||||
tasks: "任务概览",
|
||||
logs: "操作日志",
|
||||
},
|
||||
tasks: {
|
||||
title: "任务概览",
|
||||
hint: "汇总当前系统中正在运行或挂起的各类后台任务;发起人优先取近期审计记录,调度类任务显示为 scheduler/system。",
|
||||
empty: "当前没有可展示的任务。",
|
||||
open: "打开",
|
||||
kpiActive: "活跃/在途",
|
||||
kpiTotal: "条目总数",
|
||||
kpiKinds: "按类型",
|
||||
kpiStatuses: "按状态",
|
||||
colKind: "类型",
|
||||
colTitle: "任务",
|
||||
colStatus: "状态",
|
||||
colActor: "发起人",
|
||||
colTrigger: "触发",
|
||||
colProgress: "进度",
|
||||
colStarted: "开始",
|
||||
colUpdated: "更新",
|
||||
colLink: "入口",
|
||||
kind: {
|
||||
port_traffic: "端口流量",
|
||||
config_sync: "配置同步",
|
||||
ne_collect: "批量采集",
|
||||
lldp_discover: "LLDP 发现",
|
||||
ume_sync: "UME 同步",
|
||||
ne_connect: "连通性测试",
|
||||
ume_runtime: "UME 后台",
|
||||
webcrt: "WebCRT",
|
||||
},
|
||||
status: {
|
||||
collecting: "采集中",
|
||||
running: "运行中",
|
||||
testing: "测试中",
|
||||
connecting: "登录中",
|
||||
ready: "已连接",
|
||||
detached: "已断开(宽限)",
|
||||
pending: "排队",
|
||||
paused: "暂停",
|
||||
error: "错误",
|
||||
failed: "失败",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default zh;
|
||||
|
|
|
|||
|
|
@ -5957,6 +5957,23 @@ pre {
|
|||
border-left-color: var(--nm-accent);
|
||||
}
|
||||
|
||||
.network-nav__brand {
|
||||
padding: 14px 16px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--nm-brand);
|
||||
border-bottom: 1px solid var(--nm-nav-border);
|
||||
}
|
||||
|
||||
.network-nav__list--flat {
|
||||
padding: 8px 10px 12px;
|
||||
}
|
||||
|
||||
.audit-shell .network-main {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.network-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function AuditPage() {
|
|||
<div className="page-stack system-page">
|
||||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("auth.auditTitle")}</h2>
|
||||
<h2>{t("audit.logsTitle")}</h2>
|
||||
</div>
|
||||
<p className="panel__hint">{isAdmin ? t("auth.auditHintAdmin") : t("auth.auditHintUser")}</p>
|
||||
|
||||
|
|
|
|||
32
web/src/pages/audit/AuditLayout.tsx
Normal file
32
web/src/pages/audit/AuditLayout.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { NavLink, Outlet } from "react-router-dom";
|
||||
import { AUDIT_NAV } from "../../config/auditNav";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
export function AuditLayout() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="network-shell audit-shell">
|
||||
<aside className="network-nav" aria-label={t("audit.title")}>
|
||||
<div className="network-nav__brand">{t("audit.title")}</div>
|
||||
<nav className="network-nav__scroll">
|
||||
<ul className="network-nav__list network-nav__list--flat">
|
||||
{AUDIT_NAV.map((item) => (
|
||||
<li key={item.id}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) => `network-nav__link${isActive ? " is-active" : ""}`}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="network-main">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
149
web/src/pages/audit/TaskOverviewPage.tsx
Normal file
149
web/src/pages/audit/TaskOverviewPage.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { fetchOpsTasks } from "../../services/api";
|
||||
import { formatSystemTime } from "../../utils/time";
|
||||
|
||||
const POLL_MS = 4000;
|
||||
|
||||
function statusTone(status: string): string {
|
||||
const s = status.toLowerCase();
|
||||
if (s === "collecting" || s === "running" || s === "connecting" || s === "testing") return "running";
|
||||
if (s === "ready") return "ok";
|
||||
if (s === "paused" || s === "pending" || s === "detached") return "paused";
|
||||
if (s === "failed" || s === "error") return "stopped";
|
||||
return "other";
|
||||
}
|
||||
|
||||
function kindLabel(kind: string, t: (k: string) => string): string {
|
||||
const key = `audit.tasks.kind.${kind}`;
|
||||
const tr = t(key);
|
||||
return tr === key ? kind : tr;
|
||||
}
|
||||
|
||||
function statusLabel(status: string, t: (k: string) => string): string {
|
||||
const key = `audit.tasks.status.${status}`;
|
||||
const tr = t(key);
|
||||
return tr === key ? status : tr;
|
||||
}
|
||||
|
||||
export function TaskOverviewPage() {
|
||||
const { t } = useI18n();
|
||||
const query = useQuery({
|
||||
queryKey: ["opsTasks"],
|
||||
queryFn: fetchOpsTasks,
|
||||
refetchInterval: POLL_MS,
|
||||
staleTime: 1500,
|
||||
});
|
||||
|
||||
const data = query.data;
|
||||
const items = useMemo(() => data?.items || [], [data]);
|
||||
const byKind = data?.by_kind || {};
|
||||
const byStatus = data?.by_status || {};
|
||||
|
||||
return (
|
||||
<div className="page-stack system-page">
|
||||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("audit.tasks.title")}</h2>
|
||||
<button type="button" onClick={() => void query.refetch()} disabled={query.isFetching}>
|
||||
{query.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="panel__hint">{t("audit.tasks.hint")}</p>
|
||||
|
||||
<div className="pt-list-kpis">
|
||||
<div className="pt-list-kpi pt-list-kpi--live">
|
||||
<div className="pt-list-kpi__label">{t("audit.tasks.kpiActive")}</div>
|
||||
<div className="pt-list-kpi__value">{data?.active ?? "—"}</div>
|
||||
</div>
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("audit.tasks.kpiTotal")}</div>
|
||||
<div className="pt-list-kpi__value">{data?.total ?? "—"}</div>
|
||||
</div>
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("audit.tasks.kpiKinds")}</div>
|
||||
<div className="pt-list-kpi__value" style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{Object.keys(byKind).length
|
||||
? Object.entries(byKind)
|
||||
.map(([k, n]) => `${kindLabel(k, t)} ${n}`)
|
||||
.join(" · ")
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-list-kpi">
|
||||
<div className="pt-list-kpi__label">{t("audit.tasks.kpiStatuses")}</div>
|
||||
<div className="pt-list-kpi__value" style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{Object.keys(byStatus).length
|
||||
? Object.entries(byStatus)
|
||||
.map(([k, n]) => `${statusLabel(k, t)} ${n}`)
|
||||
.join(" · ")
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{query.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
|
||||
{!items.length && !query.isLoading ? (
|
||||
<div className="pt-list-empty">
|
||||
<p>{t("audit.tasks.empty")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("audit.tasks.colKind")}</th>
|
||||
<th>{t("audit.tasks.colTitle")}</th>
|
||||
<th>{t("audit.tasks.colStatus")}</th>
|
||||
<th>{t("audit.tasks.colActor")}</th>
|
||||
<th>{t("audit.tasks.colTrigger")}</th>
|
||||
<th>{t("audit.tasks.colProgress")}</th>
|
||||
<th>{t("audit.tasks.colStarted")}</th>
|
||||
<th>{t("audit.tasks.colUpdated")}</th>
|
||||
<th>{t("audit.tasks.colLink")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((row) => (
|
||||
<tr key={`${row.kind}:${row.id}`} title={row.detail || undefined}>
|
||||
<td>{kindLabel(row.kind, t)}</td>
|
||||
<td>
|
||||
<div className="pt-list-task-name">{row.title}</div>
|
||||
{row.detail ? (
|
||||
<div className="muted" style={{ fontSize: 12 }}>
|
||||
{row.detail}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`pt-list-status pt-list-status--${statusTone(row.status)}`}>
|
||||
{statusLabel(row.status, t)}
|
||||
</span>
|
||||
</td>
|
||||
<td>{row.actor || "—"}</td>
|
||||
<td>{row.trigger || "—"}</td>
|
||||
<td className="pt-list-num">{row.progress || "—"}</td>
|
||||
<td className="pt-list-time">{formatSystemTime(row.started_at) || "—"}</td>
|
||||
<td className="pt-list-time">{formatSystemTime(row.updated_at) || "—"}</td>
|
||||
<td>
|
||||
{row.href ? (
|
||||
<Link to={row.href} className="link-btn">
|
||||
{t("audit.tasks.open")}
|
||||
</Link>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -29,6 +29,10 @@ export function PortTrafficBoardPanelCell({
|
|||
}: Props) {
|
||||
const { t } = useI18n();
|
||||
const baseline = panel.baseline || "off";
|
||||
const aheadHours =
|
||||
baseline === "off" && !panel.baseline_target_id
|
||||
? 0
|
||||
: Math.max(0, Number(panel.ahead_hours ?? 1) || 0);
|
||||
const compareQuery = useQuery({
|
||||
queryKey: [
|
||||
...queryKeys.portTrafficCompare(
|
||||
|
|
@ -37,6 +41,7 @@ export function PortTrafficBoardPanelCell({
|
|||
baseline,
|
||||
baseline === "custom" ? panel.offset_hours : 0,
|
||||
panel.baseline_target_id || "",
|
||||
aheadHours,
|
||||
),
|
||||
boardId,
|
||||
panel.id,
|
||||
|
|
@ -47,6 +52,7 @@ export function PortTrafficBoardPanelCell({
|
|||
rangeHours: panel.range_hours,
|
||||
baseline,
|
||||
offsetHours: baseline === "custom" ? panel.offset_hours : undefined,
|
||||
aheadHours: aheadHours || undefined,
|
||||
baselineTargetId: panel.baseline_target_id || undefined,
|
||||
}),
|
||||
enabled: Boolean(panel.target_id) && !panel.stale,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function panelToIn(p: PortTrafficBoardPanel): PortTrafficBoardPanelIn {
|
|||
range_hours: p.range_hours,
|
||||
baseline: p.baseline || "off",
|
||||
offset_hours: p.offset_hours || 0,
|
||||
ahead_hours: p.ahead_hours ?? 1,
|
||||
baseline_target_id: p.baseline_target_id || "",
|
||||
y_mode: p.y_mode || "auto",
|
||||
ord: p.ord,
|
||||
|
|
@ -49,6 +50,10 @@ function panelToIn(p: PortTrafficBoardPanel): PortTrafficBoardPanelIn {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizePanel(p: PortTrafficBoardPanel): PortTrafficBoardPanel {
|
||||
return { ...p, ahead_hours: p.ahead_hours ?? 1 };
|
||||
}
|
||||
|
||||
/** Dedicated board wall tab at `/port-traffic/wall/:boardId` (outside Network shell). */
|
||||
export function PortTrafficWallPage() {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -136,7 +141,7 @@ export function PortTrafficWallPage() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!board || editing) return;
|
||||
setDraftPanels(board.panels || []);
|
||||
setDraftPanels((board.panels || []).map(normalizePanel));
|
||||
setDraftName(board.name || "");
|
||||
setDraftCols(board.cols || 2);
|
||||
}, [board, editing]);
|
||||
|
|
@ -188,7 +193,7 @@ export function PortTrafficWallPage() {
|
|||
|
||||
const startEdit = () => {
|
||||
if (!board) return;
|
||||
setDraftPanels([...(board.panels || [])]);
|
||||
setDraftPanels((board.panels || []).map(normalizePanel));
|
||||
setDraftName(board.name || "");
|
||||
setDraftCols(board.cols || 2);
|
||||
setEditing(true);
|
||||
|
|
@ -201,7 +206,7 @@ export function PortTrafficWallPage() {
|
|||
setDirty(false);
|
||||
closePanelSettings();
|
||||
if (board) {
|
||||
setDraftPanels(board.panels || []);
|
||||
setDraftPanels((board.panels || []).map(normalizePanel));
|
||||
setDraftName(board.name || "");
|
||||
setDraftCols(board.cols || 2);
|
||||
}
|
||||
|
|
@ -249,6 +254,7 @@ export function PortTrafficWallPage() {
|
|||
range_hours: 24,
|
||||
baseline: "off",
|
||||
offset_hours: 0,
|
||||
ahead_hours: 1,
|
||||
baseline_target_id: "",
|
||||
y_mode: "auto",
|
||||
ord: draftPanels.length,
|
||||
|
|
@ -571,9 +577,15 @@ export function PortTrafficWallPage() {
|
|||
{t("portTraffic.compare")}
|
||||
<select
|
||||
value={editPanel.baseline}
|
||||
onChange={(e) =>
|
||||
updatePanel(editPanel.id, { baseline: e.target.value as BaselineMode })
|
||||
}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value as BaselineMode;
|
||||
updatePanel(editPanel.id, {
|
||||
baseline: next,
|
||||
...(next !== "off" && !(editPanel.ahead_hours > 0)
|
||||
? { ahead_hours: 1 }
|
||||
: {}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="off">{t("portTraffic.compareOff")}</option>
|
||||
<option value="day">{t("portTraffic.compareDay")}</option>
|
||||
|
|
@ -660,6 +672,7 @@ export function PortTrafficWallPage() {
|
|||
updatePanel(editPanel.id, {
|
||||
baseline_target_id: tid,
|
||||
baseline_target: tgt,
|
||||
...(tid && !(editPanel.ahead_hours > 0) ? { ahead_hours: 1 } : {}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
@ -671,12 +684,35 @@ export function PortTrafficWallPage() {
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
{editPanel.baseline !== "off" || editPanel.baseline_target_id ? (
|
||||
<label>
|
||||
{t("portTraffic.aheadHours")}
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={24}
|
||||
step={1}
|
||||
title={t("portTraffic.aheadHoursHint")}
|
||||
value={editPanel.ahead_hours ?? 1}
|
||||
onChange={(e) =>
|
||||
updatePanel(editPanel.id, {
|
||||
ahead_hours: Math.max(0, Math.min(24, Number(e.target.value) || 0)),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
{editPanel.baseline === "week" ? (
|
||||
<p className="muted" style={{ marginTop: 10 }}>
|
||||
{t("portTraffic.retentionHint")}
|
||||
</p>
|
||||
) : null}
|
||||
{editPanel.baseline_target_id ? (
|
||||
<p className="muted" style={{ marginTop: 10 }}>
|
||||
{t("portTraffic.mapBaselineHint")}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="modal__actions">
|
||||
<button type="button" className="btn-primary" onClick={closePanelSettings}>
|
||||
{t("portTraffic.logClose")}
|
||||
|
|
|
|||
|
|
@ -205,6 +205,31 @@ export const apiPut = async <T,>(path: string, body: unknown): Promise<T> => {
|
|||
|
||||
export const fetchIntegrationStatus = () => apiGet<IntegrationStatus>("/v1/integrations/status");
|
||||
|
||||
export type OpsTaskItem = {
|
||||
kind: string;
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
trigger: string;
|
||||
actor: string;
|
||||
started_at: string | null;
|
||||
updated_at: string | null;
|
||||
progress: string;
|
||||
detail: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type OpsTasksResponse = {
|
||||
generated_at: string;
|
||||
total: number;
|
||||
active: number;
|
||||
by_kind: Record<string, number>;
|
||||
by_status: Record<string, number>;
|
||||
items: OpsTaskItem[];
|
||||
};
|
||||
|
||||
export const fetchOpsTasks = () => apiGet<OpsTasksResponse>("/v1/ops/tasks");
|
||||
|
||||
export const fetchUmeAlarmSubscriptionStatus = () =>
|
||||
apiGet<UmeAlarmSubscriptionStatus>("/v1/ume/alarm-subscription/status");
|
||||
|
||||
|
|
@ -1347,6 +1372,7 @@ export const fetchPortTrafficCompare = (params: {
|
|||
rangeHours: number;
|
||||
baseline: string;
|
||||
offsetHours?: number;
|
||||
aheadHours?: number;
|
||||
baselineTargetId?: string;
|
||||
to?: string;
|
||||
}) => {
|
||||
|
|
@ -1355,6 +1381,9 @@ export const fetchPortTrafficCompare = (params: {
|
|||
p.set("range_hours", String(params.rangeHours));
|
||||
p.set("baseline", params.baseline);
|
||||
if (params.offsetHours != null) p.set("offset_hours", String(params.offsetHours));
|
||||
if (params.aheadHours != null && params.aheadHours > 0) {
|
||||
p.set("ahead_hours", String(params.aheadHours));
|
||||
}
|
||||
if (params.baselineTargetId) p.set("baseline_target_id", params.baselineTargetId);
|
||||
if (params.to) p.set("to", params.to);
|
||||
return apiGet<PortTrafficCompare>(`/v1/port-traffic/compare?${p.toString()}`);
|
||||
|
|
|
|||
|
|
@ -798,6 +798,7 @@ export type PortTrafficBoardPanel = {
|
|||
range_hours: number;
|
||||
baseline: "off" | "day" | "week" | "shift" | "custom" | string;
|
||||
offset_hours: number;
|
||||
ahead_hours: number;
|
||||
baseline_target_id: string;
|
||||
y_mode: "auto" | "current" | "util" | string;
|
||||
ord: number;
|
||||
|
|
@ -817,6 +818,7 @@ export type PortTrafficBoardPanelIn = {
|
|||
range_hours?: number;
|
||||
baseline?: string;
|
||||
offset_hours?: number;
|
||||
ahead_hours?: number;
|
||||
baseline_target_id?: string;
|
||||
y_mode?: string;
|
||||
ord?: number;
|
||||
|
|
@ -909,6 +911,7 @@ export type PortTrafficCompare = {
|
|||
baseline: string;
|
||||
offset_hours: number;
|
||||
range_hours: number;
|
||||
ahead_hours?: number;
|
||||
current_target: PortTrafficTarget | null;
|
||||
baseline_target: PortTrafficTarget | null;
|
||||
baseline_target_id: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue