diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 1ae50c1..73ae78b 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -1398,8 +1398,9 @@ const en = {
termConnect: {
title: "Complete login",
titleCreate: "Create NE and connect",
- hint: "This node is not managed or lacks credentials. Enter IP and protocol to connect (same as creating/completing an NE).",
+ hint: "This node is not managed or lacks credentials. Enter IP, vendor, and protocol to connect (same as creating/completing an NE).",
name: "Name",
+ vendor: "Vendor",
ip: "IP address",
port: "Port",
protocol: "Protocol",
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 8916a68..27d8c34 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -1392,8 +1392,9 @@ const zh = {
termConnect: {
title: "补全登录信息",
titleCreate: "新建网元并登录",
- hint: "该网元尚未纳管或缺少登录信息,请填写 IP 与协议后连接(等同新建/补全网元)。",
+ hint: "该网元尚未纳管或缺少登录信息,请填写 IP、厂家与协议后连接(等同新建/补全网元)。",
name: "名称",
+ vendor: "厂家",
ip: "IP 地址",
port: "端口",
protocol: "协议",
diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx
index d82722f..f6faa30 100644
--- a/web/src/pages/TopologyPage.tsx
+++ b/web/src/pages/TopologyPage.tsx
@@ -42,6 +42,7 @@ import {
fetchLldpDiscoverJob,
fetchManagedNe,
fetchManagedNeById,
+ fetchManagedNeMeta,
fetchTopologyGraph,
fetchTopologyTree,
fetchUmeNe,
@@ -298,6 +299,7 @@ type CtxMenu =
type TermConnectDialog = {
neId: string;
name: string;
+ vendor: string;
ip_address: string;
port: number;
protocol: "ssh" | "telnet";
@@ -836,6 +838,16 @@ export function TopologyPage() {
});
}, [edges, expandPhysicalLinks, hidePorts, edgeFlow, edgeDefaults, selectedEdgeId]);
+ const neMetaQuery = useQuery({
+ queryKey: queryKeys.managedNeMeta,
+ queryFn: fetchManagedNeMeta,
+ enabled: Boolean(termDialog),
+ staleTime: 60_000,
+ });
+ const termVendors = neMetaQuery.data?.vendors?.length
+ ? neMetaQuery.data.vendors
+ : ["ZTE", "Huawei", "Cisco", "H3C", "Juniper", "Nokia", "Other"];
+
const treeQuery = useQuery({
queryKey: queryKeys.topologyTree,
queryFn: fetchTopologyTree,
@@ -1893,6 +1905,7 @@ export function TopologyPage() {
setTermDialog({
neId: managedId,
name: String(ne.name || node.data.label || "").trim(),
+ vendor: String(ne.vendor || node.data.vendor || "Other").trim() || "Other",
ip_address: hintIp,
port: Number(ne.port) || (proto === "telnet" ? 23 : 22),
protocol: proto,
@@ -1918,6 +1931,7 @@ export function TopologyPage() {
setTermDialog({
neId: "",
name: String(node.data.label || "").trim(),
+ vendor: String(node.data.vendor || "Other").trim() || "Other",
ip_address: String(node.data.ne_ip || "").trim(),
port: 22,
protocol: "ssh",
@@ -1945,12 +1959,14 @@ export function TopologyPage() {
}
const port = Number(termDialog.port) || (protocol === "telnet" ? 23 : 22);
const name = termDialog.name.trim() || ip;
+ const vendor = String(termDialog.vendor || "Other").trim() || "Other";
setTermBusy(true);
try {
let neId = termDialog.neId;
if (neId) {
await updateManagedNe(neId, {
name,
+ vendor,
ip_address: ip,
port,
protocol,
@@ -1960,14 +1976,22 @@ export function TopologyPage() {
setNodes((ns) =>
ns.map((n) =>
String(n.data.managed_ne_id || "") === neId
- ? { ...n, data: { ...n.data, ne_ip: ip, label: name || n.data.label } }
+ ? {
+ ...n,
+ data: {
+ ...n.data,
+ ne_ip: ip,
+ vendor,
+ label: name || n.data.label,
+ },
+ }
: n,
),
);
} else {
const created = await createManagedNe({
name,
- vendor: "Other",
+ vendor,
device_type: "generic",
ip_address: ip,
port,
@@ -1979,6 +2003,7 @@ export function TopologyPage() {
}
setTermDialog(null);
showOk(t("topology.termConnect.saved"));
+ void queryClient.invalidateQueries({ queryKey: queryKeys.managedNeAll });
openOrFocusModule({
moduleId: "webcrt",
path: `/webcrt?ne_id=${encodeURIComponent(neId)}`,
@@ -3909,6 +3934,31 @@ export function TopologyPage() {
onChange={(e) => setTermDialog({ ...termDialog, name: e.target.value })}
/>
+