mirror of
https://github.com/hansjone/netx.git
synced 2026-10-17 09:43:58 +08:00
Refactor web UI: feature-first structure, tokenized styles, modern blue theme.
Structure
- pages/ -> features/<domain>/{Page, components, hooks, lib}; 95 files moved,
333 imports rewritten, verified with tsc.
- services/api.ts -> services/api/<domain>.ts (250 exports preserved),
types.ts -> types/<domain>.ts, i18n zh/en -> locales/<lang>/<namespace>.ts
(2696 messages deep-equal before/after).
- ESLint rule: shared layers must not import features/.
Styles
- index.css (16k lines) split losslessly into styles/{base,layout,components,
pages,legacy,theme}; compiled CSS was byte-identical before tokenizing.
- ~930 hard-coded colors replaced by --nx-* tokens (theme/tokens.css); WebCRT
off-palette colors unified; fixed dark-on-dark text introduced by mapping.
- theme/theme.css: modern blue design language (rounded shapes, gradients,
unified buttons/fields/tables/modals), form-grid alignment fix, flat nav list
without tree indent, soft selected state instead of left accent bar.
Docs: WEB.md documents code structure and style rules.
Not touched: the large Biz*/Webcrt/Topology page components (to be split later).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b45d6abe3
commit
0d894efe5e
196 changed files with 26947 additions and 28091 deletions
|
|
@ -1,165 +0,0 @@
|
|||
/** Jump-host (hop) templates per vendor. */
|
||||
|
||||
import { ciscoHopTemplate, huaweiHopTemplate, isAutoHopTemplate, zteHopTemplate } from "./zteHop";
|
||||
|
||||
export type HopVendor = "zte" | "huawei" | "cisco" | "linux" | "bastion";
|
||||
|
||||
export type HopTargetAuthMode = "bastion_managed" | "manual";
|
||||
|
||||
export const HOP_VENDORS: HopVendor[] = ["zte", "huawei", "cisco", "linux", "bastion"];
|
||||
|
||||
export function bastionHopTemplate(): string {
|
||||
return "{hop_user}@{target_user}@{target_ip}";
|
||||
}
|
||||
|
||||
export const LEGACY_BASTION_HOP_TEMPLATE = "{hop_user}@{target_user}@{target_ip}@{hop_host}";
|
||||
|
||||
export function isBastionHopVendor(vendor: string): boolean {
|
||||
return String(vendor || "").toLowerCase() === "bastion";
|
||||
}
|
||||
|
||||
export function isLinuxHopVendor(vendor: string): boolean {
|
||||
return String(vendor || "").toLowerCase() === "linux";
|
||||
}
|
||||
|
||||
export function isCliHopVendor(vendor: string): boolean {
|
||||
const v = String(vendor || "").toLowerCase();
|
||||
return v === "zte" || v === "huawei" || v === "cisco";
|
||||
}
|
||||
|
||||
/** Strip ssh:// / trailing slash / :port from bastion host (IP or FQDN). */
|
||||
export function normalizeHopHost(value: string): string {
|
||||
let host = String(value || "").trim();
|
||||
if (!host) return "";
|
||||
host = host.replace(/^ssh(?:\s+-p\s+\d+)?\s+/i, "").trim();
|
||||
if (host.includes("://")) host = host.split("://", 2)[1] || host;
|
||||
host = host.trim().replace(/\/+$/, "");
|
||||
if (host.startsWith("[") && host.includes("]")) {
|
||||
const inside = host.slice(1, host.indexOf("]"));
|
||||
const rest = host.slice(host.indexOf("]") + 1);
|
||||
if (!rest || rest.startsWith(":")) return inside.trim();
|
||||
return host;
|
||||
}
|
||||
if ((host.match(/:/g) || []).length === 1) {
|
||||
const [left, right] = host.split(":");
|
||||
if (/^\d+$/.test(right || "") && left && !left.includes("@")) return left.trim();
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse OpenSSH-style bastion destination.
|
||||
* Example: bastion-user@target-user@198.51.100.20@ssh-bastion.example.com
|
||||
*/
|
||||
export function parseBastionSshDestination(value: string): {
|
||||
hop_host: string;
|
||||
hop_username: string;
|
||||
target_user: string;
|
||||
target_ip: string;
|
||||
ssh_username: string;
|
||||
} {
|
||||
let raw = String(value || "").trim().replace(/^["']|["']$/g, "");
|
||||
raw = raw.replace(/^ssh(?:\s+-p\s+\d+)?\s+/i, "").trim();
|
||||
if (!raw) {
|
||||
return { hop_host: "", hop_username: "", target_user: "", target_ip: "", ssh_username: "" };
|
||||
}
|
||||
if (!raw.includes("@")) {
|
||||
return {
|
||||
hop_host: normalizeHopHost(raw),
|
||||
hop_username: "",
|
||||
target_user: "",
|
||||
target_ip: "",
|
||||
ssh_username: "",
|
||||
};
|
||||
}
|
||||
const at = raw.lastIndexOf("@");
|
||||
const userPart = raw.slice(0, at).trim();
|
||||
const hopHost = normalizeHopHost(raw.slice(at + 1));
|
||||
const parts = userPart.split("@").filter(Boolean);
|
||||
return {
|
||||
hop_host: hopHost,
|
||||
hop_username: parts[0] || "",
|
||||
target_user: parts[1] || "",
|
||||
target_ip: parts[2] || "",
|
||||
ssh_username: userPart,
|
||||
};
|
||||
}
|
||||
|
||||
/** If hop_host is a pasted user@…@bastion string, split into host + hop username. */
|
||||
export function expandBastionHopFields(
|
||||
hopHost: string,
|
||||
hopUsername = "",
|
||||
): {
|
||||
hop_host: string;
|
||||
hop_username: string;
|
||||
target_user: string;
|
||||
target_ip: string;
|
||||
} {
|
||||
const curUser = String(hopUsername || "").trim();
|
||||
const raw = String(hopHost || "").trim();
|
||||
if (!raw.includes("@")) {
|
||||
return { hop_host: normalizeHopHost(raw), hop_username: curUser, target_user: "", target_ip: "" };
|
||||
}
|
||||
const parsed = parseBastionSshDestination(raw);
|
||||
return {
|
||||
hop_host: parsed.hop_host,
|
||||
hop_username: parsed.hop_username || curUser,
|
||||
target_user: parsed.target_user,
|
||||
target_ip: parsed.target_ip,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultHopTemplate(vendor: string, protocol: string, vrf: string): string {
|
||||
const v = String(vendor || "zte").toLowerCase();
|
||||
if (v === "huawei") return huaweiHopTemplate(protocol, vrf);
|
||||
if (v === "cisco") return ciscoHopTemplate(protocol, vrf);
|
||||
if (v === "linux") return "";
|
||||
if (v === "bastion") return bastionHopTemplate();
|
||||
return zteHopTemplate(protocol, vrf);
|
||||
}
|
||||
|
||||
export function patchHopVendorChange(
|
||||
vendor: HopVendor,
|
||||
prev: { hop_protocol: string; hop_vrf: string; hop_command_template: string; hop_vendor?: string },
|
||||
): {
|
||||
hop_vendor: HopVendor;
|
||||
hop_protocol: string;
|
||||
hop_vrf: string;
|
||||
hop_command_template: string;
|
||||
hop_port?: number;
|
||||
hop_target_auth_mode?: HopTargetAuthMode;
|
||||
hop_enter_system_view?: boolean;
|
||||
} {
|
||||
if (vendor === "linux") {
|
||||
return {
|
||||
hop_vendor: "linux",
|
||||
hop_protocol: "ssh",
|
||||
hop_vrf: "",
|
||||
hop_command_template: "",
|
||||
hop_enter_system_view: false,
|
||||
};
|
||||
}
|
||||
if (vendor === "bastion") {
|
||||
return {
|
||||
hop_vendor: "bastion",
|
||||
hop_protocol: "ssh",
|
||||
hop_port: 22,
|
||||
hop_vrf: "",
|
||||
hop_command_template: bastionHopTemplate(),
|
||||
hop_target_auth_mode: "bastion_managed" as HopTargetAuthMode,
|
||||
hop_enter_system_view: false,
|
||||
};
|
||||
}
|
||||
const protocol = prev.hop_protocol || "ssh";
|
||||
const vrf = prev.hop_vrf || "";
|
||||
return {
|
||||
hop_vendor: vendor,
|
||||
hop_protocol: protocol,
|
||||
hop_vrf: vrf,
|
||||
hop_command_template: defaultHopTemplate(vendor, protocol, vrf),
|
||||
// Explicit Huawei option only; reset when leaving/entering other CLI hop vendors.
|
||||
hop_enter_system_view: false,
|
||||
};
|
||||
}
|
||||
|
||||
export { ciscoHopTemplate, huaweiHopTemplate, isAutoHopTemplate, zteHopTemplate };
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { openNewModuleWindow } from "./moduleWindows";
|
||||
|
||||
export const PORT_TRAFFIC_WALL_MODULE_ID = "port-traffic-wall";
|
||||
|
||||
export function portTrafficBoardPath(boardId: string): string {
|
||||
return `/port-traffic/wall/${encodeURIComponent(boardId)}`;
|
||||
}
|
||||
|
||||
/** Open a dedicated browser tab for one board (outside Network management). */
|
||||
export function openPortTrafficBoardWindow(boardId: string): void {
|
||||
const id = String(boardId || "").trim();
|
||||
if (!id) return;
|
||||
openNewModuleWindow({
|
||||
moduleId: PORT_TRAFFIC_WALL_MODULE_ID,
|
||||
path: portTrafficBoardPath(id),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/** Map backend runtime status codes (rt:* / ws:*) and legacy Chinese to i18n keys. */
|
||||
|
||||
type TFunc = (key: string) => string;
|
||||
|
||||
const LEGACY_ZH_RUNTIME_ERROR: Record<string, string> = {
|
||||
"启动:正在同步当前告警(完成后连接 WSS)…": "ume.tasks.runtimeError.startup_alarm_sync_before_ws",
|
||||
"启动 REST 全量同步未完成,定时 REST 与 WSS 均待命": "ume.tasks.runtimeError.startup_gate_waiting",
|
||||
"WSS 实时接收中,已跳过 REST 同步": "ume.tasks.runtimeError.wss_active_skip_rest",
|
||||
"正在拉取 UME 当前告警…": "ume.tasks.runtimeError.pulling_alarms_current",
|
||||
"另一条当前告警 REST 同步进行中,已跳过": "ume.tasks.runtimeError.alarms_sync_in_progress_skip",
|
||||
"正在拉取 UME 网元清单…": "ume.tasks.runtimeError.pulling_inventory",
|
||||
"正在拉取 UME 拓扑(节点/链路)…": "ume.tasks.runtimeError.pulling_topology",
|
||||
"未启用或未配置 UME_BASE_URL": "ume.tasks.runtimeError.ume_ws_disabled_no_base_url",
|
||||
"已恢复:将跳过本轮周期等待并尽快同步": "ume.tasks.runtimeError.resumed_sync_soon",
|
||||
"已恢复:将尽快重连 WSS": "ume.tasks.runtimeError.resumed_wss_reconnect",
|
||||
已恢复: "ume.tasks.runtimeError.resumed",
|
||||
初始化: "ume.tasks.wsState.init",
|
||||
已连接: "ume.tasks.wsState.connected",
|
||||
连接中: "ume.tasks.wsState.connecting",
|
||||
已断开: "ume.tasks.wsState.disconnected",
|
||||
无订阅: "ume.tasks.wsState.no_subscription",
|
||||
"等待 token": "ume.tasks.wsState.waiting_token",
|
||||
已暂停: "ume.tasks.wsState.paused",
|
||||
连接异常: "ume.tasks.wsState.error",
|
||||
重连等待: "ume.tasks.wsState.reconnecting",
|
||||
UME订阅已丢失: "ume.tasks.wsState.subscription_lost",
|
||||
};
|
||||
|
||||
function translatePrefixed(code: string, prefix: string, i18nSection: string, t: TFunc): string | null {
|
||||
if (!code.startsWith(prefix)) return null;
|
||||
const slug = code.slice(prefix.length);
|
||||
if (!slug) return null;
|
||||
const key = `${i18nSection}.${slug}`;
|
||||
const tr = t(key);
|
||||
return tr !== key ? tr : null;
|
||||
}
|
||||
|
||||
export function runtimeLastError(raw: string | null | undefined, t: TFunc): string {
|
||||
const s = String(raw ?? "").trim();
|
||||
if (!s) return "";
|
||||
|
||||
const rt = translatePrefixed(s, "rt:", "ume.tasks.runtimeError", t);
|
||||
if (rt) return rt;
|
||||
const ws = translatePrefixed(s, "ws:", "ume.tasks.wsState", t);
|
||||
if (ws) return ws;
|
||||
|
||||
if (s === "keepalive_failed") {
|
||||
const tr = t("ume.tasks.runtimeError.keepalive_failed");
|
||||
if (tr !== "ume.tasks.runtimeError.keepalive_failed") return tr;
|
||||
}
|
||||
|
||||
const legacyKey = LEGACY_ZH_RUNTIME_ERROR[s];
|
||||
if (legacyKey) {
|
||||
const tr = t(legacyKey);
|
||||
if (tr !== legacyKey) return tr;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
export function wsConnectionLabel(
|
||||
state: string | null | undefined,
|
||||
label: string | null | undefined,
|
||||
t: TFunc,
|
||||
): string {
|
||||
const fromLabel = runtimeLastError(label, t);
|
||||
if (fromLabel && fromLabel !== String(label ?? "").trim()) return fromLabel;
|
||||
const st = String(state ?? "").trim();
|
||||
if (st) {
|
||||
const tr = runtimeLastError(`ws:${st}`, t);
|
||||
if (tr) return tr;
|
||||
}
|
||||
return fromLabel || st || "";
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
/** SecureCRT-like keyword highlighting for WebCRT stdout (ANSI inject). */
|
||||
|
||||
export type KeywordRule = {
|
||||
id: string;
|
||||
pattern: string;
|
||||
regex: boolean;
|
||||
};
|
||||
|
||||
export type KeywordHighlightConfig = {
|
||||
enabled: boolean;
|
||||
caseSensitive: boolean;
|
||||
/** Foreground (character) highlight color (hex). */
|
||||
color: string;
|
||||
keywords: KeywordRule[];
|
||||
};
|
||||
|
||||
export const KEYWORD_HL_STORAGE_KEY = "netx.webcrt.keywordHighlight";
|
||||
|
||||
const DEFAULT_COLOR = "#ffff00";
|
||||
|
||||
export function defaultKeywordHighlightConfig(): KeywordHighlightConfig {
|
||||
return {
|
||||
enabled: false,
|
||||
caseSensitive: false,
|
||||
color: DEFAULT_COLOR,
|
||||
keywords: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function newKeywordId(): string {
|
||||
return `kw_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function normalizeHexColor(hex: string): string | null {
|
||||
const m = /^#?([0-9a-fA-F]{6})$/.exec(String(hex || "").trim());
|
||||
return m ? `#${m[1].toLowerCase()}` : null;
|
||||
}
|
||||
|
||||
export function loadKeywordHighlightConfig(): KeywordHighlightConfig {
|
||||
const base = defaultKeywordHighlightConfig();
|
||||
try {
|
||||
const raw = localStorage.getItem(KEYWORD_HL_STORAGE_KEY);
|
||||
if (!raw) return base;
|
||||
const j = JSON.parse(raw) as Partial<KeywordHighlightConfig>;
|
||||
const keywords = Array.isArray(j.keywords)
|
||||
? j.keywords
|
||||
.map((k) => ({
|
||||
id: String((k as KeywordRule).id || newKeywordId()),
|
||||
pattern: String((k as KeywordRule).pattern || ""),
|
||||
regex: Boolean((k as KeywordRule).regex),
|
||||
}))
|
||||
.filter((k) => k.pattern.trim())
|
||||
: [];
|
||||
return {
|
||||
// Keywords imply active highlighting (avoids stale enabled:false in localStorage).
|
||||
enabled: keywords.length > 0,
|
||||
caseSensitive: Boolean(j.caseSensitive),
|
||||
color: normalizeHexColor(String(j.color || base.color)) || DEFAULT_COLOR,
|
||||
keywords,
|
||||
};
|
||||
} catch {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveKeywordHighlightConfig(cfg: KeywordHighlightConfig): void {
|
||||
const keywords = (cfg.keywords || []).filter((k) => String(k.pattern || "").trim());
|
||||
localStorage.setItem(
|
||||
KEYWORD_HL_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
...cfg,
|
||||
enabled: keywords.length > 0 && cfg.enabled !== false,
|
||||
keywords,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const n = normalizeHexColor(hex) || DEFAULT_COLOR;
|
||||
const v = parseInt(n.slice(1), 16);
|
||||
return { r: (v >> 16) & 255, g: (v >> 8) & 255, b: v & 255 };
|
||||
}
|
||||
|
||||
/** Map highlight color to bold foreground (character color), not background. */
|
||||
function sgrForColor(hex: string): { open: string; close: string } {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
// 16-color bright fg fallback + truecolor fg for xterm.
|
||||
let brightFg = 93; // bright yellow
|
||||
if (g >= r && g >= b && g > 80) brightFg = 92; // bright green
|
||||
else if (r >= g && r >= b && r > 80 && g < 100) brightFg = 91; // bright red
|
||||
else if (b >= r && b >= g && b > 80) brightFg = 94; // bright blue
|
||||
else if (r > 180 && g > 180 && b < 120) brightFg = 93; // yellow
|
||||
const open = `\x1b[1;${brightFg}m\x1b[38;2;${r};${g};${b}m`;
|
||||
const close = "\x1b[22;39m";
|
||||
return { open, close };
|
||||
}
|
||||
|
||||
type CompiledRule = {
|
||||
re: RegExp;
|
||||
open: string;
|
||||
close: string;
|
||||
};
|
||||
|
||||
export function compileKeywordRules(config: KeywordHighlightConfig | null | undefined): CompiledRule[] {
|
||||
if (!config?.keywords?.length) return [];
|
||||
if (!config.enabled) return [];
|
||||
const { open, close } = sgrForColor(config.color || DEFAULT_COLOR);
|
||||
const flags = config.caseSensitive ? "g" : "gi";
|
||||
const out: CompiledRule[] = [];
|
||||
for (const kw of config.keywords) {
|
||||
const pattern = String(kw.pattern || "").trim();
|
||||
if (!pattern) continue;
|
||||
try {
|
||||
const source = kw.regex ? pattern : escapeRegExp(pattern);
|
||||
out.push({ re: new RegExp(source, flags), open, close });
|
||||
} catch {
|
||||
/* skip invalid regex */
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** CSI / OSC / simple charset sequences — leave untouched. */
|
||||
const ANSI_RE =
|
||||
/\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[()][0-2AB]|[>=])/g;
|
||||
|
||||
function highlightPlain(text: string, rules: CompiledRule[]): string {
|
||||
if (!text || !rules.length) return text;
|
||||
type Match = { start: number; end: number; open: string; close: string };
|
||||
const matches: Match[] = [];
|
||||
for (const rule of rules) {
|
||||
rule.re.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = rule.re.exec(text)) !== null) {
|
||||
if (!m[0]) {
|
||||
rule.re.lastIndex += 1;
|
||||
continue;
|
||||
}
|
||||
matches.push({
|
||||
start: m.index,
|
||||
end: m.index + m[0].length,
|
||||
open: rule.open,
|
||||
close: rule.close,
|
||||
});
|
||||
if (!rule.re.global) break;
|
||||
}
|
||||
}
|
||||
if (!matches.length) return text;
|
||||
matches.sort((a, b) => a.start - b.start || b.end - a.end);
|
||||
const picked: Match[] = [];
|
||||
let cursor = 0;
|
||||
for (const m of matches) {
|
||||
if (m.start < cursor) continue;
|
||||
picked.push(m);
|
||||
cursor = m.end;
|
||||
}
|
||||
let out = "";
|
||||
let i = 0;
|
||||
for (const m of picked) {
|
||||
out += text.slice(i, m.start) + m.open + text.slice(m.start, m.end) + m.close;
|
||||
i = m.end;
|
||||
}
|
||||
out += text.slice(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function applyKeywordHighlight(
|
||||
text: string,
|
||||
config: KeywordHighlightConfig | null | undefined,
|
||||
): string {
|
||||
if (!text) return text;
|
||||
const rules = compileKeywordRules(config);
|
||||
if (!rules.length) return text;
|
||||
let out = "";
|
||||
let last = 0;
|
||||
ANSI_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = ANSI_RE.exec(text)) !== null) {
|
||||
if (m.index > last) out += highlightPlain(text.slice(last, m.index), rules);
|
||||
out += m[0];
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
if (last < text.length) out += highlightPlain(text.slice(last), rules);
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/** Vendor CLI jump command templates (placeholders). */
|
||||
|
||||
const LEGACY_HOP_TEMPLATES = new Set([
|
||||
"ssh {target_user}@{target_ip}",
|
||||
"ssh {target_ip}",
|
||||
"telnet {target_ip}",
|
||||
]);
|
||||
|
||||
export function zteHopTemplate(protocol: string, vrf: string): string {
|
||||
const cmd = String(protocol || "ssh").toLowerCase() === "telnet" ? "telnet" : "ssh";
|
||||
const v = String(vrf || "").trim();
|
||||
return v ? `${cmd} {target_ip} vrf {vrf}` : `${cmd} {target_ip}`;
|
||||
}
|
||||
|
||||
/** Cisco: ssh -vrf VRF IP; telnet IP [/vrf VRF]. */
|
||||
export function ciscoHopTemplate(protocol: string, vrf: string): string {
|
||||
const v = String(vrf || "").trim();
|
||||
if (String(protocol || "ssh").toLowerCase() === "telnet") {
|
||||
return v ? `telnet {target_ip} /vrf {vrf}` : `telnet {target_ip}`;
|
||||
}
|
||||
return v ? `ssh -vrf {vrf} {target_ip}` : `ssh {target_ip}`;
|
||||
}
|
||||
|
||||
/** Huawei: telnet [vpn-instance VRF] IP; SSH uses stelnet. */
|
||||
export function huaweiHopTemplate(protocol: string, vrf: string): string {
|
||||
const v = String(vrf || "").trim();
|
||||
if (String(protocol || "ssh").toLowerCase() === "telnet") {
|
||||
return v ? `telnet vpn-instance {vrf} {target_ip}` : `telnet {target_ip}`;
|
||||
}
|
||||
return v ? `stelnet {target_ip} -vpn-instance {vrf}` : `stelnet {target_ip}`;
|
||||
}
|
||||
|
||||
export function isAutoHopTemplate(
|
||||
template: string,
|
||||
vendor: string,
|
||||
protocol: string,
|
||||
vrf: string,
|
||||
): boolean {
|
||||
const t = String(template || "").trim();
|
||||
if (!t || LEGACY_HOP_TEMPLATES.has(t)) return true;
|
||||
const v = String(vendor || "zte").toLowerCase();
|
||||
if (v === "huawei") return t === huaweiHopTemplate(protocol, vrf);
|
||||
if (v === "cisco") return t === ciscoHopTemplate(protocol, vrf);
|
||||
if (v === "linux") return t === "";
|
||||
if (v === "bastion") {
|
||||
return (
|
||||
t === "{hop_user}@{target_user}@{target_ip}"
|
||||
|| t === "{hop_user}@{target_user}@{target_ip}@{hop_host}"
|
||||
);
|
||||
}
|
||||
return t === zteHopTemplate(protocol, vrf);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue