mirror of
https://github.com/hansjone/netx.git
synced 2026-10-13 02:03:23 +08:00
Route incomplete topology terminals to NE inventory instead of a mini form.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
417ed66a53
commit
c4968e334f
4 changed files with 113 additions and 332 deletions
|
|
@ -1395,25 +1395,8 @@ const en = {
|
||||||
edgeDefaults: "Default edge styles",
|
edgeDefaults: "Default edge styles",
|
||||||
edgeDefaultsReset: "Factory reset",
|
edgeDefaultsReset: "Factory reset",
|
||||||
noNeLink: "Not linked to a managed NE",
|
noNeLink: "Not linked to a managed NE",
|
||||||
termConnect: {
|
completeNeFirst:
|
||||||
title: "Complete login",
|
"Login details are incomplete (vendor, hop/proxy, etc. belong in NE inventory). Complete the NE first, then open the terminal.",
|
||||||
titleCreate: "Create NE and connect",
|
|
||||||
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",
|
|
||||||
username: "Username",
|
|
||||||
password: "Password",
|
|
||||||
connect: "Save and open terminal",
|
|
||||||
connecting: "Connecting…",
|
|
||||||
cancel: "Cancel",
|
|
||||||
ipRequired: "IP address is required",
|
|
||||||
userRequired: "Username is required for SSH",
|
|
||||||
passwordRequired: "Password is required for SSH",
|
|
||||||
saved: "Login details saved",
|
|
||||||
},
|
|
||||||
edgeManual: "Manual",
|
edgeManual: "Manual",
|
||||||
edgeDiscovered: "Discovered",
|
edgeDiscovered: "Discovered",
|
||||||
edgeStale: "Missing",
|
edgeStale: "Missing",
|
||||||
|
|
|
||||||
|
|
@ -1389,25 +1389,8 @@ const zh = {
|
||||||
edgeDefaults: "默认链路样式",
|
edgeDefaults: "默认链路样式",
|
||||||
edgeDefaultsReset: "恢复出厂",
|
edgeDefaultsReset: "恢复出厂",
|
||||||
noNeLink: "未关联托管网元",
|
noNeLink: "未关联托管网元",
|
||||||
termConnect: {
|
completeNeFirst:
|
||||||
title: "补全登录信息",
|
"该网元登录信息不完整(厂家、代理跳转等需在网元管理填写)。请先完善后再打开终端。",
|
||||||
titleCreate: "新建网元并登录",
|
|
||||||
hint: "该网元尚未纳管或缺少登录信息,请填写 IP、厂家与协议后连接(等同新建/补全网元)。",
|
|
||||||
name: "名称",
|
|
||||||
vendor: "厂家",
|
|
||||||
ip: "IP 地址",
|
|
||||||
port: "端口",
|
|
||||||
protocol: "协议",
|
|
||||||
username: "用户名",
|
|
||||||
password: "密码",
|
|
||||||
connect: "保存并打开终端",
|
|
||||||
connecting: "连接中…",
|
|
||||||
cancel: "取消",
|
|
||||||
ipRequired: "请填写 IP 地址",
|
|
||||||
userRequired: "SSH 请填写用户名",
|
|
||||||
passwordRequired: "SSH 请填写密码",
|
|
||||||
saved: "登录信息已保存",
|
|
||||||
},
|
|
||||||
edgeManual: "人工",
|
edgeManual: "人工",
|
||||||
edgeDiscovered: "发现",
|
edgeDiscovered: "发现",
|
||||||
edgeStale: "未发现",
|
edgeStale: "未发现",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
batchApplyAccountManagedNe,
|
batchApplyAccountManagedNe,
|
||||||
|
|
@ -10,6 +11,7 @@ import {
|
||||||
deleteManagedNe,
|
deleteManagedNe,
|
||||||
fetchIdsByTag,
|
fetchIdsByTag,
|
||||||
fetchManagedNe,
|
fetchManagedNe,
|
||||||
|
fetchManagedNeById,
|
||||||
fetchManagedNeMeta,
|
fetchManagedNeMeta,
|
||||||
fetchManagedNeStats,
|
fetchManagedNeStats,
|
||||||
importManagedNe,
|
importManagedNe,
|
||||||
|
|
@ -66,6 +68,15 @@ type AccountState = {
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function deviceTypeForVendor(vendor: string): string {
|
||||||
|
if (vendor === "ZTE") return "zte_zxros";
|
||||||
|
if (vendor === "Huawei") return "huawei";
|
||||||
|
if (vendor === "Cisco") return "cisco_ios";
|
||||||
|
if (vendor === "Juniper") return "juniper_junos";
|
||||||
|
if (vendor === "Nokia") return "nokia_sros";
|
||||||
|
return "generic";
|
||||||
|
}
|
||||||
|
|
||||||
const emptyForm = (): FormState => ({
|
const emptyForm = (): FormState => ({
|
||||||
name: "",
|
name: "",
|
||||||
vendor: "ZTE",
|
vendor: "ZTE",
|
||||||
|
|
@ -136,6 +147,8 @@ export function NePage() {
|
||||||
const canWriteNe = isAdmin || hasScope("ne:write");
|
const canWriteNe = isAdmin || hasScope("ne:write");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const importRef = useRef<HTMLInputElement>(null);
|
const importRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const deepLinkHandled = useRef("");
|
||||||
|
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [vendorFilter, setVendorFilter] = useState("");
|
const [vendorFilter, setVendorFilter] = useState("");
|
||||||
|
|
@ -466,6 +479,63 @@ export function NePage() {
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Deep links from topology: /ne?ne_id=… (edit) or /ne?create=1&name=&ip_address=&vendor=
|
||||||
|
useEffect(() => {
|
||||||
|
const neId = String(searchParams.get("ne_id") || "").trim();
|
||||||
|
const wantCreate = searchParams.get("create") === "1";
|
||||||
|
if (!neId && !wantCreate) {
|
||||||
|
deepLinkHandled.current = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKey = neId ? `edit:${neId}` : `create:${searchParams.toString()}`;
|
||||||
|
if (deepLinkHandled.current === handleKey) return;
|
||||||
|
deepLinkHandled.current = handleKey;
|
||||||
|
|
||||||
|
const clearDeepLink = () => {
|
||||||
|
setSearchParams(
|
||||||
|
(prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
for (const key of ["ne_id", "create", "name", "ip_address", "vendor"]) {
|
||||||
|
next.delete(key);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
},
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (neId) {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const row = await fetchManagedNeById(neId);
|
||||||
|
openEdit(row);
|
||||||
|
} catch (err) {
|
||||||
|
showError(String(err));
|
||||||
|
} finally {
|
||||||
|
clearDeepLink();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = String(searchParams.get("name") || "").trim();
|
||||||
|
const ip = String(searchParams.get("ip_address") || "").trim();
|
||||||
|
const vendorRaw = String(searchParams.get("vendor") || "").trim();
|
||||||
|
const base = emptyForm();
|
||||||
|
const vendor = vendorRaw || base.vendor;
|
||||||
|
setEditing(null);
|
||||||
|
setForm({
|
||||||
|
...base,
|
||||||
|
name,
|
||||||
|
vendor,
|
||||||
|
device_type: deviceTypeForVendor(vendor),
|
||||||
|
ip_address: ip,
|
||||||
|
});
|
||||||
|
setModalOpen(true);
|
||||||
|
clearDeepLink();
|
||||||
|
}, [searchParams, setSearchParams, showError]);
|
||||||
|
|
||||||
const toggleSelect = (id: string) => {
|
const toggleSelect = (id: string) => {
|
||||||
setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
setSelected((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ import "@xyflow/react/dist/style.css";
|
||||||
import {
|
import {
|
||||||
addTopologyViewNodes,
|
addTopologyViewNodes,
|
||||||
createFabricManualEdge,
|
createFabricManualEdge,
|
||||||
createManagedNe,
|
|
||||||
createTopologyFolder,
|
createTopologyFolder,
|
||||||
createTopologyView,
|
createTopologyView,
|
||||||
deleteTopologyFolder,
|
deleteTopologyFolder,
|
||||||
|
|
@ -42,7 +41,6 @@ import {
|
||||||
fetchLldpDiscoverJob,
|
fetchLldpDiscoverJob,
|
||||||
fetchManagedNe,
|
fetchManagedNe,
|
||||||
fetchManagedNeById,
|
fetchManagedNeById,
|
||||||
fetchManagedNeMeta,
|
|
||||||
fetchTopologyGraph,
|
fetchTopologyGraph,
|
||||||
fetchTopologyTree,
|
fetchTopologyTree,
|
||||||
fetchUmeNe,
|
fetchUmeNe,
|
||||||
|
|
@ -52,7 +50,6 @@ import {
|
||||||
removeTopologyViewNodes,
|
removeTopologyViewNodes,
|
||||||
searchFabricNodes,
|
searchFabricNodes,
|
||||||
startLldpDiscover,
|
startLldpDiscover,
|
||||||
updateManagedNe,
|
|
||||||
updateTopologyFolder,
|
updateTopologyFolder,
|
||||||
updateTopologyMap,
|
updateTopologyMap,
|
||||||
} from "../services/api";
|
} from "../services/api";
|
||||||
|
|
@ -295,18 +292,6 @@ type CtxMenu =
|
||||||
| { kind: "edge"; id: string; x: number; y: number }
|
| { kind: "edge"; id: string; x: number; y: number }
|
||||||
| { kind: "selection"; x: number; y: number };
|
| { kind: "selection"; x: number; y: number };
|
||||||
|
|
||||||
/** Complete / create login for unmanaged topology nodes (LLDP placeholders, orphans). */
|
|
||||||
type TermConnectDialog = {
|
|
||||||
neId: string;
|
|
||||||
name: string;
|
|
||||||
vendor: string;
|
|
||||||
ip_address: string;
|
|
||||||
port: number;
|
|
||||||
protocol: "ssh" | "telnet";
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TopoDisplayContext = createContext<TopoDisplayOpts>({
|
const TopoDisplayContext = createContext<TopoDisplayOpts>({
|
||||||
hideIp: true,
|
hideIp: true,
|
||||||
hideVendor: true,
|
hideVendor: true,
|
||||||
|
|
@ -724,8 +709,6 @@ export function TopologyPage() {
|
||||||
);
|
);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [addNeOpen, setAddNeOpen] = useState(false);
|
const [addNeOpen, setAddNeOpen] = useState(false);
|
||||||
const [termDialog, setTermDialog] = useState<TermConnectDialog | null>(null);
|
|
||||||
const [termBusy, setTermBusy] = useState(false);
|
|
||||||
const [paletteSource, setPaletteSource] = useState<PaletteSource>("managed");
|
const [paletteSource, setPaletteSource] = useState<PaletteSource>("managed");
|
||||||
const [paletteSelectedKeys, setPaletteSelectedKeys] = useState<string[]>([]);
|
const [paletteSelectedKeys, setPaletteSelectedKeys] = useState<string[]>([]);
|
||||||
const [paletteAdding, setPaletteAdding] = useState(false);
|
const [paletteAdding, setPaletteAdding] = useState(false);
|
||||||
|
|
@ -837,16 +820,6 @@ export function TopologyPage() {
|
||||||
});
|
});
|
||||||
}, [edges, expandPhysicalLinks, hidePorts, edgeFlow, edgeDefaults, selectedEdgeId]);
|
}, [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({
|
const treeQuery = useQuery({
|
||||||
queryKey: queryKeys.topologyTree,
|
queryKey: queryKeys.topologyTree,
|
||||||
queryFn: fetchTopologyTree,
|
queryFn: fetchTopologyTree,
|
||||||
|
|
@ -1873,44 +1846,53 @@ export function TopologyPage() {
|
||||||
closeCtxMenu();
|
closeCtxMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openNeInventory = (opts: {
|
||||||
|
neId?: string;
|
||||||
|
create?: boolean;
|
||||||
|
name?: string;
|
||||||
|
ip_address?: string;
|
||||||
|
vendor?: string;
|
||||||
|
}) => {
|
||||||
|
const q = new URLSearchParams();
|
||||||
|
if (opts.neId) {
|
||||||
|
q.set("ne_id", opts.neId);
|
||||||
|
} else if (opts.create) {
|
||||||
|
q.set("create", "1");
|
||||||
|
if (opts.name) q.set("name", opts.name);
|
||||||
|
if (opts.ip_address) q.set("ip_address", opts.ip_address);
|
||||||
|
if (opts.vendor) q.set("vendor", opts.vendor);
|
||||||
|
}
|
||||||
|
const qs = q.toString();
|
||||||
|
openOrFocusModule({
|
||||||
|
moduleId: "ne",
|
||||||
|
path: qs ? `/ne?${qs}` : "/ne",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const openWebcrtFor = (node: Node<NeNodeData> | null) => {
|
const openWebcrtFor = (node: Node<NeNodeData> | null) => {
|
||||||
closeCtxMenu();
|
closeCtxMenu();
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const managedId = String(node.data.managed_ne_id || "").trim();
|
const managedId = String(node.data.managed_ne_id || "").trim();
|
||||||
const umeId = String(node.data.ume_ne_id || "").trim();
|
const umeId = String(node.data.ume_ne_id || "").trim();
|
||||||
|
|
||||||
const openManaged = (neId: string) => {
|
|
||||||
openOrFocusModule({
|
|
||||||
moduleId: "webcrt",
|
|
||||||
path: `/webcrt?ne_id=${encodeURIComponent(neId)}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (managedId) {
|
if (managedId) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const ne = await fetchManagedNeById(managedId);
|
const ne = await fetchManagedNeById(managedId);
|
||||||
const src = String(ne.source || "").trim().toLowerCase();
|
const src = String(ne.source || "").trim().toLowerCase();
|
||||||
const hasIp = Boolean(String(ne.ip_address || "").trim());
|
const hasIp = Boolean(String(ne.ip_address || "").trim());
|
||||||
// Ready inventory / WebCRT hosts: reuse stored credentials.
|
// Ready inventory hosts: reuse stored credentials (incl. hop/proxy).
|
||||||
// LLDP placeholders (empty IP) and other incomplete rows need a form.
|
// LLDP placeholders / incomplete rows → full NE edit form (vendor, hop, etc.).
|
||||||
const needsSetup = !hasIp || src === "lldp";
|
const needsSetup = !hasIp || src === "lldp";
|
||||||
if (!needsSetup) {
|
if (!needsSetup) {
|
||||||
openManaged(managedId);
|
openOrFocusModule({
|
||||||
|
moduleId: "webcrt",
|
||||||
|
path: `/webcrt?ne_id=${encodeURIComponent(managedId)}`,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hintIp = String(ne.ip_address || ne.source_ref || node.data.ne_ip || "").trim();
|
showOk(t("topology.completeNeFirst"));
|
||||||
const proto = String(ne.protocol || "ssh").toLowerCase() === "telnet" ? "telnet" : "ssh";
|
openNeInventory({ neId: managedId });
|
||||||
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,
|
|
||||||
username: String(ne.username || "").trim(),
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(String(err));
|
showError(String(err));
|
||||||
}
|
}
|
||||||
|
|
@ -1926,103 +1908,22 @@ export function TopologyPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No inventory link — same as creating a managed NE, then open terminal.
|
// No inventory link — open NE create with topology hint fields.
|
||||||
setTermDialog({
|
showOk(t("topology.completeNeFirst"));
|
||||||
neId: "",
|
openNeInventory({
|
||||||
|
create: true,
|
||||||
name: String(node.data.label || "").trim(),
|
name: String(node.data.label || "").trim(),
|
||||||
vendor: String(node.data.vendor || "Other").trim() || "Other",
|
|
||||||
ip_address: String(node.data.ne_ip || "").trim(),
|
ip_address: String(node.data.ne_ip || "").trim(),
|
||||||
port: 22,
|
vendor: String(node.data.vendor || "").trim(),
|
||||||
protocol: "ssh",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitTermDialog = async () => {
|
|
||||||
if (!termDialog || termBusy) return;
|
|
||||||
const ip = termDialog.ip_address.trim();
|
|
||||||
if (!ip) {
|
|
||||||
showError(t("topology.termConnect.ipRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const protocol = termDialog.protocol;
|
|
||||||
const username = termDialog.username.trim();
|
|
||||||
if (protocol === "ssh" && !username) {
|
|
||||||
showError(t("topology.termConnect.userRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (protocol === "ssh" && !termDialog.password) {
|
|
||||||
showError(t("topology.termConnect.passwordRequired"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
username,
|
|
||||||
...(protocol === "ssh" ? { password: termDialog.password } : {}),
|
|
||||||
});
|
|
||||||
setNodes((ns) =>
|
|
||||||
ns.map((n) =>
|
|
||||||
String(n.data.managed_ne_id || "") === neId
|
|
||||||
? {
|
|
||||||
...n,
|
|
||||||
data: {
|
|
||||||
...n.data,
|
|
||||||
ne_ip: ip,
|
|
||||||
vendor,
|
|
||||||
label: name || n.data.label,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: n,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const created = await createManagedNe({
|
|
||||||
name,
|
|
||||||
vendor,
|
|
||||||
device_type: "generic",
|
|
||||||
ip_address: ip,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
username: username || (protocol === "telnet" ? "" : username),
|
|
||||||
password: protocol === "ssh" ? termDialog.password : "",
|
|
||||||
});
|
|
||||||
neId = created.id;
|
|
||||||
}
|
|
||||||
setTermDialog(null);
|
|
||||||
showOk(t("topology.termConnect.saved"));
|
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.managedNeAll });
|
|
||||||
openOrFocusModule({
|
|
||||||
moduleId: "webcrt",
|
|
||||||
path: `/webcrt?ne_id=${encodeURIComponent(neId)}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
showError(String(err));
|
|
||||||
} finally {
|
|
||||||
setTermBusy(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openNeFor = (node: Node<NeNodeData> | null) => {
|
const openNeFor = (node: Node<NeNodeData> | null) => {
|
||||||
closeCtxMenu();
|
closeCtxMenu();
|
||||||
const managedId = node?.data.managed_ne_id;
|
const managedId = String(node?.data.managed_ne_id || "").trim();
|
||||||
const umeId = node?.data.ume_ne_id;
|
const umeId = String(node?.data.ume_ne_id || "").trim();
|
||||||
if (managedId) {
|
if (managedId) {
|
||||||
openOrFocusModule({
|
openNeInventory({ neId: managedId });
|
||||||
moduleId: "ne",
|
|
||||||
path: "/ne",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (umeId) {
|
if (umeId) {
|
||||||
|
|
@ -3904,162 +3805,6 @@ export function TopologyPage() {
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{termDialog ? (
|
|
||||||
<div
|
|
||||||
className="modal-backdrop"
|
|
||||||
role="presentation"
|
|
||||||
onClick={() => {
|
|
||||||
if (!termBusy) setTermDialog(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="modal"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="topo-term-connect-title"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<h3 id="topo-term-connect-title">
|
|
||||||
{termDialog.neId ? t("topology.termConnect.title") : t("topology.termConnect.titleCreate")}
|
|
||||||
</h3>
|
|
||||||
<p className="form-hint">{t("topology.termConnect.hint")}</p>
|
|
||||||
<div className="form-grid">
|
|
||||||
<label>
|
|
||||||
<span className="form-label">{t("topology.termConnect.name")}</span>
|
|
||||||
<input
|
|
||||||
value={termDialog.name}
|
|
||||||
disabled={termBusy}
|
|
||||||
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")}
|
|
||||||
<span className="form-label__required" aria-hidden="true">
|
|
||||||
{" "}
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={termDialog.protocol}
|
|
||||||
disabled={termBusy}
|
|
||||||
onChange={(e) => {
|
|
||||||
const protocol = e.target.value === "telnet" ? "telnet" : "ssh";
|
|
||||||
setTermDialog({
|
|
||||||
...termDialog,
|
|
||||||
protocol,
|
|
||||||
port: protocol === "telnet" ? 23 : 22,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="ssh">SSH</option>
|
|
||||||
<option value="telnet">Telnet</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span className="form-label">
|
|
||||||
{t("topology.termConnect.ip")}
|
|
||||||
<span className="form-label__required" aria-hidden="true">
|
|
||||||
{" "}
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
value={termDialog.ip_address}
|
|
||||||
disabled={termBusy}
|
|
||||||
placeholder="192.168.1.1"
|
|
||||||
onChange={(e) => setTermDialog({ ...termDialog, ip_address: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span className="form-label">{t("topology.termConnect.port")}</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={termDialog.port}
|
|
||||||
disabled={termBusy}
|
|
||||||
onChange={(e) =>
|
|
||||||
setTermDialog({
|
|
||||||
...termDialog,
|
|
||||||
port: Number(e.target.value) || (termDialog.protocol === "telnet" ? 23 : 22),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{termDialog.protocol === "ssh" ? (
|
|
||||||
<>
|
|
||||||
<label>
|
|
||||||
<span className="form-label">
|
|
||||||
{t("topology.termConnect.username")}
|
|
||||||
<span className="form-label__required" aria-hidden="true">
|
|
||||||
{" "}
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
value={termDialog.username}
|
|
||||||
disabled={termBusy}
|
|
||||||
onChange={(e) => setTermDialog({ ...termDialog, username: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span className="form-label">
|
|
||||||
{t("topology.termConnect.password")}
|
|
||||||
<span className="form-label__required" aria-hidden="true">
|
|
||||||
{" "}
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={termDialog.password}
|
|
||||||
disabled={termBusy}
|
|
||||||
onChange={(e) => setTermDialog({ ...termDialog, password: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="modal__actions">
|
|
||||||
<button type="button" disabled={termBusy} onClick={() => setTermDialog(null)}>
|
|
||||||
{t("topology.termConnect.cancel")}
|
|
||||||
</button>
|
|
||||||
<button type="button" disabled={termBusy} onClick={() => void submitTermDialog()}>
|
|
||||||
{termBusy ? t("topology.termConnect.connecting") : t("topology.termConnect.connect")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue