diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index c746a12..da6e345 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -21,7 +21,7 @@ const en = { backToWorkbench: "Back to Workbench", openInNewTab: "Open in new tab", openModule: "Open or switch to this module tab", - monitoring: "Monitoring", + monitoring: "System integration", operations: "Operations", system: "System", welcomeEyebrow: "Workbench", @@ -350,6 +350,7 @@ const en = { targetKeywordPh: "Name / IP / vendor", jobsTitle: "Collect jobs", jobDetailTitle: "Job details", + expand: "Details", edgesTitle: "Fabric links", edgeKeywordPh: "Endpoint name or IP", edgeStatusAll: "All statuses", @@ -417,7 +418,7 @@ const en = { tasksTitle: "Cycle tasks", taskKeywordPh: "Name / IP / message", allStatus: "All statuses", - expand: "Expand", + expand: "Details", collapse: "Collapse", kpi: { snapshots: "Snapshots", @@ -910,8 +911,8 @@ const en = { }, keyAlert: { title: "AI alarm monitor (OClaw WhatsApp)", - showPanel: "Expand", - hidePanel: "Collapse", + showPanel: "Details", + 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.", ws: "OClaw WSS", wsConnected: "connected", @@ -1030,19 +1031,21 @@ const en = { }, syncStatus: { title: "Sync history", - showPanel: "Expand", - hidePanel: "Collapse", + showPanel: "Details", + hidePanel: "Close", empty: "No sync records", + summary: "{{total}} record(s), {{running}} running", }, ne: { title: "Network elements", - showPanel: "Expand", - hidePanel: "Collapse", + showPanel: "Details", + hidePanel: "Close", keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)", clearTitle: "Clear keyword and reset to page 1", - expand: "Expand details", - collapse: "Collapse details", + expand: "Details", + collapse: "Close", cliConnect: "CLI connect", + summary: "{{total}} inventory NE(s)", }, tabs: { main: "UME sync", @@ -1050,8 +1053,8 @@ const en = { }, cli: { title: "UME CLI connection profile", - showPanel: "Expand", - hidePanel: "Collapse", + showPanel: "Details", + 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.", statusReady: "CLI profile is ready for UME NE command execution.", statusNotReady: "Set target username and hop/bastion settings (if enabled).", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index c1dd159..a0a2ab6 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -21,7 +21,7 @@ const zh = { backToWorkbench: "返回工作台", openInNewTab: "在新标签页中打开", openModule: "打开或切换到该模块页签", - monitoring: "监控", + monitoring: "系统对接", operations: "运维", system: "系统管理", welcomeEyebrow: "工作台", @@ -347,6 +347,7 @@ const zh = { targetKeywordPh: "名称 / IP / 厂商", jobsTitle: "采集任务", jobDetailTitle: "任务明细", + expand: "详情", edgesTitle: "Fabric 链路", edgeKeywordPh: "本端 / 对端名称或 IP", edgeStatusAll: "全部状态", @@ -414,7 +415,7 @@ const zh = { tasksTitle: "周期任务", taskKeywordPh: "名称 / IP / 消息", allStatus: "全部状态", - expand: "展开", + expand: "详情", collapse: "收起", kpi: { snapshots: "配置覆盖数", @@ -904,8 +905,8 @@ const zh = { }, keyAlert: { title: "AI 告警监控(OClaw WhatsApp)", - showPanel: "展开", - hidePanel: "收起", + showPanel: "详情", + hidePanel: "关闭", help: "按 notificationId 精确匹配,或按告警描述关键字模糊匹配(不区分大小写,匹配 nativeProbableCause 等字段);可选限定 UME Inventory 中的网元类型 (ne_type),不选则匹配全部设备。经 OClaw WebSocket 推送到 WhatsApp 群。鉴权与 NETX_OCLAW_ANALYZE_TOKEN 共用。", ws: "OClaw WSS", wsConnected: "已连接", @@ -1024,19 +1025,21 @@ const zh = { }, syncStatus: { title: "同步状态", - showPanel: "展开", - hidePanel: "收起", + showPanel: "详情", + hidePanel: "关闭", empty: "暂无同步记录", + summary: "共 {{total}} 条记录,进行中 {{running}}", }, ne: { title: "网元清单", - showPanel: "展开", - hidePanel: "收起", + showPanel: "详情", + hidePanel: "关闭", keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)", clearTitle: "清空 keyword,回到第 1 页", - expand: "展开详情", - collapse: "收起详情", + expand: "详情", + collapse: "关闭", cliConnect: "CLI 连通", + summary: "Inventory 网元 {{total}} 台", }, tabs: { main: "UME 同步", @@ -1044,8 +1047,8 @@ const zh = { }, cli: { title: "UME CLI 连接配置", - showPanel: "展开", - hidePanel: "收起", + showPanel: "详情", + hidePanel: "关闭", hint: "为 UME 清单网元配置统一 SSH 凭据与跳板/堡垒机;OClaw MCP 可通过 ume_ne_id 直接执行 show 命令,无需逐台录入 managed_ne。", statusReady: "CLI 配置已就绪,可对 UME 网元执行命令。", statusNotReady: "请填写目标用户名,并配置跳板/堡垒机(若启用)。", diff --git a/web/src/index.css b/web/src/index.css index 1f061c1..af3befd 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -8100,6 +8100,8 @@ pre { color: #94a3b8; } +/* Shared detail dialog for config view / LLDP jobs / config-sync cycles */ +.ops-detail-modal, .nm-config-modal { display: flex; flex-direction: column; @@ -8108,6 +8110,7 @@ pre { overflow: hidden; } +.ops-detail-modal__head, .nm-config-modal__head { display: flex; align-items: flex-start; @@ -8115,21 +8118,172 @@ pre { gap: 12px; flex-wrap: wrap; margin-bottom: 12px; + flex: 0 0 auto; } +.ops-detail-modal__title h3, .nm-config-modal__title h3 { margin: 0 0 4px; } +.ops-detail-modal__title .muted, .nm-config-modal__title .muted { margin: 0; font-size: 13px; } +.ops-detail-modal__actions, .nm-config-modal__actions { 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 { margin-bottom: 10px; } diff --git a/web/src/pages/ConfigSyncPage.tsx b/web/src/pages/ConfigSyncPage.tsx index 9b473f7..0a18204 100644 --- a/web/src/pages/ConfigSyncPage.tsx +++ b/web/src/pages/ConfigSyncPage.tsx @@ -436,12 +436,15 @@ export function ConfigSyncPage() { {c.id.slice(0, 8)} @@ -524,74 +527,127 @@ export function ConfigSyncPage() { {expandedCycleId ? ( -
-

{t("configSync.tasksTitle")}

-
- { - setTaskKeyword(e.target.value); - setTaskPage(1); - }} - /> - -
- - - - - - - - - - - - - {(tasksQuery.data?.items ?? []).map((task) => ( - - - - - - - - - ))} - -
{t("configSync.col.name")}IP{t("configSync.col.vendor")}{t("configSync.col.source")}{t("configSync.col.status")}{t("configSync.col.message")}
{task.ne_name || task.target_id}{task.ne_ip}{task.vendor || "-"}{task.source}{task.status}{task.message || "-"}
-
- - - {t("common.pagerMeta", { - total: String(tasksQuery.data?.total ?? 0), - page: String(taskPage), - pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)), - })} - - +
setExpandedCycleId("")} + > +
e.stopPropagation()} + > +
+
+

{t("configSync.tasksTitle")}

+

+ {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}` + : ""; + })()} +

+
+
+ +
+
+ +
+ { + setTaskKeyword(e.target.value); + setTaskPage(1); + }} + /> + +
+ + {tasksQuery.isLoading ?

{t("common.refreshing")}

: null} + {tasksQuery.isError ? ( +

+ {tasksQuery.error instanceof Error ? tasksQuery.error.message : String(tasksQuery.error)} +

+ ) : null} + +
+
+ + + + + + + + + + + + + {(tasksQuery.data?.items ?? []).map((task) => ( + + + + + + + + + ))} + {!tasksQuery.isLoading && !(tasksQuery.data?.items ?? []).length ? ( + + + + ) : null} + +
{t("configSync.col.name")}IP{t("configSync.col.vendor")}{t("configSync.col.source")}{t("configSync.col.status")}{t("configSync.col.message")}
{task.ne_name || task.target_id}{task.ne_ip}{task.vendor || "-"}{task.source}{task.status}{task.message || "-"}
+ {t("common.empty")} +
+
+
+ +
+ + {t("common.pagerMeta", { + total: String(tasksQuery.data?.total ?? 0), + page: String(taskPage), + pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)), + })} + +
+ + +
+
) : null} diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 3abc7ad..3ccddeb 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiPost, @@ -123,12 +123,12 @@ export function UmePage() { }), staleTime: 3000, refetchInterval: 5000, - enabled: keyAlertPanelOpen, }); const keyAlertNeTypesQuery = useQuery({ queryKey: queryKeys.umeInventoryNeTypes, queryFn: () => fetchUmeInventoryNeTypes(), staleTime: 60_000, + enabled: keyAlertPanelOpen || Boolean(keyAlertEditRule), }); const confirmClearLocalSubscription = (hint?: string) => @@ -405,6 +405,10 @@ export function UmePage() { const syncPages = pageCount(syncTotal, syncPageSize); const neTotal = Number(neQuery.data?.total || 0); 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 keyAlertRules = keyAlertMonitorQuery.data?.rules || []; @@ -773,32 +777,33 @@ export function UmePage() {
-
+ + +
+

{t("ume.cli.title")}

-
- {cliPanelOpen ? : null} +

{t("ume.cli.hint")}

-
+

{t("ume.keyAlert.title")}

-
- {keyAlertPanelOpen ? ( - <> -
+
{t("ume.keyAlert.ws")}:{" "} {!keyAlertForwarder?.enabled @@ -814,127 +819,221 @@ export function UmePage() { {t("ume.keyAlert.publishedOk")}: {Number(keyAlertForwarder.published_ok || 0)} - - {t("ume.keyAlert.publishedFail")}: {Number(keyAlertForwarder.published_fail || 0)} - {t("ume.keyAlert.queue")}: {Number(keyAlertForwarder.queue_size || 0)} ) : null}
-
-
+ +
+
+

{t("ume.syncStatus.title")}

+ +
+

+ {syncTotal + ? t("ume.syncStatus.summary", { + total: String(syncTotal), + running: String(runningTasks.length), + }) + : t("ume.syncStatus.empty")} +

+
+ +
+
+

{t("ume.ne.title")}

+ +
+

+ {t("ume.ne.summary", { total: String(neTotal) })} +

+
+ +
+
+

{t("ume.alarms.title")}

+
+

+ {t("ume.alarms.movedHint")}{" "} + { + e.preventDefault(); + openOrFocusModule({ moduleId: "network", path: "/network/alarms" }); + }} > - - - setKeyAlertMatchValue(e.target.value)} - list="ume-key-alert-match-options" - /> - setKeyAlertLabel(e.target.value)} - /> - - {keyAlertMatchType === "keyword" - ? keyAlertKeywordHints.map((kw) => - ))} - -

+
+
+ + {cliPanelOpen ? ( +
setCliPanelOpen(false)}> +
e.stopPropagation()} + > +
+
+

{t("ume.cli.title")}

+

{t("ume.cli.hint")}

+
+
+ +
+
+
+ +
+
+
+ ) : null} + + {keyAlertPanelOpen ? ( +
setKeyAlertPanelOpen(false)}> +
e.stopPropagation()} + > +
+
+

{t("ume.keyAlert.title")}

+

+ {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)}` + : ""} +

+
+
+ + +
+
+ +
+
+ + + setKeyAlertMatchValue(e.target.value)} + list="ume-key-alert-match-options" + /> + setKeyAlertLabel(e.target.value)} + /> + + {keyAlertMatchType === "keyword" + ? keyAlertKeywordHints.map((kw) => + ))} + + + - -
-
-
- {t("ume.keyAlert.neTypesLabel")} + > + {keyAlertAddMutation.isPending ? t("ume.keyAlert.adding") : t("ume.keyAlert.add")} +
+ +
{t("ume.keyAlert.neTypesLabel")}
{keyAlertNeTypesQuery.isLoading ? ( {t("common.refreshing")} ) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? ( {t("ume.keyAlert.neTypesEmpty")} ) : ( -
+
{(keyAlertNeTypesQuery.data?.items || []).map((item) => ( -
-
- -
- {keyAlertOpError ? ( -
- {t("common.opFailed")}: {keyAlertOpError} -
- ) : null} -
- { - setKeyAlertFilterKeyword(e.target.value); - setKeyAlertPage(1); - }} - /> - - -
-
- - - - - - - - - - - - - - - - {keyAlertRules.map((rule) => ( - - - - - - - - - - - - ))} - {!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? ( - - - + + {keyAlertOpError ? ( +

+ {t("common.opFailed")}: {keyAlertOpError} +

) : null} - -
{t("ume.keyAlert.colMonitor")}{t("ume.keyAlert.colType")}{t("ume.keyAlert.colMatch")}{t("ume.keyAlert.colLabel")}{t("ume.keyAlert.colNeTypes")}{t("ume.keyAlert.colPublished")}{t("ume.keyAlert.colAttempts")}{t("ume.keyAlert.colLast")}{t("ume.keyAlert.colActions")}
- - - {rule.match_type === "keyword" - ? t("ume.keyAlert.matchKeyword") - : t("ume.keyAlert.matchNotificationId")} - {rule.match_value || rule.notification_id}{rule.label || t("common.empty")} - {rule.ne_types?.length - ? rule.ne_types.join(", ") - : t("ume.keyAlert.neTypesAll")} - {Number(rule.forward_stats?.published_ok || 0)}{Number(rule.forward_stats?.attempts || 0)} - {rule.forward_stats?.last_forwarded_at - ? formatSystemTime(rule.forward_stats.last_forwarded_at) - : t("common.empty")} - -
- - -
-
{t("ume.keyAlert.emptyRules")}
-
-
-
- {t("common.pagerMeta", { total: keyAlertTotal, page: keyAlertPage, pages: keyAlertPages })}
-
- - - { - setKeyAlertPageSize(Number(e.target.value) || 20); + setKeyAlertFilterKeyword(e.target.value); + setKeyAlertPage(1); + }} + /> + +
-
- - ) : null} - - -
-
-

{t("ume.syncStatus.title")}

- -
- {syncStatusPanelOpen ? ( -
-
- -
-
- - - - - - - - - - - - - - - - - {(syncStatusQuery.data?.items || []).map((x) => ( - - - - - - - - - - - - - ))} - {!syncStatusQuery.isLoading && (syncStatusQuery.data?.items || []).length === 0 && ( - - - - )} - -
IDdomainstatuspulledinsertedupdateddeletedstarted_atended_aterror
{x.id}{x.domain}{x.status}{x.pulled_count}{x.inserted_count}{x.updated_count}{Number(x.deleted ?? 0)}{formatSystemTime(x.started_at)}{x.ended_at ? formatSystemTime(x.ended_at) : t("common.empty")}{x.error_message || t("common.empty")}
{t("ume.syncStatus.empty")}
-
-
-
{t("common.pagerMeta", { total: syncTotal, page: syncPage, pages: syncPages })}
-
- - - +
+
+ + + + + + + + + + + + + + + + {keyAlertRules.map((rule) => ( + + + + + + + + + + + + ))} + {!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? ( + + + + ) : null} + +
{t("ume.keyAlert.colMonitor")}{t("ume.keyAlert.colType")}{t("ume.keyAlert.colMatch")}{t("ume.keyAlert.colLabel")}{t("ume.keyAlert.colNeTypes")}{t("ume.keyAlert.colPublished")}{t("ume.keyAlert.colAttempts")}{t("ume.keyAlert.colLast")}{t("ume.keyAlert.colActions")}
+ + + {rule.match_type === "keyword" + ? t("ume.keyAlert.matchKeyword") + : t("ume.keyAlert.matchNotificationId")} + {rule.match_value || rule.notification_id}{rule.label || t("common.empty")} + {rule.ne_types?.length ? rule.ne_types.join(", ") : t("ume.keyAlert.neTypesAll")} + {Number(rule.forward_stats?.published_ok || 0)}{Number(rule.forward_stats?.attempts || 0)} + {rule.forward_stats?.last_forwarded_at + ? formatSystemTime(rule.forward_stats.last_forwarded_at) + : t("common.empty")} + +
+ + +
+
+ {t("ume.keyAlert.emptyRules")} +
+
+
+ +
+ + {t("common.pagerMeta", { + total: String(keyAlertTotal), + page: String(keyAlertPage), + pages: String(keyAlertPages), + })} + +
+ + + +
+
-
- ) : null} -
+ ) : null} -
-
-

{t("ume.ne.title")}

- -
- {nePanelOpen ? ( -
-
- setNeKeyword(e.target.value)} /> - - -
-
- - - - - - - - - - - - - - - - {(neQuery.data?.items || []).map((x) => ( - - - - - - - - - - - - - {expandedNeId === x.ne_id ? ( - - - - ) : null} - - ))} - -
ne_iduser_labeliptypedevice_levelhost_name{t("ume.ne.cliConnect")}hw_verlast_seen
- - {x.user_label}{x.ip_address}{x.ne_type}{x.device_level || t("common.empty")}{x.host_name || t("common.empty")}{cliStatusByNeId.get(x.ne_id) || "unknown"}{x.hardware_version || t("common.empty")}{x.last_seen_at ? formatSystemTime(x.last_seen_at) : t("common.empty")}
-
-
consistent_state: {x.consistent_state || t("common.empty")}
-
admin_status: {x.admin_status || t("common.empty")}
-
connection_status: {x.connection_status || t("common.empty")}
-
maintain_status: {x.maintain_status || t("common.empty")}
-
address_type: {x.address_type || t("common.empty")}
-
location: {x.location || t("common.empty")}
-
loopback: {x.loopback || t("common.empty")}
-
net_mask: {x.net_mask || t("common.empty")}
-
mac: {x.mac || t("common.empty")}
-
interface_version: {x.interface_version || t("common.empty")}
-
create_time: {x.create_time || t("common.empty")}
-
creator: {x.creator || t("common.empty")}
-
-
-
-
-
{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}
-
- - - -
-
-
- ) : null} -
+
+
+

{t("ume.syncStatus.title")}

+

+ {t("ume.syncStatus.summary", { + total: String(syncTotal), + running: String(runningTasks.length), + })} +

+
+
+ + +
+
-
-
-

{t("ume.alarms.title")}

+
+
+ + + + + + + + + + + + + + + + + {(syncStatusQuery.data?.items || []).map((x) => ( + + + + + + + + + + + + + ))} + {!syncStatusQuery.isLoading && (syncStatusQuery.data?.items || []).length === 0 ? ( + + + + ) : null} + +
IDdomainstatuspulledinsertedupdateddeletedstarted_atended_aterror
+ {String(x.id).slice(0, 8)} + {x.domain}{x.status}{x.pulled_count}{x.inserted_count}{x.updated_count}{Number(x.deleted ?? 0)}{formatSystemTime(x.started_at)}{x.ended_at ? formatSystemTime(x.ended_at) : t("common.empty")}{x.error_message || t("common.empty")}
+ {t("ume.syncStatus.empty")} +
+
+
+ +
+ + {t("common.pagerMeta", { + total: String(syncTotal), + page: String(syncPage), + pages: String(syncPages), + })} + +
+ + + +
+
+
-

- {t("ume.alarms.movedHint")}{" "} - { - e.preventDefault(); - openOrFocusModule({ moduleId: "network", path: "/network/alarms" }); - }}> - {t("network.nav.alarms")} - -

- -
+ ) : null} + + {nePanelOpen ? ( +
{ + setNePanelOpen(false); + setExpandedNeId(""); + }} + > +
e.stopPropagation()} + > +
+
+

{t("ume.ne.title")}

+

{t("ume.ne.summary", { total: String(neTotal) })}

+
+
+ +
+
+ +
+ { + setNeKeyword(e.target.value); + setNePage(1); + }} + /> + + +
+ +
+
+ + + + + + + + + + + + + + + + {(neQuery.data?.items || []).map((x) => ( + + + + + + + + + + + + + ))} + {!neQuery.isLoading && !(neQuery.data?.items || []).length ? ( + + + + ) : null} + +
ne_iduser_labeliptypedevice_levelhost_name{t("ume.ne.cliConnect")}hw_verlast_seen +
+ {x.ne_id} + {x.user_label}{x.ip_address}{x.ne_type}{x.device_level || t("common.empty")}{x.host_name || t("common.empty")}{cliStatusByNeId.get(x.ne_id) || "unknown"}{x.hardware_version || t("common.empty")}{x.last_seen_at ? formatSystemTime(x.last_seen_at) : t("common.empty")} + +
+ {t("common.empty")} +
+
+
+ +
+ + {t("common.pagerMeta", { + total: String(neTotal), + page: String(nePage), + pages: String(nePages), + })} + +
+ + + +
+
+
+
+ ) : null} + + {expandedNe ? ( +
setExpandedNeId("")}> +
e.stopPropagation()} + > +
+
+

{expandedNe.user_label || expandedNe.ne_id}

+

+ {expandedNe.ne_id} + {expandedNe.ip_address ? ` · ${expandedNe.ip_address}` : ""} + {expandedNe.ne_type ? ` · ${expandedNe.ne_type}` : ""} +

+
+
+ +
+
+
+
+
+ consistent_state +
{expandedNe.consistent_state || t("common.empty")}
+
+
+ admin_status +
{expandedNe.admin_status || t("common.empty")}
+
+
+ connection_status +
{expandedNe.connection_status || t("common.empty")}
+
+
+ maintain_status +
{expandedNe.maintain_status || t("common.empty")}
+
+
+ address_type +
{expandedNe.address_type || t("common.empty")}
+
+
+ location +
{expandedNe.location || t("common.empty")}
+
+
+ loopback +
{expandedNe.loopback || t("common.empty")}
+
+
+ net_mask +
{expandedNe.net_mask || t("common.empty")}
+
+
+ mac +
{expandedNe.mac || t("common.empty")}
+
+
+ interface_version +
{expandedNe.interface_version || t("common.empty")}
+
+
+ create_time +
{expandedNe.create_time || t("common.empty")}
+
+
+ creator +
{expandedNe.creator || t("common.empty")}
+
+
+ host_name +
{expandedNe.host_name || t("common.empty")}
+
+
+ hardware_version +
{expandedNe.hardware_version || t("common.empty")}
+
+
+ {t("ume.ne.cliConnect")} +
{cliStatusByNeId.get(expandedNe.ne_id) || "unknown"}
+
+
+ last_seen +
+ {expandedNe.last_seen_at + ? formatSystemTime(expandedNe.last_seen_at) + : t("common.empty")} +
+
+
+
+
+
+ ) : null} {keyAlertEditRule ? (
setKeyAlertEditRule(null)}> -
e.stopPropagation()}> -

{t("ume.keyAlert.editTitle")}

-

- {keyAlertEditRule.match_type === "keyword" - ? t("ume.keyAlert.matchKeyword") - : t("ume.keyAlert.matchNotificationId")} - : {keyAlertEditRule.match_value || keyAlertEditRule.notification_id} - {keyAlertEditRule.label ? ` · ${keyAlertEditRule.label}` : ""} -

-
- {t("ume.keyAlert.neTypesLabel")} +
e.stopPropagation()} + > +
+
+

{t("ume.keyAlert.editTitle")}

+

+ {keyAlertEditRule.match_type === "keyword" + ? t("ume.keyAlert.matchKeyword") + : t("ume.keyAlert.matchNotificationId")} + : {keyAlertEditRule.match_value || keyAlertEditRule.notification_id} + {keyAlertEditRule.label ? ` · ${keyAlertEditRule.label}` : ""} +

+
+
+ +
+
{t("ume.keyAlert.neTypesLabel")}
{keyAlertNeTypesQuery.isLoading ? ( {t("common.refreshing")} ) : (keyAlertNeTypesQuery.data?.items || []).length === 0 ? ( {t("ume.keyAlert.neTypesEmpty")} ) : ( -
+
{(keyAlertNeTypesQuery.data?.items || []).map((item) => ( -
diff --git a/web/src/pages/network/LldpLinksPage.tsx b/web/src/pages/network/LldpLinksPage.tsx index ebe6a02..56eb072 100644 --- a/web/src/pages/network/LldpLinksPage.tsx +++ b/web/src/pages/network/LldpLinksPage.tsx @@ -596,20 +596,19 @@ export function LldpLinksPage() { {jobs.map((job) => { - const open = expandedJobId === job.id; return ( - + {job.id.slice(0, 8)} @@ -700,83 +699,128 @@ export function LldpLinksPage() {
{expandedJobId ? ( -
-

{t("lldpLinks.jobDetailTitle")}

- {jobDetailQuery.isLoading ?

…

: null} - {jobDetailQuery.data?.error ? ( -

- {jobDetailQuery.data.error} -

- ) : null} - - - - - - - - - - - - - {detailItems.map((it) => { - const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0); - return ( - - - - - - - - - - ); - })} - {!detailItems.length && !jobDetailQuery.isLoading ? ( - - - - ) : null} - -
{t("lldpLinks.col.name")}IP{t("lldpLinks.col.status")}{t("lldpLinks.col.neighbors")}{t("lldpLinks.col.edges")}{t("lldpLinks.col.error")} -
{it.ne_name || it.ne_id || "—"}{it.ne_ip || "—"} - {!it.ok ? "fail" : it.parser_stub || unmatchedCount > 0 ? "warn" : "ok"} - {it.neighbors} - +{it.edges_added} / ~{it.edges_updated} - {unmatchedCount > 0 - ? ` · ${t("topology.discoverUnmatched").replace("{{count}}", String(unmatchedCount))}` - : ""} - {it.error || "—"} - -
- {t("common.empty")} -
-
- - - {t("common.pagerMeta", { - total: String(itemTotal), - page: String(itemPage), - pages: String(itemPages), - })} - - +
{ + setExpandedJobId(""); + setItemDetail(null); + }} + > +
e.stopPropagation()} + > +
+
+

{t("lldpLinks.jobDetailTitle")}

+

+ {(() => { + const job = jobs.find((j) => j.id === expandedJobId); + return job + ? `${job.id.slice(0, 8)} · ${job.status} · ${job.done}/${job.total}` + : expandedJobId.slice(0, 8); + })()} +

+
+
+ +
+
+ + {jobDetailQuery.isLoading ?

{t("common.refreshing")}

: null} + {jobDetailQuery.data?.error ? ( +

{jobDetailQuery.data.error}

+ ) : null} + +
+
+ + + + + + + + + + + + + {detailItems.map((it) => { + const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0); + return ( + + + + + + + + + + ); + })} + {!detailItems.length && !jobDetailQuery.isLoading ? ( + + + + ) : null} + +
{t("lldpLinks.col.name")}IP{t("lldpLinks.col.status")}{t("lldpLinks.col.neighbors")}{t("lldpLinks.col.edges")}{t("lldpLinks.col.error")} +
{it.ne_name || it.ne_id || "—"}{it.ne_ip || "—"} + {!it.ok ? "fail" : it.parser_stub || unmatchedCount > 0 ? "warn" : "ok"} + {it.neighbors} + +{it.edges_added} / ~{it.edges_updated} + {unmatchedCount > 0 + ? ` · ${t("topology.discoverUnmatched").replace("{{count}}", String(unmatchedCount))}` + : ""} + {it.error || "—"} + +
+ {t("common.empty")} +
+
+
+ +
+ + {t("common.pagerMeta", { + total: String(itemTotal), + page: String(itemPage), + pages: String(itemPages), + })} + +
+ + +
+
) : null}