mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 19:57:29 +08:00
Add topology display pickers for label, edge-number, and vendor icon colors.
Persist choices in localStorage, apply via canvas CSS variables, and tint router icons with mask fills so each vendor color is editable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4a0918ab44
commit
65afe8f29f
4 changed files with 288 additions and 51 deletions
|
|
@ -1466,7 +1466,25 @@ const en = {
|
||||||
display: "Display",
|
display: "Display",
|
||||||
canvasBg: "Canvas background",
|
canvasBg: "Canvas background",
|
||||||
canvasBgColor: "Background color",
|
canvasBgColor: "Background color",
|
||||||
canvasBgReset: "Reset light blue",
|
canvasBgReset: "Reset default",
|
||||||
|
textColors: "Text colors",
|
||||||
|
textColorsReset: "Reset default",
|
||||||
|
labelColor: "NE name",
|
||||||
|
edgeLabelColor: "Link numbers",
|
||||||
|
vendorColors: "Vendor icon colors",
|
||||||
|
vendorColorsReset: "Reset default",
|
||||||
|
vendorTone: {
|
||||||
|
cisco: "Cisco",
|
||||||
|
huawei: "Huawei",
|
||||||
|
zte: "ZTE",
|
||||||
|
juniper: "Juniper",
|
||||||
|
nokia: "Nokia",
|
||||||
|
ericsson: "Ericsson",
|
||||||
|
h3c: "H3C",
|
||||||
|
ruijie: "Ruijie",
|
||||||
|
mikrotik: "MikroTik",
|
||||||
|
gray: "Unknown / unmanaged",
|
||||||
|
},
|
||||||
hideIp: "Hide IP",
|
hideIp: "Hide IP",
|
||||||
hideVendor: "Hide vendor",
|
hideVendor: "Hide vendor",
|
||||||
hidePorts: "Hide ports",
|
hidePorts: "Hide ports",
|
||||||
|
|
|
||||||
|
|
@ -1460,7 +1460,25 @@ const zh = {
|
||||||
display: "显示",
|
display: "显示",
|
||||||
canvasBg: "画布背景",
|
canvasBg: "画布背景",
|
||||||
canvasBgColor: "背景色",
|
canvasBgColor: "背景色",
|
||||||
canvasBgReset: "恢复浅蓝",
|
canvasBgReset: "恢复默认",
|
||||||
|
textColors: "文字颜色",
|
||||||
|
textColorsReset: "恢复默认",
|
||||||
|
labelColor: "网元名称",
|
||||||
|
edgeLabelColor: "链路数字",
|
||||||
|
vendorColors: "厂商图标色",
|
||||||
|
vendorColorsReset: "恢复默认",
|
||||||
|
vendorTone: {
|
||||||
|
cisco: "Cisco",
|
||||||
|
huawei: "华为",
|
||||||
|
zte: "中兴",
|
||||||
|
juniper: "Juniper",
|
||||||
|
nokia: "Nokia",
|
||||||
|
ericsson: "Ericsson",
|
||||||
|
h3c: "H3C",
|
||||||
|
ruijie: "锐捷",
|
||||||
|
mikrotik: "MikroTik",
|
||||||
|
gray: "未知/未托管",
|
||||||
|
},
|
||||||
hideIp: "隐藏 IP",
|
hideIp: "隐藏 IP",
|
||||||
hideVendor: "隐藏厂商",
|
hideVendor: "隐藏厂商",
|
||||||
hidePorts: "隐藏端口",
|
hidePorts: "隐藏端口",
|
||||||
|
|
|
||||||
|
|
@ -6877,7 +6877,7 @@ pre {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: var(--topo-canvas-bg, #0f172a);
|
background: var(--topo-canvas-bg, #0f172a);
|
||||||
--xy-edge-label-background-color: transparent;
|
--xy-edge-label-background-color: transparent;
|
||||||
--xy-edge-label-color: #e2e8f0;
|
--xy-edge-label-color: var(--topo-edge-label-color, #e2e8f0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-canvas:fullscreen,
|
.topo-canvas:fullscreen,
|
||||||
|
|
@ -7023,50 +7023,39 @@ pre {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* Base asset /topo/ne-router.png is navy-blue; tints approximate brand hues. */
|
--topo-icon-color: var(--topo-vendor-gray, #94a3b8);
|
||||||
--topo-icon-filter: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cisco — cyan / light blue (#049FD9) */
|
|
||||||
.topo-node--cisco {
|
.topo-node--cisco {
|
||||||
--topo-icon-filter: hue-rotate(-18deg) saturate(1.25) brightness(1.08);
|
--topo-icon-color: var(--topo-vendor-cisco, #049fd9);
|
||||||
}
|
}
|
||||||
/* Huawei — red (#CF0A2C); was incorrectly greenish at 155deg */
|
|
||||||
.topo-node--huawei {
|
.topo-node--huawei {
|
||||||
--topo-icon-filter: hue-rotate(128deg) saturate(1.85) brightness(1.02);
|
--topo-icon-color: var(--topo-vendor-huawei, #cf0a2c);
|
||||||
}
|
}
|
||||||
/* ZTE — keep navy-blue base (#0091DA family) */
|
|
||||||
.topo-node--zte {
|
.topo-node--zte {
|
||||||
--topo-icon-filter: saturate(1.15) brightness(1.02);
|
--topo-icon-color: var(--topo-vendor-zte, #0091da);
|
||||||
}
|
}
|
||||||
/* Juniper — green (#84B135) */
|
|
||||||
.topo-node--juniper {
|
.topo-node--juniper {
|
||||||
--topo-icon-filter: hue-rotate(78deg) saturate(1.35) brightness(1.05);
|
--topo-icon-color: var(--topo-vendor-juniper, #84b135);
|
||||||
}
|
}
|
||||||
/* Nokia / Alcatel — deep blue-purple (#124191) */
|
|
||||||
.topo-node--nokia {
|
.topo-node--nokia {
|
||||||
--topo-icon-filter: hue-rotate(-35deg) saturate(1.2) brightness(0.92);
|
--topo-icon-color: var(--topo-vendor-nokia, #124191);
|
||||||
}
|
}
|
||||||
/* Ericsson — dark navy */
|
|
||||||
.topo-node--ericsson {
|
.topo-node--ericsson {
|
||||||
--topo-icon-filter: hue-rotate(-8deg) saturate(1.1) brightness(0.82);
|
--topo-icon-color: var(--topo-vendor-ericsson, #1e3a5f);
|
||||||
}
|
}
|
||||||
/* H3C — green (#7AC143) */
|
|
||||||
.topo-node--h3c {
|
.topo-node--h3c {
|
||||||
--topo-icon-filter: hue-rotate(88deg) saturate(1.45) brightness(1.06);
|
--topo-icon-color: var(--topo-vendor-h3c, #7ac143);
|
||||||
}
|
}
|
||||||
/* Ruijie — purple */
|
|
||||||
.topo-node--ruijie {
|
.topo-node--ruijie {
|
||||||
--topo-icon-filter: hue-rotate(45deg) saturate(1.4) brightness(1.0);
|
--topo-icon-color: var(--topo-vendor-ruijie, #7c3aed);
|
||||||
}
|
}
|
||||||
/* MikroTik — red-orange */
|
|
||||||
.topo-node--mikrotik {
|
.topo-node--mikrotik {
|
||||||
--topo-icon-filter: hue-rotate(145deg) saturate(1.7) brightness(1.05);
|
--topo-icon-color: var(--topo-vendor-mikrotik, #ea580c);
|
||||||
}
|
}
|
||||||
/* Unmanaged / unknown vendor */
|
|
||||||
.topo-node--gray,
|
.topo-node--gray,
|
||||||
.topo-node--other {
|
.topo-node--other {
|
||||||
--topo-icon-filter: grayscale(1) brightness(0.92) contrast(0.95);
|
--topo-icon-color: var(--topo-vendor-gray, #94a3b8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-node__glyph {
|
.topo-node__glyph {
|
||||||
|
|
@ -7125,16 +7114,15 @@ pre {
|
||||||
|
|
||||||
.topo-node.is-selected .topo-node__icon {
|
.topo-node.is-selected .topo-node__icon {
|
||||||
transform: none;
|
transform: none;
|
||||||
filter: var(--topo-icon-filter, none);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-node.is-selected .topo-node__caption {
|
.topo-node.is-selected .topo-node__caption {
|
||||||
color: #f8fafc;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-node.is-selected .topo-node__caption-name {
|
.topo-node.is-selected .topo-node__caption-name {
|
||||||
color: #93c5fd;
|
color: var(--topo-label-color, #f1f5f9);
|
||||||
|
filter: brightness(1.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selected link: light highlight only (do not alter geometry / arrows). */
|
/* Selected link: light highlight only (do not alter geometry / arrows). */
|
||||||
|
|
@ -7148,11 +7136,18 @@ pre {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
object-fit: contain;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
filter: var(--topo-icon-filter, none);
|
background-color: var(--topo-icon-color, #3b82f6);
|
||||||
transition: transform 0.12s ease, filter 0.12s ease;
|
-webkit-mask-image: url(/topo/ne-router.png);
|
||||||
|
mask-image: url(/topo/ne-router.png);
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
mask-size: contain;
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-position: center;
|
||||||
|
mask-position: center;
|
||||||
|
transition: transform 0.12s ease, background-color 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-node__caption {
|
.topo-node__caption {
|
||||||
|
|
@ -7161,7 +7156,7 @@ pre {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: #e2e8f0;
|
color: var(--topo-label-color, #f1f5f9);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -7175,7 +7170,7 @@ pre {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #f1f5f9;
|
color: var(--topo-label-color, #f1f5f9);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
@ -7185,7 +7180,7 @@ pre {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #94a3b8;
|
color: color-mix(in srgb, var(--topo-label-color, #f1f5f9) 55%, #64748b);
|
||||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -7198,7 +7193,7 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-canvas .react-flow__edge-text {
|
.topo-canvas .react-flow__edge-text {
|
||||||
fill: #e2e8f0;
|
fill: var(--topo-edge-label-color, #e2e8f0);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
paint-order: stroke;
|
paint-order: stroke;
|
||||||
stroke: rgba(15, 23, 42, 0.85);
|
stroke: rgba(15, 23, 42, 0.85);
|
||||||
|
|
@ -7207,7 +7202,7 @@ pre {
|
||||||
|
|
||||||
.topo-edge-label {
|
.topo-edge-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #e2e8f0;
|
color: var(--topo-edge-label-color, #e2e8f0);
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
@ -7220,8 +7215,9 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-edge-label.is-selected {
|
.topo-edge-label.is-selected {
|
||||||
color: #93c5fd;
|
color: var(--topo-edge-label-color, #e2e8f0);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
filter: brightness(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-ctx__member-list {
|
.topo-ctx__member-list {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
type CSSProperties,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
@ -370,17 +371,9 @@ function nodeMatchesQuery(n: Node<NeNodeData>, query: string): boolean {
|
||||||
return tokens.every((tok) => bits.some((b) => fuzzyIncludes(String(b || ""), tok)));
|
return tokens.every((tok) => bits.some((b) => fuzzyIncludes(String(b || ""), tok)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Asset: /topo/ne-router.png (navy blue base); vendors tint via CSS filters. */
|
/** Silhouette via CSS mask; fill color comes from --topo-icon-color (vendor palette). */
|
||||||
function RouterIcon() {
|
function RouterIcon() {
|
||||||
return (
|
return <span className="topo-node__icon" aria-hidden="true" />;
|
||||||
<img
|
|
||||||
className="topo-node__icon"
|
|
||||||
src="/topo/ne-router.png"
|
|
||||||
alt=""
|
|
||||||
draggable={false}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fixed box for onlyRenderVisibleElements (xyflow skips off-screen mount when sized + handles set). */
|
/** Fixed box for onlyRenderVisibleElements (xyflow skips off-screen mount when sized + handles set). */
|
||||||
|
|
@ -455,6 +448,46 @@ const CANVAS_BG_KEY = "netx.topology.canvasBg";
|
||||||
const DEFAULT_CANVAS_BG = "#0f172a";
|
const DEFAULT_CANVAS_BG = "#0f172a";
|
||||||
/** Previous light default — migrate so existing sessions pick up dark canvas. */
|
/** Previous light default — migrate so existing sessions pick up dark canvas. */
|
||||||
const LEGACY_CANVAS_BG = "#dbeafe";
|
const LEGACY_CANVAS_BG = "#dbeafe";
|
||||||
|
const LABEL_COLORS_KEY = "netx.topology.labelColors";
|
||||||
|
const VENDOR_COLORS_KEY = "netx.topology.vendorColors";
|
||||||
|
|
||||||
|
const VENDOR_TONE_KEYS = [
|
||||||
|
"cisco",
|
||||||
|
"huawei",
|
||||||
|
"zte",
|
||||||
|
"juniper",
|
||||||
|
"nokia",
|
||||||
|
"ericsson",
|
||||||
|
"h3c",
|
||||||
|
"ruijie",
|
||||||
|
"mikrotik",
|
||||||
|
"gray",
|
||||||
|
] as const;
|
||||||
|
type VendorToneKey = (typeof VENDOR_TONE_KEYS)[number];
|
||||||
|
type VendorColors = Record<VendorToneKey, string>;
|
||||||
|
type LabelColors = { name: string; edgeLabel: string };
|
||||||
|
|
||||||
|
const DEFAULT_LABEL_COLORS: LabelColors = {
|
||||||
|
name: "#f1f5f9",
|
||||||
|
edgeLabel: "#e2e8f0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_VENDOR_COLORS: VendorColors = {
|
||||||
|
cisco: "#049fd9",
|
||||||
|
huawei: "#cf0a2c",
|
||||||
|
zte: "#0091da",
|
||||||
|
juniper: "#84b135",
|
||||||
|
nokia: "#124191",
|
||||||
|
ericsson: "#1e3a5f",
|
||||||
|
h3c: "#7ac143",
|
||||||
|
ruijie: "#7c3aed",
|
||||||
|
mikrotik: "#ea580c",
|
||||||
|
gray: "#94a3b8",
|
||||||
|
};
|
||||||
|
|
||||||
|
function isHexColor(value: string): boolean {
|
||||||
|
return /^#[0-9a-fA-F]{6}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
function loadCanvasBg(): string {
|
function loadCanvasBg(): string {
|
||||||
try {
|
try {
|
||||||
|
|
@ -463,7 +496,7 @@ function loadCanvasBg(): string {
|
||||||
persistCanvasBg(DEFAULT_CANVAS_BG);
|
persistCanvasBg(DEFAULT_CANVAS_BG);
|
||||||
return DEFAULT_CANVAS_BG;
|
return DEFAULT_CANVAS_BG;
|
||||||
}
|
}
|
||||||
if (/^#[0-9a-fA-F]{6}$/i.test(raw)) return raw;
|
if (isHexColor(raw)) return raw;
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|
@ -478,6 +511,54 @@ function persistCanvasBg(value: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadLabelColors(): LabelColors {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LABEL_COLORS_KEY);
|
||||||
|
if (!raw) return { ...DEFAULT_LABEL_COLORS };
|
||||||
|
const parsed = JSON.parse(raw) as Partial<LabelColors>;
|
||||||
|
return {
|
||||||
|
name: isHexColor(String(parsed.name || "")) ? String(parsed.name).toLowerCase() : DEFAULT_LABEL_COLORS.name,
|
||||||
|
edgeLabel: isHexColor(String(parsed.edgeLabel || ""))
|
||||||
|
? String(parsed.edgeLabel).toLowerCase()
|
||||||
|
: DEFAULT_LABEL_COLORS.edgeLabel,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_LABEL_COLORS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLabelColors(value: LabelColors) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LABEL_COLORS_KEY, JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVendorColors(): VendorColors {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(VENDOR_COLORS_KEY);
|
||||||
|
if (!raw) return { ...DEFAULT_VENDOR_COLORS };
|
||||||
|
const parsed = JSON.parse(raw) as Partial<VendorColors>;
|
||||||
|
const out = { ...DEFAULT_VENDOR_COLORS };
|
||||||
|
for (const key of VENDOR_TONE_KEYS) {
|
||||||
|
const c = String(parsed?.[key] || "").trim().toLowerCase();
|
||||||
|
if (isHexColor(c)) out[key] = c;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_VENDOR_COLORS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistVendorColors(value: VendorColors) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(VENDOR_COLORS_KEY, JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function canvasDotColor(bg: string): string {
|
function canvasDotColor(bg: string): string {
|
||||||
const hex = String(bg || "").replace("#", "");
|
const hex = String(bg || "").replace("#", "");
|
||||||
if (hex.length !== 6) return "#93c5fd";
|
if (hex.length !== 6) return "#93c5fd";
|
||||||
|
|
@ -741,6 +822,8 @@ export function TopologyPage() {
|
||||||
const [edgeFlow, setEdgeFlow] = useState(false);
|
const [edgeFlow, setEdgeFlow] = useState(false);
|
||||||
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
||||||
const [canvasBg, setCanvasBg] = useState(loadCanvasBg);
|
const [canvasBg, setCanvasBg] = useState(loadCanvasBg);
|
||||||
|
const [labelColors, setLabelColors] = useState<LabelColors>(() => loadLabelColors());
|
||||||
|
const [vendorColors, setVendorColors] = useState<VendorColors>(() => loadVendorColors());
|
||||||
const [toolMode, setToolMode] = useState<ToolMode>("select");
|
const [toolMode, setToolMode] = useState<ToolMode>("select");
|
||||||
const [snapToGrid, setSnapToGrid] = useState(true);
|
const [snapToGrid, setSnapToGrid] = useState(true);
|
||||||
const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(loadAutoLayoutAfterDiscover);
|
const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(loadAutoLayoutAfterDiscover);
|
||||||
|
|
@ -2900,16 +2983,122 @@ export function TopologyPage() {
|
||||||
const raw = e.target.value.trim();
|
const raw = e.target.value.trim();
|
||||||
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
||||||
setCanvasBg(raw);
|
setCanvasBg(raw);
|
||||||
if (/^#[0-9a-fA-F]{6}$/.test(raw)) persistCanvasBg(raw.toLowerCase());
|
if (isHexColor(raw)) persistCanvasBg(raw.toLowerCase());
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (!/^#[0-9a-fA-F]{6}$/.test(canvasBg)) {
|
if (!isHexColor(canvasBg)) {
|
||||||
setCanvasBg(loadCanvasBg());
|
setCanvasBg(loadCanvasBg());
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="topo-display-defaults topo-display-defaults--canvas-bg">
|
||||||
|
<div className="topo-display-defaults__head">
|
||||||
|
<strong>{t("topology.textColors")}</strong>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn--sm btn--ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setLabelColors({ ...DEFAULT_LABEL_COLORS });
|
||||||
|
persistLabelColors({ ...DEFAULT_LABEL_COLORS });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("topology.textColorsReset")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["name", t("topology.labelColor")],
|
||||||
|
["edgeLabel", t("topology.edgeLabelColor")],
|
||||||
|
] as const
|
||||||
|
).map(([key, label]) => (
|
||||||
|
<div key={key} className="topo-display-defaults__row">
|
||||||
|
<span className="topo-display-defaults__name">{label}</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={labelColors[key]}
|
||||||
|
title={label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = { ...labelColors, [key]: e.target.value.toLowerCase() };
|
||||||
|
setLabelColors(next);
|
||||||
|
persistLabelColors(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="topo-toolbar__select topo-canvas-bg-hex"
|
||||||
|
value={labelColors[key]}
|
||||||
|
spellCheck={false}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value.trim();
|
||||||
|
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
||||||
|
const next = { ...labelColors, [key]: raw };
|
||||||
|
setLabelColors(next);
|
||||||
|
if (isHexColor(raw)) persistLabelColors({ ...next, [key]: raw.toLowerCase() });
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!isHexColor(labelColors[key])) {
|
||||||
|
setLabelColors(loadLabelColors());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="topo-display-defaults topo-display-defaults--canvas-bg">
|
||||||
|
<div className="topo-display-defaults__head">
|
||||||
|
<strong>{t("topology.vendorColors")}</strong>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn--sm btn--ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setVendorColors({ ...DEFAULT_VENDOR_COLORS });
|
||||||
|
persistVendorColors({ ...DEFAULT_VENDOR_COLORS });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("topology.vendorColorsReset")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{VENDOR_TONE_KEYS.map((key) => {
|
||||||
|
const label = t(`topology.vendorTone.${key}`);
|
||||||
|
return (
|
||||||
|
<div key={key} className="topo-display-defaults__row">
|
||||||
|
<span className="topo-display-defaults__name">{label}</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={vendorColors[key]}
|
||||||
|
title={label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = { ...vendorColors, [key]: e.target.value.toLowerCase() };
|
||||||
|
setVendorColors(next);
|
||||||
|
persistVendorColors(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="topo-toolbar__select topo-canvas-bg-hex"
|
||||||
|
value={vendorColors[key]}
|
||||||
|
spellCheck={false}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value.trim();
|
||||||
|
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
||||||
|
const next = { ...vendorColors, [key]: raw };
|
||||||
|
setVendorColors(next);
|
||||||
|
if (isHexColor(raw)) {
|
||||||
|
persistVendorColors({ ...next, [key]: raw.toLowerCase() });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!isHexColor(vendorColors[key])) {
|
||||||
|
setVendorColors(loadVendorColors());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<div className="topo-display-defaults">
|
<div className="topo-display-defaults">
|
||||||
<div className="topo-display-defaults__head">
|
<div className="topo-display-defaults__head">
|
||||||
<strong>{t("topology.edgeDefaults")}</strong>
|
<strong>{t("topology.edgeDefaults")}</strong>
|
||||||
|
|
@ -3327,7 +3516,23 @@ export function TopologyPage() {
|
||||||
<div
|
<div
|
||||||
className={`topo-canvas${fullscreen ? " is-fullscreen" : ""}${toolMode === "pan" ? " is-pan-mode" : ""}`}
|
className={`topo-canvas${fullscreen ? " is-fullscreen" : ""}${toolMode === "pan" ? " is-pan-mode" : ""}`}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
style={{ ["--topo-canvas-bg" as string]: canvasBg }}
|
style={
|
||||||
|
{
|
||||||
|
"--topo-canvas-bg": canvasBg,
|
||||||
|
"--topo-label-color": labelColors.name,
|
||||||
|
"--topo-edge-label-color": labelColors.edgeLabel,
|
||||||
|
"--topo-vendor-cisco": vendorColors.cisco,
|
||||||
|
"--topo-vendor-huawei": vendorColors.huawei,
|
||||||
|
"--topo-vendor-zte": vendorColors.zte,
|
||||||
|
"--topo-vendor-juniper": vendorColors.juniper,
|
||||||
|
"--topo-vendor-nokia": vendorColors.nokia,
|
||||||
|
"--topo-vendor-ericsson": vendorColors.ericsson,
|
||||||
|
"--topo-vendor-h3c": vendorColors.h3c,
|
||||||
|
"--topo-vendor-ruijie": vendorColors.ruijie,
|
||||||
|
"--topo-vendor-mikrotik": vendorColors.mikrotik,
|
||||||
|
"--topo-vendor-gray": vendorColors.gray,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
onDragOver={onCanvasDragOver}
|
onDragOver={onCanvasDragOver}
|
||||||
onDrop={onCanvasDrop}
|
onDrop={onCanvasDrop}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue