mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 13:50:44 +08:00
Unify ops detail dialogs and rename workbench monitoring section.
LLDP job detail, config-sync cycle tasks, and UME fold panels now use the shared modal layout; workbench section label is 系统对接. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5a77e36839
commit
b7a179d492
6 changed files with 1245 additions and 723 deletions
|
|
@ -21,7 +21,7 @@ const en = {
|
||||||
backToWorkbench: "Back to Workbench",
|
backToWorkbench: "Back to Workbench",
|
||||||
openInNewTab: "Open in new tab",
|
openInNewTab: "Open in new tab",
|
||||||
openModule: "Open or switch to this module tab",
|
openModule: "Open or switch to this module tab",
|
||||||
monitoring: "Monitoring",
|
monitoring: "System integration",
|
||||||
operations: "Operations",
|
operations: "Operations",
|
||||||
system: "System",
|
system: "System",
|
||||||
welcomeEyebrow: "Workbench",
|
welcomeEyebrow: "Workbench",
|
||||||
|
|
@ -350,6 +350,7 @@ const en = {
|
||||||
targetKeywordPh: "Name / IP / vendor",
|
targetKeywordPh: "Name / IP / vendor",
|
||||||
jobsTitle: "Collect jobs",
|
jobsTitle: "Collect jobs",
|
||||||
jobDetailTitle: "Job details",
|
jobDetailTitle: "Job details",
|
||||||
|
expand: "Details",
|
||||||
edgesTitle: "Fabric links",
|
edgesTitle: "Fabric links",
|
||||||
edgeKeywordPh: "Endpoint name or IP",
|
edgeKeywordPh: "Endpoint name or IP",
|
||||||
edgeStatusAll: "All statuses",
|
edgeStatusAll: "All statuses",
|
||||||
|
|
@ -417,7 +418,7 @@ const en = {
|
||||||
tasksTitle: "Cycle tasks",
|
tasksTitle: "Cycle tasks",
|
||||||
taskKeywordPh: "Name / IP / message",
|
taskKeywordPh: "Name / IP / message",
|
||||||
allStatus: "All statuses",
|
allStatus: "All statuses",
|
||||||
expand: "Expand",
|
expand: "Details",
|
||||||
collapse: "Collapse",
|
collapse: "Collapse",
|
||||||
kpi: {
|
kpi: {
|
||||||
snapshots: "Snapshots",
|
snapshots: "Snapshots",
|
||||||
|
|
@ -910,8 +911,8 @@ const en = {
|
||||||
},
|
},
|
||||||
keyAlert: {
|
keyAlert: {
|
||||||
title: "AI alarm monitor (OClaw WhatsApp)",
|
title: "AI alarm monitor (OClaw WhatsApp)",
|
||||||
showPanel: "Expand",
|
showPanel: "Details",
|
||||||
hidePanel: "Collapse",
|
hidePanel: "Close",
|
||||||
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.",
|
help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Pushed via OClaw WebSocket. Shares NETX_OCLAW_ANALYZE_TOKEN.",
|
||||||
ws: "OClaw WSS",
|
ws: "OClaw WSS",
|
||||||
wsConnected: "connected",
|
wsConnected: "connected",
|
||||||
|
|
@ -1030,19 +1031,21 @@ const en = {
|
||||||
},
|
},
|
||||||
syncStatus: {
|
syncStatus: {
|
||||||
title: "Sync history",
|
title: "Sync history",
|
||||||
showPanel: "Expand",
|
showPanel: "Details",
|
||||||
hidePanel: "Collapse",
|
hidePanel: "Close",
|
||||||
empty: "No sync records",
|
empty: "No sync records",
|
||||||
|
summary: "{{total}} record(s), {{running}} running",
|
||||||
},
|
},
|
||||||
ne: {
|
ne: {
|
||||||
title: "Network elements",
|
title: "Network elements",
|
||||||
showPanel: "Expand",
|
showPanel: "Details",
|
||||||
hidePanel: "Collapse",
|
hidePanel: "Close",
|
||||||
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
||||||
clearTitle: "Clear keyword and reset to page 1",
|
clearTitle: "Clear keyword and reset to page 1",
|
||||||
expand: "Expand details",
|
expand: "Details",
|
||||||
collapse: "Collapse details",
|
collapse: "Close",
|
||||||
cliConnect: "CLI connect",
|
cliConnect: "CLI connect",
|
||||||
|
summary: "{{total}} inventory NE(s)",
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
main: "UME sync",
|
main: "UME sync",
|
||||||
|
|
@ -1050,8 +1053,8 @@ const en = {
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
title: "UME CLI connection profile",
|
title: "UME CLI connection profile",
|
||||||
showPanel: "Expand",
|
showPanel: "Details",
|
||||||
hidePanel: "Collapse",
|
hidePanel: "Close",
|
||||||
hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.",
|
hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.",
|
||||||
statusReady: "CLI profile is ready for UME NE command execution.",
|
statusReady: "CLI profile is ready for UME NE command execution.",
|
||||||
statusNotReady: "Set target username and hop/bastion settings (if enabled).",
|
statusNotReady: "Set target username and hop/bastion settings (if enabled).",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const zh = {
|
||||||
backToWorkbench: "返回工作台",
|
backToWorkbench: "返回工作台",
|
||||||
openInNewTab: "在新标签页中打开",
|
openInNewTab: "在新标签页中打开",
|
||||||
openModule: "打开或切换到该模块页签",
|
openModule: "打开或切换到该模块页签",
|
||||||
monitoring: "监控",
|
monitoring: "系统对接",
|
||||||
operations: "运维",
|
operations: "运维",
|
||||||
system: "系统管理",
|
system: "系统管理",
|
||||||
welcomeEyebrow: "工作台",
|
welcomeEyebrow: "工作台",
|
||||||
|
|
@ -347,6 +347,7 @@ const zh = {
|
||||||
targetKeywordPh: "名称 / IP / 厂商",
|
targetKeywordPh: "名称 / IP / 厂商",
|
||||||
jobsTitle: "采集任务",
|
jobsTitle: "采集任务",
|
||||||
jobDetailTitle: "任务明细",
|
jobDetailTitle: "任务明细",
|
||||||
|
expand: "详情",
|
||||||
edgesTitle: "Fabric 链路",
|
edgesTitle: "Fabric 链路",
|
||||||
edgeKeywordPh: "本端 / 对端名称或 IP",
|
edgeKeywordPh: "本端 / 对端名称或 IP",
|
||||||
edgeStatusAll: "全部状态",
|
edgeStatusAll: "全部状态",
|
||||||
|
|
@ -414,7 +415,7 @@ const zh = {
|
||||||
tasksTitle: "周期任务",
|
tasksTitle: "周期任务",
|
||||||
taskKeywordPh: "名称 / IP / 消息",
|
taskKeywordPh: "名称 / IP / 消息",
|
||||||
allStatus: "全部状态",
|
allStatus: "全部状态",
|
||||||
expand: "展开",
|
expand: "详情",
|
||||||
collapse: "收起",
|
collapse: "收起",
|
||||||
kpi: {
|
kpi: {
|
||||||
snapshots: "配置覆盖数",
|
snapshots: "配置覆盖数",
|
||||||
|
|
@ -904,8 +905,8 @@ const zh = {
|
||||||
},
|
},
|
||||||
keyAlert: {
|
keyAlert: {
|
||||||
title: "AI 告警监控(OClaw WhatsApp)",
|
title: "AI 告警监控(OClaw WhatsApp)",
|
||||||
showPanel: "展开",
|
showPanel: "详情",
|
||||||
hidePanel: "收起",
|
hidePanel: "关闭",
|
||||||
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。",
|
help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。",
|
||||||
ws: "OClaw WSS",
|
ws: "OClaw WSS",
|
||||||
wsConnected: "已连接",
|
wsConnected: "已连接",
|
||||||
|
|
@ -1024,19 +1025,21 @@ const zh = {
|
||||||
},
|
},
|
||||||
syncStatus: {
|
syncStatus: {
|
||||||
title: "同步状态",
|
title: "同步状态",
|
||||||
showPanel: "展开",
|
showPanel: "详情",
|
||||||
hidePanel: "收起",
|
hidePanel: "关闭",
|
||||||
empty: "暂无同步记录",
|
empty: "暂无同步记录",
|
||||||
|
summary: "共 {{total}} 条记录,进行中 {{running}}",
|
||||||
},
|
},
|
||||||
ne: {
|
ne: {
|
||||||
title: "网元清单",
|
title: "网元清单",
|
||||||
showPanel: "展开",
|
showPanel: "详情",
|
||||||
hidePanel: "收起",
|
hidePanel: "关闭",
|
||||||
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)",
|
||||||
clearTitle: "清空 keyword,回到第 1 页",
|
clearTitle: "清空 keyword,回到第 1 页",
|
||||||
expand: "展开详情",
|
expand: "详情",
|
||||||
collapse: "收起详情",
|
collapse: "关闭",
|
||||||
cliConnect: "CLI 连通",
|
cliConnect: "CLI 连通",
|
||||||
|
summary: "Inventory 网元 {{total}} 台",
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
main: "UME 同步",
|
main: "UME 同步",
|
||||||
|
|
@ -1044,8 +1047,8 @@ const zh = {
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
title: "UME CLI 连接配置",
|
title: "UME CLI 连接配置",
|
||||||
showPanel: "展开",
|
showPanel: "详情",
|
||||||
hidePanel: "收起",
|
hidePanel: "关闭",
|
||||||
hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。",
|
hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。",
|
||||||
statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。",
|
statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。",
|
||||||
statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。",
|
statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。",
|
||||||
|
|
|
||||||
|
|
@ -8100,6 +8100,8 @@ pre {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shared detail dialog for config view / LLDP jobs / config-sync cycles */
|
||||||
|
.ops-detail-modal,
|
||||||
.nm-config-modal {
|
.nm-config-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -8108,6 +8110,7 @@ pre {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__head,
|
||||||
.nm-config-modal__head {
|
.nm-config-modal__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
@ -8115,21 +8118,172 @@ pre {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__title h3,
|
||||||
.nm-config-modal__title h3 {
|
.nm-config-modal__title h3 {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__title .muted,
|
||||||
.nm-config-modal__title .muted {
|
.nm-config-modal__title .muted {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__actions,
|
||||||
.nm-config-modal__actions {
|
.nm-config-modal__actions {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__scroll {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(2, 8, 23, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__scroll .pt-list-table-wrap,
|
||||||
|
.ops-detail-modal__scroll .data-table {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__foot {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__error {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: #fca5a5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal--xl {
|
||||||
|
width: min(1100px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-detail-modal__scroll--pad {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page > .ume-entry-grid > .panel {
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
border-color: var(--nm-panel-border);
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 27, 45, 0.06);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 118px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry .panel__toolbar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry__summary {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry__pills {
|
||||||
|
margin: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-modal-form {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-modal-form__grow {
|
||||||
|
flex: 1 1 160px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-modal-form__label {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-modal-form__hint {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-modal-form__opts {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-ne-types {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 16px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(2, 8, 23, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-ne-types--tall {
|
||||||
|
max-height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-ne-types__item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-ne-detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-ne-detail-grid > div > .muted {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.nm-config-modal__tabs {
|
.nm-config-modal__tabs {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -436,12 +436,15 @@ export function ConfigSyncPage() {
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className="btn btn--sm btn--ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setExpandedCycleId((id) => (id === c.id ? "" : c.id));
|
setExpandedCycleId(c.id);
|
||||||
setTaskPage(1);
|
setTaskPage(1);
|
||||||
|
setTaskKeyword("");
|
||||||
|
setTaskStatus("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{expandedCycleId === c.id ? t("configSync.collapse") : t("configSync.expand")}
|
{t("configSync.expand")}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td title={c.id} className="pt-list-num">{c.id.slice(0, 8)}</td>
|
<td title={c.id} className="pt-list-num">{c.id.slice(0, 8)}</td>
|
||||||
|
|
@ -524,9 +527,39 @@ export function ConfigSyncPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedCycleId ? (
|
{expandedCycleId ? (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div
|
||||||
|
className="modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={() => setExpandedCycleId("")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("configSync.tasksTitle")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
<h3>{t("configSync.tasksTitle")}</h3>
|
<h3>{t("configSync.tasksTitle")}</h3>
|
||||||
<div className="filter-inline">
|
<p className="muted">
|
||||||
|
{expandedCycleId.slice(0, 8)}
|
||||||
|
{(() => {
|
||||||
|
const c = cycles.find((x) => x.id === expandedCycleId);
|
||||||
|
return c
|
||||||
|
? ` · ${c.trigger_mode} · ${c.status} · ${c.success_count}/${c.planned_count}`
|
||||||
|
: "";
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button type="button" onClick={() => setExpandedCycleId("")}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__toolbar filter-inline">
|
||||||
<input
|
<input
|
||||||
value={taskKeyword}
|
value={taskKeyword}
|
||||||
placeholder={t("configSync.taskKeywordPh")}
|
placeholder={t("configSync.taskKeywordPh")}
|
||||||
|
|
@ -550,7 +583,17 @@ export function ConfigSyncPage() {
|
||||||
<option value="skipped">skipped</option>
|
<option value="skipped">skipped</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<table className="data-table">
|
|
||||||
|
{tasksQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||||
|
{tasksQuery.isError ? (
|
||||||
|
<p className="ops-detail-modal__error">
|
||||||
|
{tasksQuery.error instanceof Error ? tasksQuery.error.message : String(tasksQuery.error)}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__scroll">
|
||||||
|
<div className="pt-list-table-wrap">
|
||||||
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("configSync.col.name")}</th>
|
<th>{t("configSync.col.name")}</th>
|
||||||
|
|
@ -572,12 +615,19 @@ export function ConfigSyncPage() {
|
||||||
<td title={task.message}>{task.message || "-"}</td>
|
<td title={task.message}>{task.message || "-"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
{!tasksQuery.isLoading && !(tasksQuery.data?.items ?? []).length ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="muted">
|
||||||
|
{t("common.empty")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="pager">
|
</div>
|
||||||
<button type="button" disabled={taskPage <= 1} onClick={() => setTaskPage((p) => p - 1)}>
|
</div>
|
||||||
{t("common.prevPage")}
|
|
||||||
</button>
|
<div className="ops-detail-modal__foot">
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{t("common.pagerMeta", {
|
{t("common.pagerMeta", {
|
||||||
total: String(tasksQuery.data?.total ?? 0),
|
total: String(tasksQuery.data?.total ?? 0),
|
||||||
|
|
@ -585,6 +635,10 @@ export function ConfigSyncPage() {
|
||||||
pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)),
|
pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
<div className="btn-row">
|
||||||
|
<button type="button" disabled={taskPage <= 1} onClick={() => setTaskPage((p) => p - 1)}>
|
||||||
|
{t("common.prevPage")}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={taskPage >= pageCount(Number(tasksQuery.data?.total || 0), 20)}
|
disabled={taskPage >= pageCount(Number(tasksQuery.data?.total || 0), 20)}
|
||||||
|
|
@ -594,6 +648,8 @@ export function ConfigSyncPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Fragment, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
apiPost,
|
apiPost,
|
||||||
|
|
@ -123,12 +123,12 @@ export function UmePage() {
|
||||||
}),
|
}),
|
||||||
staleTime: 3000,
|
staleTime: 3000,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
enabled: keyAlertPanelOpen,
|
|
||||||
});
|
});
|
||||||
const keyAlertNeTypesQuery = useQuery({
|
const keyAlertNeTypesQuery = useQuery({
|
||||||
queryKey: queryKeys.umeInventoryNeTypes,
|
queryKey: queryKeys.umeInventoryNeTypes,
|
||||||
queryFn: () => fetchUmeInventoryNeTypes(),
|
queryFn: () => fetchUmeInventoryNeTypes(),
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
|
enabled: keyAlertPanelOpen || Boolean(keyAlertEditRule),
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirmClearLocalSubscription = (hint?: string) =>
|
const confirmClearLocalSubscription = (hint?: string) =>
|
||||||
|
|
@ -405,6 +405,10 @@ export function UmePage() {
|
||||||
const syncPages = pageCount(syncTotal, syncPageSize);
|
const syncPages = pageCount(syncTotal, syncPageSize);
|
||||||
const neTotal = Number(neQuery.data?.total || 0);
|
const neTotal = Number(neQuery.data?.total || 0);
|
||||||
const nePages = pageCount(neTotal, nePageSize);
|
const nePages = pageCount(neTotal, nePageSize);
|
||||||
|
const expandedNe = useMemo(
|
||||||
|
() => (neQuery.data?.items || []).find((x) => x.ne_id === expandedNeId) || null,
|
||||||
|
[neQuery.data, expandedNeId],
|
||||||
|
);
|
||||||
|
|
||||||
const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder;
|
const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder;
|
||||||
const keyAlertRules = keyAlertMonitorQuery.data?.rules || [];
|
const keyAlertRules = keyAlertMonitorQuery.data?.rules || [];
|
||||||
|
|
@ -773,32 +777,33 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel">
|
</section>
|
||||||
|
|
||||||
|
<section className="ume-entry-grid">
|
||||||
|
<article className="panel ume-entry">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2 className="card-title-with-hint">
|
<h2 className="card-title-with-hint">
|
||||||
{t("ume.cli.title")}
|
{t("ume.cli.title")}
|
||||||
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
|
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" className="link-btn" onClick={() => setCliPanelOpen((x) => !x)}>
|
<button type="button" className="btn btn--sm" onClick={() => setCliPanelOpen(true)}>
|
||||||
{cliPanelOpen ? t("ume.cli.hidePanel") : t("ume.cli.showPanel")}
|
{t("ume.cli.showPanel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{cliPanelOpen ? <UmeCliConnectPanel embedded enabled /> : null}
|
<p className="ume-entry__summary muted">{t("ume.cli.hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel">
|
<article className="panel ume-entry">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2 className="card-title-with-hint">
|
<h2 className="card-title-with-hint">
|
||||||
{t("ume.keyAlert.title")}
|
{t("ume.keyAlert.title")}
|
||||||
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
||||||
</h2>
|
</h2>
|
||||||
<button type="button" className="link-btn" onClick={() => setKeyAlertPanelOpen((x) => !x)}>
|
<button type="button" className="btn btn--sm" onClick={() => setKeyAlertPanelOpen(true)}>
|
||||||
{keyAlertPanelOpen ? t("ume.keyAlert.hidePanel") : t("ume.keyAlert.showPanel")}
|
{t("ume.keyAlert.showPanel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{keyAlertPanelOpen ? (
|
<div className="ume-entry__pills actions-row actions-row--inline">
|
||||||
<>
|
|
||||||
<div className="actions-row actions-row--inline">
|
|
||||||
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
||||||
{t("ume.keyAlert.ws")}:{" "}
|
{t("ume.keyAlert.ws")}:{" "}
|
||||||
{!keyAlertForwarder?.enabled
|
{!keyAlertForwarder?.enabled
|
||||||
|
|
@ -814,20 +819,132 @@ export function UmePage() {
|
||||||
<span className="conn-pill">
|
<span className="conn-pill">
|
||||||
{t("ume.keyAlert.publishedOk")}: {Number(keyAlertForwarder.published_ok || 0)}
|
{t("ume.keyAlert.publishedOk")}: {Number(keyAlertForwarder.published_ok || 0)}
|
||||||
</span>
|
</span>
|
||||||
<span className="conn-pill">
|
|
||||||
{t("ume.keyAlert.publishedFail")}: {Number(keyAlertForwarder.published_fail || 0)}
|
|
||||||
</span>
|
|
||||||
<span className="conn-pill">
|
<span className="conn-pill">
|
||||||
{t("ume.keyAlert.queue")}: {Number(keyAlertForwarder.queue_size || 0)}
|
{t("ume.keyAlert.queue")}: {Number(keyAlertForwarder.queue_size || 0)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 10 }}>
|
</article>
|
||||||
<div
|
|
||||||
className="actions-row actions-row--inline"
|
<article className="panel ume-entry">
|
||||||
style={{ flexWrap: "wrap", alignItems: "center", gap: 8 }}
|
<div className="panel__toolbar">
|
||||||
|
<h2>{t("ume.syncStatus.title")}</h2>
|
||||||
|
<button type="button" className="btn btn--sm" onClick={() => setSyncStatusPanelOpen(true)}>
|
||||||
|
{t("ume.syncStatus.showPanel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="ume-entry__summary muted">
|
||||||
|
{syncTotal
|
||||||
|
? t("ume.syncStatus.summary", {
|
||||||
|
total: String(syncTotal),
|
||||||
|
running: String(runningTasks.length),
|
||||||
|
})
|
||||||
|
: t("ume.syncStatus.empty")}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="panel ume-entry">
|
||||||
|
<div className="panel__toolbar">
|
||||||
|
<h2>{t("ume.ne.title")}</h2>
|
||||||
|
<button type="button" className="btn btn--sm" onClick={() => setNePanelOpen(true)}>
|
||||||
|
{t("ume.ne.showPanel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="ume-entry__summary muted">
|
||||||
|
{t("ume.ne.summary", { total: String(neTotal) })}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="panel ume-entry">
|
||||||
|
<div className="panel__toolbar">
|
||||||
|
<h2>{t("ume.alarms.title")}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="ume-entry__summary muted">
|
||||||
|
{t("ume.alarms.movedHint")}{" "}
|
||||||
|
<a
|
||||||
|
href="/network/alarms"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openOrFocusModule({ moduleId: "network", path: "/network/alarms" });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{t("network.nav.alarms")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cliPanelOpen ? (
|
||||||
|
<div className="modal-backdrop" role="presentation" onClick={() => setCliPanelOpen(false)}>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.cli.title")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{t("ume.cli.title")}</h3>
|
||||||
|
<p className="muted">{t("ume.cli.hint")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button type="button" onClick={() => setCliPanelOpen(false)}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ops-detail-modal__scroll ops-detail-modal__scroll--pad">
|
||||||
|
<UmeCliConnectPanel embedded enabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{keyAlertPanelOpen ? (
|
||||||
|
<div className="modal-backdrop" role="presentation" onClick={() => setKeyAlertPanelOpen(false)}>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.keyAlert.title")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{t("ume.keyAlert.title")}</h3>
|
||||||
|
<p className="muted">
|
||||||
|
{t("ume.keyAlert.ws")}:{" "}
|
||||||
|
{!keyAlertForwarder?.enabled
|
||||||
|
? t("ume.keyAlert.wsDisabled")
|
||||||
|
: keyAlertForwarder.paused
|
||||||
|
? t("ume.keyAlert.wsPaused")
|
||||||
|
: keyAlertForwarder.connected
|
||||||
|
? t("ume.keyAlert.wsConnected")
|
||||||
|
: t("ume.keyAlert.wsDisconnected")}
|
||||||
|
{keyAlertForwarder?.enabled
|
||||||
|
? ` · ${t("ume.keyAlert.publishedOk")} ${Number(keyAlertForwarder.published_ok || 0)} · ${t("ume.keyAlert.queue")} ${Number(keyAlertForwarder.queue_size || 0)}`
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll })}
|
||||||
|
disabled={keyAlertMonitorQuery.isFetching}
|
||||||
|
>
|
||||||
|
{keyAlertMonitorQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setKeyAlertPanelOpen(false)}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ume-modal-form">
|
||||||
|
<div className="ops-detail-modal__toolbar filter-inline">
|
||||||
<label className="muted">{t("ume.keyAlert.matchType")}</label>
|
<label className="muted">{t("ume.keyAlert.matchType")}</label>
|
||||||
<select
|
<select
|
||||||
className="input"
|
className="input"
|
||||||
|
|
@ -842,8 +959,7 @@ export function UmePage() {
|
||||||
<option value="keyword">{t("ume.keyAlert.matchKeyword")}</option>
|
<option value="keyword">{t("ume.keyAlert.matchKeyword")}</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input ume-modal-form__grow"
|
||||||
style={{ flex: "1 1 180px", minWidth: 160 }}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
keyAlertMatchType === "keyword"
|
keyAlertMatchType === "keyword"
|
||||||
? t("ume.keyAlert.matchValuePhKeyword")
|
? t("ume.keyAlert.matchValuePhKeyword")
|
||||||
|
|
@ -854,8 +970,7 @@ export function UmePage() {
|
||||||
list="ume-key-alert-match-options"
|
list="ume-key-alert-match-options"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input ume-modal-form__grow"
|
||||||
style={{ flex: "1 1 140px", minWidth: 120 }}
|
|
||||||
placeholder={t("ume.keyAlert.labelPh")}
|
placeholder={t("ume.keyAlert.labelPh")}
|
||||||
value={keyAlertLabel}
|
value={keyAlertLabel}
|
||||||
onChange={(e) => setKeyAlertLabel(e.target.value)}
|
onChange={(e) => setKeyAlertLabel(e.target.value)}
|
||||||
|
|
@ -909,32 +1024,16 @@ export function UmePage() {
|
||||||
{keyAlertAddMutation.isPending ? t("ume.keyAlert.adding") : t("ume.keyAlert.add")}
|
{keyAlertAddMutation.isPending ? t("ume.keyAlert.adding") : t("ume.keyAlert.add")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 8 }}>
|
|
||||||
<div className="muted" style={{ marginBottom: 4 }}>
|
<div className="muted ume-modal-form__label">{t("ume.keyAlert.neTypesLabel")}</div>
|
||||||
{t("ume.keyAlert.neTypesLabel")}
|
|
||||||
</div>
|
|
||||||
{keyAlertNeTypesQuery.isLoading ? (
|
{keyAlertNeTypesQuery.isLoading ? (
|
||||||
<span className="muted">{t("common.refreshing")}</span>
|
<span className="muted">{t("common.refreshing")}</span>
|
||||||
) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? (
|
) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? (
|
||||||
<span className="muted">{t("ume.keyAlert.neTypesEmpty")}</span>
|
<span className="muted">{t("ume.keyAlert.neTypesEmpty")}</span>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="ume-ne-types">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "8px 16px",
|
|
||||||
maxHeight: 120,
|
|
||||||
overflowY: "auto",
|
|
||||||
padding: "6px 8px",
|
|
||||||
border: "1px solid var(--border, #ddd)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(keyAlertNeTypesQuery.data?.items || []).map((item) => (
|
{(keyAlertNeTypesQuery.data?.items || []).map((item) => (
|
||||||
<label
|
<label key={item.ne_type} className="ume-ne-types__item">
|
||||||
key={item.ne_type}
|
|
||||||
style={{ display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={keyAlertNeTypes.includes(item.ne_type)}
|
checked={keyAlertNeTypes.includes(item.ne_type)}
|
||||||
|
|
@ -952,16 +1051,10 @@ export function UmePage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="muted" style={{ marginTop: 4, fontSize: 12 }}>
|
<p className="muted ume-modal-form__hint">{t("ume.keyAlert.neTypesHint")}</p>
|
||||||
{t("ume.keyAlert.neTypesHint")}
|
|
||||||
</div>
|
<div className="ops-detail-modal__toolbar ume-modal-form__opts">
|
||||||
</div>
|
<label className="ume-ne-types__item muted">
|
||||||
<div
|
|
||||||
className="actions-row actions-row--inline"
|
|
||||||
style={{ marginTop: 8, flexWrap: "wrap", alignItems: "center", justifyContent: "space-between" }}
|
|
||||||
>
|
|
||||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
|
|
||||||
<label className="muted" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={keyAlertForwardOnClear}
|
checked={keyAlertForwardOnClear}
|
||||||
|
|
@ -969,28 +1062,21 @@ export function UmePage() {
|
||||||
onChange={(e) => keyAlertConfigMutation.mutate(e.target.checked)}
|
onChange={(e) => keyAlertConfigMutation.mutate(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
{t("ume.keyAlert.forwardOnClear")}
|
{t("ume.keyAlert.forwardOnClear")}
|
||||||
</label>
|
|
||||||
<HelpHint
|
<HelpHint
|
||||||
text={t("ume.keyAlert.forwardOnClearHelp")}
|
text={t("ume.keyAlert.forwardOnClearHelp")}
|
||||||
ariaLabel={t("common.help")}
|
ariaLabel={t("common.help")}
|
||||||
align="start"
|
align="start"
|
||||||
/>
|
/>
|
||||||
</span>
|
</label>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitorAll })}
|
|
||||||
disabled={keyAlertMonitorQuery.isFetching}
|
|
||||||
>
|
|
||||||
{keyAlertMonitorQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{keyAlertOpError ? (
|
{keyAlertOpError ? (
|
||||||
<div className="pill pill--high" style={{ marginTop: 8 }}>
|
<p className="ops-detail-modal__error">
|
||||||
{t("common.opFailed")}: {keyAlertOpError}
|
{t("common.opFailed")}: {keyAlertOpError}
|
||||||
</div>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="filter-inline" style={{ marginTop: 12 }}>
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__toolbar filter-inline">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={t("ume.keyAlert.filterKeywordPh")}
|
placeholder={t("ume.keyAlert.filterKeywordPh")}
|
||||||
|
|
@ -1023,8 +1109,10 @@ export function UmePage() {
|
||||||
<option value="false">{t("ume.keyAlert.filterEnabledOff")}</option>
|
<option value="false">{t("ume.keyAlert.filterEnabledOff")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__scroll">
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table pt-list-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("ume.keyAlert.colMonitor")}</th>
|
<th>{t("ume.keyAlert.colMonitor")}</th>
|
||||||
|
|
@ -1041,8 +1129,8 @@ export function UmePage() {
|
||||||
<tbody>
|
<tbody>
|
||||||
{keyAlertRules.map((rule) => (
|
{keyAlertRules.map((rule) => (
|
||||||
<tr key={rule.notification_id} className={rule.enabled ? undefined : "row--muted"}>
|
<tr key={rule.notification_id} className={rule.enabled ? undefined : "row--muted"}>
|
||||||
<td style={{ whiteSpace: "nowrap" }}>
|
<td className="nowrap">
|
||||||
<label style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
<label className="ume-ne-types__item">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={Boolean(rule.enabled)}
|
checked={Boolean(rule.enabled)}
|
||||||
|
|
@ -1065,9 +1153,7 @@ export function UmePage() {
|
||||||
<td>{rule.match_value || rule.notification_id}</td>
|
<td>{rule.match_value || rule.notification_id}</td>
|
||||||
<td>{rule.label || t("common.empty")}</td>
|
<td>{rule.label || t("common.empty")}</td>
|
||||||
<td>
|
<td>
|
||||||
{rule.ne_types?.length
|
{rule.ne_types?.length ? rule.ne_types.join(", ") : t("ume.keyAlert.neTypesAll")}
|
||||||
? rule.ne_types.join(", ")
|
|
||||||
: t("ume.keyAlert.neTypesAll")}
|
|
||||||
</td>
|
</td>
|
||||||
<td>{Number(rule.forward_stats?.published_ok || 0)}</td>
|
<td>{Number(rule.forward_stats?.published_ok || 0)}</td>
|
||||||
<td>{Number(rule.forward_stats?.attempts || 0)}</td>
|
<td>{Number(rule.forward_stats?.attempts || 0)}</td>
|
||||||
|
|
@ -1077,9 +1163,10 @@ export function UmePage() {
|
||||||
: t("common.empty")}
|
: t("common.empty")}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
<div className="btn-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className="btn btn--sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setKeyAlertEditRule(rule);
|
setKeyAlertEditRule(rule);
|
||||||
setKeyAlertEditNeTypes(rule.ne_types || []);
|
setKeyAlertEditNeTypes(rule.ne_types || []);
|
||||||
|
|
@ -1090,6 +1177,7 @@ export function UmePage() {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
className="btn btn--sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm(t("ume.keyAlert.confirmDelete"))) {
|
if (window.confirm(t("ume.keyAlert.confirmDelete"))) {
|
||||||
keyAlertDeleteMutation.mutate(rule.notification_id);
|
keyAlertDeleteMutation.mutate(rule.notification_id);
|
||||||
|
|
@ -1105,28 +1193,36 @@ export function UmePage() {
|
||||||
))}
|
))}
|
||||||
{!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? (
|
{!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9}>{t("ume.keyAlert.emptyRules")}</td>
|
<td colSpan={9} className="muted">
|
||||||
|
{t("ume.keyAlert.emptyRules")}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
<div className="pager pt-list-pager">
|
|
||||||
<div className="muted">
|
|
||||||
{t("common.pagerMeta", { total: keyAlertTotal, page: keyAlertPage, pages: keyAlertPages })}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__foot">
|
||||||
|
<span className="muted">
|
||||||
|
{t("common.pagerMeta", {
|
||||||
|
total: String(keyAlertTotal),
|
||||||
|
page: String(keyAlertPage),
|
||||||
|
pages: String(keyAlertPages),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
<button
|
<button
|
||||||
className="pager__btn"
|
type="button"
|
||||||
onClick={() => setKeyAlertPage(Math.max(1, keyAlertPage - 1))}
|
|
||||||
disabled={keyAlertPage <= 1}
|
disabled={keyAlertPage <= 1}
|
||||||
|
onClick={() => setKeyAlertPage(Math.max(1, keyAlertPage - 1))}
|
||||||
>
|
>
|
||||||
{t("common.prevPage")}
|
{t("common.prevPage")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="pager__btn"
|
type="button"
|
||||||
onClick={() => setKeyAlertPage(keyAlertPage + 1)}
|
|
||||||
disabled={keyAlertPage >= keyAlertPages}
|
disabled={keyAlertPage >= keyAlertPages}
|
||||||
|
onClick={() => setKeyAlertPage(keyAlertPage + 1)}
|
||||||
>
|
>
|
||||||
{t("common.nextPage")}
|
{t("common.nextPage")}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1138,33 +1234,56 @@ export function UmePage() {
|
||||||
setKeyAlertPage(1);
|
setKeyAlertPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="10">10 / page</option>
|
<option value="10">{perPage(10)}</option>
|
||||||
<option value="20">20 / page</option>
|
<option value="20">{perPage(20)}</option>
|
||||||
<option value="50">50 / page</option>
|
<option value="50">{perPage(50)}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel">
|
|
||||||
<div className="panel__toolbar">
|
|
||||||
<h2>{t("ume.syncStatus.title")}</h2>
|
|
||||||
<button type="button" className="link-btn" onClick={() => setSyncStatusPanelOpen((x) => !x)}>
|
|
||||||
{syncStatusPanelOpen ? t("ume.syncStatus.hidePanel") : t("ume.syncStatus.showPanel")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{syncStatusPanelOpen ? (
|
{syncStatusPanelOpen ? (
|
||||||
<div className="pt-list">
|
<div
|
||||||
<div className="actions-row actions-row--inline">
|
className="modal-backdrop"
|
||||||
<button type="button" onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })} disabled={syncStatusQuery.isFetching}>
|
role="presentation"
|
||||||
{t("common.refresh")}
|
onClick={() => setSyncStatusPanelOpen(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.syncStatus.title")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{t("ume.syncStatus.title")}</h3>
|
||||||
|
<p className="muted">
|
||||||
|
{t("ume.syncStatus.summary", {
|
||||||
|
total: String(syncTotal),
|
||||||
|
running: String(runningTasks.length),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })}
|
||||||
|
disabled={syncStatusQuery.isFetching}
|
||||||
|
>
|
||||||
|
{syncStatusQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setSyncStatusPanelOpen(false)}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__scroll">
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table pt-list-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|
@ -1182,7 +1301,9 @@ export function UmePage() {
|
||||||
<tbody>
|
<tbody>
|
||||||
{(syncStatusQuery.data?.items || []).map((x) => (
|
{(syncStatusQuery.data?.items || []).map((x) => (
|
||||||
<tr key={x.id}>
|
<tr key={x.id}>
|
||||||
<td>{x.id}</td>
|
<td className="pt-list-num" title={String(x.id)}>
|
||||||
|
{String(x.id).slice(0, 8)}
|
||||||
|
</td>
|
||||||
<td>{x.domain}</td>
|
<td>{x.domain}</td>
|
||||||
<td>{x.status}</td>
|
<td>{x.status}</td>
|
||||||
<td>{x.pulled_count}</td>
|
<td>{x.pulled_count}</td>
|
||||||
|
|
@ -1191,24 +1312,42 @@ export function UmePage() {
|
||||||
<td>{Number(x.deleted ?? 0)}</td>
|
<td>{Number(x.deleted ?? 0)}</td>
|
||||||
<td>{formatSystemTime(x.started_at)}</td>
|
<td>{formatSystemTime(x.started_at)}</td>
|
||||||
<td>{x.ended_at ? formatSystemTime(x.ended_at) : t("common.empty")}</td>
|
<td>{x.ended_at ? formatSystemTime(x.ended_at) : t("common.empty")}</td>
|
||||||
<td>{x.error_message || t("common.empty")}</td>
|
<td title={x.error_message || ""}>{x.error_message || t("common.empty")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!syncStatusQuery.isLoading && (syncStatusQuery.data?.items || []).length === 0 && (
|
{!syncStatusQuery.isLoading && (syncStatusQuery.data?.items || []).length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={10}>{t("ume.syncStatus.empty")}</td>
|
<td colSpan={10} className="muted">
|
||||||
|
{t("ume.syncStatus.empty")}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="pager pt-list-pager">
|
</div>
|
||||||
<div className="muted">{t("common.pagerMeta", { total: syncTotal, page: syncPage, pages: syncPages })}</div>
|
|
||||||
|
<div className="ops-detail-modal__foot">
|
||||||
|
<span className="muted">
|
||||||
|
{t("common.pagerMeta", {
|
||||||
|
total: String(syncTotal),
|
||||||
|
page: String(syncPage),
|
||||||
|
pages: String(syncPages),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
<button className="pager__btn" onClick={() => setSyncPage(Math.max(1, syncPage - 1))} disabled={syncPage <= 1}>
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={syncPage <= 1}
|
||||||
|
onClick={() => setSyncPage(Math.max(1, syncPage - 1))}
|
||||||
|
>
|
||||||
{t("common.prevPage")}
|
{t("common.prevPage")}
|
||||||
</button>
|
</button>
|
||||||
<button className="pager__btn" onClick={() => setSyncPage(syncPage + 1)} disabled={syncPage >= syncPages}>
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={syncPage >= syncPages}
|
||||||
|
onClick={() => setSyncPage(syncPage + 1)}
|
||||||
|
>
|
||||||
{t("common.nextPage")}
|
{t("common.nextPage")}
|
||||||
</button>
|
</button>
|
||||||
<select
|
<select
|
||||||
|
|
@ -1227,20 +1366,52 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel">
|
{nePanelOpen ? (
|
||||||
<div className="panel__toolbar">
|
<div
|
||||||
<h2>{t("ume.ne.title")}</h2>
|
className="modal-backdrop"
|
||||||
<button type="button" className="link-btn" onClick={() => setNePanelOpen((x) => !x)}>
|
role="presentation"
|
||||||
{nePanelOpen ? t("ume.ne.hidePanel") : t("ume.ne.showPanel")}
|
onClick={() => {
|
||||||
|
setNePanelOpen(false);
|
||||||
|
setExpandedNeId("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.ne.title")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{t("ume.ne.title")}</h3>
|
||||||
|
<p className="muted">{t("ume.ne.summary", { total: String(neTotal) })}</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setNePanelOpen(false);
|
||||||
|
setExpandedNeId("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{nePanelOpen ? (
|
</div>
|
||||||
<div className="pt-list">
|
|
||||||
<div className="filter-inline">
|
<div className="ops-detail-modal__toolbar filter-inline">
|
||||||
<input value={neKeyword} placeholder={t("ume.ne.keywordPh")} onChange={(e) => setNeKeyword(e.target.value)} />
|
<input
|
||||||
|
value={neKeyword}
|
||||||
|
placeholder={t("ume.ne.keywordPh")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNeKeyword(e.target.value);
|
||||||
|
setNePage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<button type="button" onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeNEAll })}>
|
<button type="button" onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeNEAll })}>
|
||||||
{t("common.query")}
|
{t("common.query")}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1256,8 +1427,10 @@ export function UmePage() {
|
||||||
{t("common.clearFilters")}
|
{t("common.clearFilters")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ops-detail-modal__scroll">
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table pt-list-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ne_id</th>
|
<th>ne_id</th>
|
||||||
|
|
@ -1269,20 +1442,14 @@ export function UmePage() {
|
||||||
<th>{t("ume.ne.cliConnect")}</th>
|
<th>{t("ume.ne.cliConnect")}</th>
|
||||||
<th>hw_ver</th>
|
<th>hw_ver</th>
|
||||||
<th>last_seen</th>
|
<th>last_seen</th>
|
||||||
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(neQuery.data?.items || []).map((x) => (
|
{(neQuery.data?.items || []).map((x) => (
|
||||||
<Fragment key={x.ne_id}>
|
<tr key={x.ne_id}>
|
||||||
<tr>
|
<td className="pt-list-num" title={x.ne_id}>
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
className="link-btn"
|
|
||||||
onClick={() => setExpandedNeId(expandedNeId === x.ne_id ? "" : x.ne_id)}
|
|
||||||
title={expandedNeId === x.ne_id ? t("ume.ne.collapse") : t("ume.ne.expand")}
|
|
||||||
>
|
|
||||||
{x.ne_id}
|
{x.ne_id}
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
<td>{x.user_label}</td>
|
<td>{x.user_label}</td>
|
||||||
<td>{x.ip_address}</td>
|
<td>{x.ip_address}</td>
|
||||||
|
|
@ -1292,39 +1459,50 @@ export function UmePage() {
|
||||||
<td>{cliStatusByNeId.get(x.ne_id) || "unknown"}</td>
|
<td>{cliStatusByNeId.get(x.ne_id) || "unknown"}</td>
|
||||||
<td>{x.hardware_version || t("common.empty")}</td>
|
<td>{x.hardware_version || t("common.empty")}</td>
|
||||||
<td>{x.last_seen_at ? formatSystemTime(x.last_seen_at) : t("common.empty")}</td>
|
<td>{x.last_seen_at ? formatSystemTime(x.last_seen_at) : t("common.empty")}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn--sm btn--ghost"
|
||||||
|
onClick={() => setExpandedNeId(x.ne_id)}
|
||||||
|
>
|
||||||
|
{t("ume.ne.expand")}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expandedNeId === x.ne_id ? (
|
))}
|
||||||
|
{!neQuery.isLoading && !(neQuery.data?.items || []).length ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9}>
|
<td colSpan={10} className="muted">
|
||||||
<div style={{ fontSize: 12, display: "grid", gridTemplateColumns: "repeat(3, minmax(180px, 1fr))", gap: 8 }}>
|
{t("common.empty")}
|
||||||
<div>consistent_state: {x.consistent_state || t("common.empty")}</div>
|
|
||||||
<div>admin_status: {x.admin_status || t("common.empty")}</div>
|
|
||||||
<div>connection_status: {x.connection_status || t("common.empty")}</div>
|
|
||||||
<div>maintain_status: {x.maintain_status || t("common.empty")}</div>
|
|
||||||
<div>address_type: {x.address_type || t("common.empty")}</div>
|
|
||||||
<div>location: {x.location || t("common.empty")}</div>
|
|
||||||
<div>loopback: {x.loopback || t("common.empty")}</div>
|
|
||||||
<div>net_mask: {x.net_mask || t("common.empty")}</div>
|
|
||||||
<div>mac: {x.mac || t("common.empty")}</div>
|
|
||||||
<div>interface_version: {x.interface_version || t("common.empty")}</div>
|
|
||||||
<div>create_time: {x.create_time || t("common.empty")}</div>
|
|
||||||
<div>creator: {x.creator || t("common.empty")}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="pager pt-list-pager">
|
</div>
|
||||||
<div className="muted">{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}</div>
|
|
||||||
|
<div className="ops-detail-modal__foot">
|
||||||
|
<span className="muted">
|
||||||
|
{t("common.pagerMeta", {
|
||||||
|
total: String(neTotal),
|
||||||
|
page: String(nePage),
|
||||||
|
pages: String(nePages),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
<button className="pager__btn" onClick={() => setNePage(Math.max(1, nePage - 1))} disabled={nePage <= 1}>
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={nePage <= 1}
|
||||||
|
onClick={() => setNePage(Math.max(1, nePage - 1))}
|
||||||
|
>
|
||||||
{t("common.prevPage")}
|
{t("common.prevPage")}
|
||||||
</button>
|
</button>
|
||||||
<button className="pager__btn" onClick={() => setNePage(nePage + 1)} disabled={nePage >= nePages}>
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={nePage >= nePages}
|
||||||
|
onClick={() => setNePage(nePage + 1)}
|
||||||
|
>
|
||||||
{t("common.nextPage")}
|
{t("common.nextPage")}
|
||||||
</button>
|
</button>
|
||||||
<select
|
<select
|
||||||
|
|
@ -1344,61 +1522,144 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel">
|
{expandedNe ? (
|
||||||
<div className="panel__toolbar">
|
<div className="modal-backdrop" role="presentation" onClick={() => setExpandedNeId("")}>
|
||||||
<h2>{t("ume.alarms.title")}</h2>
|
<div
|
||||||
</div>
|
className="modal modal--wide ops-detail-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.ne.expand")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{expandedNe.user_label || expandedNe.ne_id}</h3>
|
||||||
<p className="muted">
|
<p className="muted">
|
||||||
{t("ume.alarms.movedHint")}{" "}
|
{expandedNe.ne_id}
|
||||||
<a href="/network/alarms" onClick={(e) => {
|
{expandedNe.ip_address ? ` · ${expandedNe.ip_address}` : ""}
|
||||||
e.preventDefault();
|
{expandedNe.ne_type ? ` · ${expandedNe.ne_type}` : ""}
|
||||||
openOrFocusModule({ moduleId: "network", path: "/network/alarms" });
|
|
||||||
}}>
|
|
||||||
{t("network.nav.alarms")}
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button type="button" onClick={() => setExpandedNeId("")}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ops-detail-modal__scroll ops-detail-modal__scroll--pad">
|
||||||
|
<div className="ume-ne-detail-grid">
|
||||||
|
<div>
|
||||||
|
<span className="muted">consistent_state</span>
|
||||||
|
<div>{expandedNe.consistent_state || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">admin_status</span>
|
||||||
|
<div>{expandedNe.admin_status || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">connection_status</span>
|
||||||
|
<div>{expandedNe.connection_status || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">maintain_status</span>
|
||||||
|
<div>{expandedNe.maintain_status || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">address_type</span>
|
||||||
|
<div>{expandedNe.address_type || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">location</span>
|
||||||
|
<div>{expandedNe.location || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">loopback</span>
|
||||||
|
<div>{expandedNe.loopback || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">net_mask</span>
|
||||||
|
<div>{expandedNe.net_mask || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">mac</span>
|
||||||
|
<div>{expandedNe.mac || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">interface_version</span>
|
||||||
|
<div>{expandedNe.interface_version || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">create_time</span>
|
||||||
|
<div>{expandedNe.create_time || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">creator</span>
|
||||||
|
<div>{expandedNe.creator || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">host_name</span>
|
||||||
|
<div>{expandedNe.host_name || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">hardware_version</span>
|
||||||
|
<div>{expandedNe.hardware_version || t("common.empty")}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">{t("ume.ne.cliConnect")}</span>
|
||||||
|
<div>{cliStatusByNeId.get(expandedNe.ne_id) || "unknown"}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="muted">last_seen</span>
|
||||||
|
<div>
|
||||||
|
{expandedNe.last_seen_at
|
||||||
|
? formatSystemTime(expandedNe.last_seen_at)
|
||||||
|
: t("common.empty")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{keyAlertEditRule ? (
|
{keyAlertEditRule ? (
|
||||||
<div className="modal-backdrop" role="presentation" onClick={() => setKeyAlertEditRule(null)}>
|
<div className="modal-backdrop" role="presentation" onClick={() => setKeyAlertEditRule(null)}>
|
||||||
<div className="modal" role="dialog" onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("ume.keyAlert.editTitle")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
<h3>{t("ume.keyAlert.editTitle")}</h3>
|
<h3>{t("ume.keyAlert.editTitle")}</h3>
|
||||||
<p className="form-hint muted">
|
<p className="muted">
|
||||||
{keyAlertEditRule.match_type === "keyword"
|
{keyAlertEditRule.match_type === "keyword"
|
||||||
? t("ume.keyAlert.matchKeyword")
|
? t("ume.keyAlert.matchKeyword")
|
||||||
: t("ume.keyAlert.matchNotificationId")}
|
: t("ume.keyAlert.matchNotificationId")}
|
||||||
: {keyAlertEditRule.match_value || keyAlertEditRule.notification_id}
|
: {keyAlertEditRule.match_value || keyAlertEditRule.notification_id}
|
||||||
{keyAlertEditRule.label ? ` · ${keyAlertEditRule.label}` : ""}
|
{keyAlertEditRule.label ? ` · ${keyAlertEditRule.label}` : ""}
|
||||||
</p>
|
</p>
|
||||||
<div className="muted" style={{ marginBottom: 4 }}>
|
|
||||||
{t("ume.keyAlert.neTypesLabel")}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button type="button" onClick={() => setKeyAlertEditRule(null)} disabled={keyAlertEditMutation.isPending}>
|
||||||
|
{t("ume.keyAlert.cancel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="muted ume-modal-form__label">{t("ume.keyAlert.neTypesLabel")}</div>
|
||||||
{keyAlertNeTypesQuery.isLoading ? (
|
{keyAlertNeTypesQuery.isLoading ? (
|
||||||
<span className="muted">{t("common.refreshing")}</span>
|
<span className="muted">{t("common.refreshing")}</span>
|
||||||
) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? (
|
) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? (
|
||||||
<span className="muted">{t("ume.keyAlert.neTypesEmpty")}</span>
|
<span className="muted">{t("ume.keyAlert.neTypesEmpty")}</span>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="ume-ne-types ume-ne-types--tall">
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "8px 16px",
|
|
||||||
maxHeight: 240,
|
|
||||||
overflowY: "auto",
|
|
||||||
padding: "6px 8px",
|
|
||||||
border: "1px solid var(--border, #ddd)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(keyAlertNeTypesQuery.data?.items || []).map((item) => (
|
{(keyAlertNeTypesQuery.data?.items || []).map((item) => (
|
||||||
<label
|
<label key={item.ne_type} className="ume-ne-types__item">
|
||||||
key={item.ne_type}
|
|
||||||
style={{ display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={keyAlertEditNeTypes.includes(item.ne_type)}
|
checked={keyAlertEditNeTypes.includes(item.ne_type)}
|
||||||
|
|
@ -1416,10 +1677,10 @@ export function UmePage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="muted" style={{ marginTop: 4, fontSize: 12 }}>
|
<p className="muted ume-modal-form__hint">{t("ume.keyAlert.neTypesHint")}</p>
|
||||||
{t("ume.keyAlert.neTypesHint")}
|
<div className="ops-detail-modal__foot">
|
||||||
</div>
|
<span />
|
||||||
<div className="actions-row actions-row--inline" style={{ marginTop: 16 }}>
|
<div className="btn-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -1438,6 +1699,7 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -596,20 +596,19 @@ export function LldpLinksPage() {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{jobs.map((job) => {
|
{jobs.map((job) => {
|
||||||
const open = expandedJobId === job.id;
|
|
||||||
return (
|
return (
|
||||||
<tr key={job.id} className={open ? "is-expanded" : undefined}>
|
<tr key={job.id}>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn--sm btn--ghost"
|
className="btn btn--sm btn--ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setItemDetail(null);
|
setItemDetail(null);
|
||||||
setExpandedJobId(open ? "" : job.id);
|
setExpandedJobId(job.id);
|
||||||
setItemPage(1);
|
setItemPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{open ? "−" : "+"}
|
{t("lldpLinks.expand")}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="mono">{job.id.slice(0, 8)}</td>
|
<td className="mono">{job.id.slice(0, 8)}</td>
|
||||||
|
|
@ -700,15 +699,54 @@ export function LldpLinksPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedJobId ? (
|
{expandedJobId ? (
|
||||||
<div className="panel" style={{ marginTop: 16 }}>
|
<div
|
||||||
|
className="modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={() => {
|
||||||
|
setExpandedJobId("");
|
||||||
|
setItemDetail(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("lldpLinks.jobDetailTitle")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
<h3>{t("lldpLinks.jobDetailTitle")}</h3>
|
<h3>{t("lldpLinks.jobDetailTitle")}</h3>
|
||||||
{jobDetailQuery.isLoading ? <p className="muted">…</p> : null}
|
<p className="muted">
|
||||||
{jobDetailQuery.data?.error ? (
|
{(() => {
|
||||||
<p className="muted" style={{ color: "var(--danger, #b00)" }}>
|
const job = jobs.find((j) => j.id === expandedJobId);
|
||||||
{jobDetailQuery.data.error}
|
return job
|
||||||
|
? `${job.id.slice(0, 8)} · ${job.status} · ${job.done}/${job.total}`
|
||||||
|
: expandedJobId.slice(0, 8);
|
||||||
|
})()}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setExpandedJobId("");
|
||||||
|
setItemDetail(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{jobDetailQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||||
|
{jobDetailQuery.data?.error ? (
|
||||||
|
<p className="ops-detail-modal__error">{jobDetailQuery.data.error}</p>
|
||||||
) : null}
|
) : null}
|
||||||
<table className="data-table">
|
|
||||||
|
<div className="ops-detail-modal__scroll">
|
||||||
|
<div className="pt-list-table-wrap">
|
||||||
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("lldpLinks.col.name")}</th>
|
<th>{t("lldpLinks.col.name")}</th>
|
||||||
|
|
@ -759,10 +797,10 @@ export function LldpLinksPage() {
|
||||||
) : null}
|
) : null}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div className="pager">
|
</div>
|
||||||
<button type="button" disabled={itemPage <= 1} onClick={() => setItemPage((p) => p - 1)}>
|
</div>
|
||||||
{t("common.prevPage")}
|
|
||||||
</button>
|
<div className="ops-detail-modal__foot">
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{t("common.pagerMeta", {
|
{t("common.pagerMeta", {
|
||||||
total: String(itemTotal),
|
total: String(itemTotal),
|
||||||
|
|
@ -770,6 +808,10 @@ export function LldpLinksPage() {
|
||||||
pages: String(itemPages),
|
pages: String(itemPages),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
|
<div className="btn-row">
|
||||||
|
<button type="button" disabled={itemPage <= 1} onClick={() => setItemPage((p) => p - 1)}>
|
||||||
|
{t("common.prevPage")}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={itemPage >= itemPages}
|
disabled={itemPage >= itemPages}
|
||||||
|
|
@ -779,6 +821,8 @@ export function LldpLinksPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{itemDetail ? (
|
{itemDetail ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue