Require vendor when completing unmanaged NEs from topology.

Persist vendor on create/update and refresh the canvas node so icon tone matches inventory.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-04 11:50:05 +08:00
parent 57f45fdaaf
commit bee86eea0d
3 changed files with 56 additions and 4 deletions

View file

@ -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",

View file

@ -1392,8 +1392,9 @@ const zh = {
termConnect: {
title: "补全登录信息",
titleCreate: "新建网元并登录",
hint: "该网元尚未纳管或缺少登录信息,请填写 IP 与协议后连接(等同新建/补全网元)。",
hint: "该网元尚未纳管或缺少登录信息,请填写 IP、厂家与协议后连接(等同新建/补全网元)。",
name: "名称",
vendor: "厂家",
ip: "IP 地址",
port: "端口",
protocol: "协议",

View file

@ -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 })}
/>
</label>
<label>
<span className="form-label">
{t("topology.termConnect.vendor")}
<span className="form-label__required" aria-hidden="true">
{" "}
*
</span>
</span>
<select
value={
termVendors.includes(termDialog.vendor) ? termDialog.vendor : termDialog.vendor || "Other"
}
disabled={termBusy}
onChange={(e) => setTermDialog({ ...termDialog, vendor: e.target.value })}
>
{!termVendors.includes(termDialog.vendor) && termDialog.vendor ? (
<option value={termDialog.vendor}>{termDialog.vendor}</option>
) : null}
{termVendors.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</label>
<label>
<span className="form-label">
{t("topology.termConnect.protocol")}