mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 03:30:49 +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
|
|
@ -7,6 +7,7 @@
|
|||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
} from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
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)));
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
return (
|
||||
<img
|
||||
className="topo-node__icon"
|
||||
src="/topo/ne-router.png"
|
||||
alt=""
|
||||
draggable={false}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
return <span className="topo-node__icon" aria-hidden="true" />;
|
||||
}
|
||||
|
||||
/** 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";
|
||||
/** Previous light default — migrate so existing sessions pick up dark canvas. */
|
||||
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 {
|
||||
try {
|
||||
|
|
@ -463,7 +496,7 @@ function loadCanvasBg(): string {
|
|||
persistCanvasBg(DEFAULT_CANVAS_BG);
|
||||
return DEFAULT_CANVAS_BG;
|
||||
}
|
||||
if (/^#[0-9a-fA-F]{6}$/i.test(raw)) return raw;
|
||||
if (isHexColor(raw)) return raw;
|
||||
} catch {
|
||||
/* 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 {
|
||||
const hex = String(bg || "").replace("#", "");
|
||||
if (hex.length !== 6) return "#93c5fd";
|
||||
|
|
@ -741,6 +822,8 @@ export function TopologyPage() {
|
|||
const [edgeFlow, setEdgeFlow] = useState(false);
|
||||
const [edgeDefaults, setEdgeDefaults] = useState<EdgeDefaults>(() => loadEdgeDefaults());
|
||||
const [canvasBg, setCanvasBg] = useState(loadCanvasBg);
|
||||
const [labelColors, setLabelColors] = useState<LabelColors>(() => loadLabelColors());
|
||||
const [vendorColors, setVendorColors] = useState<VendorColors>(() => loadVendorColors());
|
||||
const [toolMode, setToolMode] = useState<ToolMode>("select");
|
||||
const [snapToGrid, setSnapToGrid] = useState(true);
|
||||
const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(loadAutoLayoutAfterDiscover);
|
||||
|
|
@ -2900,16 +2983,122 @@ export function TopologyPage() {
|
|||
const raw = e.target.value.trim();
|
||||
if (!/^#[0-9a-fA-F]{0,6}$/.test(raw)) return;
|
||||
setCanvasBg(raw);
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(raw)) persistCanvasBg(raw.toLowerCase());
|
||||
if (isHexColor(raw)) persistCanvasBg(raw.toLowerCase());
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(canvasBg)) {
|
||||
if (!isHexColor(canvasBg)) {
|
||||
setCanvasBg(loadCanvasBg());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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__head">
|
||||
<strong>{t("topology.edgeDefaults")}</strong>
|
||||
|
|
@ -3327,7 +3516,23 @@ export function TopologyPage() {
|
|||
<div
|
||||
className={`topo-canvas${fullscreen ? " is-fullscreen" : ""}${toolMode === "pan" ? " is-pan-mode" : ""}`}
|
||||
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}
|
||||
onDrop={onCanvasDrop}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue