Offset cluster subnet labels with outward leader pipes.
Keep CIDR/VLAN pills outside the spoke ring so pulse edges no longer cover the IP text. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -222,12 +222,25 @@
|
|||||||
const nodes = [];
|
const nodes = [];
|
||||||
const hubs = [];
|
const hubs = [];
|
||||||
const R = Math.min(w, h) * 0.32;
|
const R = Math.min(w, h) * 0.32;
|
||||||
|
const subnetMeta = new Map((state.data?.subnets || []).map((s) => [s.cidr, s]));
|
||||||
groups.forEach((g, si) => {
|
groups.forEach((g, si) => {
|
||||||
const a = (si / Math.max(groups.length, 1)) * Math.PI * 2 - Math.PI / 2;
|
const a = (si / Math.max(groups.length, 1)) * Math.PI * 2 - Math.PI / 2;
|
||||||
const hx = cx + Math.cos(a) * R;
|
const hx = cx + Math.cos(a) * R;
|
||||||
const hy = cy + Math.sin(a) * R;
|
const hy = cy + Math.sin(a) * R;
|
||||||
hubs.push({ x: hx, y: hy, label: g.cidr, color: colorFor(state.subnetColors, g.cidr) });
|
|
||||||
const localR = 28 + Math.min(90, g.members.length * 4.5);
|
const localR = 28 + Math.min(90, g.members.length * 4.5);
|
||||||
|
const meta = subnetMeta.get(g.cidr) || {};
|
||||||
|
const vlanBit = meta.vlan_id != null ? `VLAN ${meta.vlan_id}` : null;
|
||||||
|
const label = vlanBit ? `${vlanBit} · ${g.cidr}` : g.cidr;
|
||||||
|
hubs.push({
|
||||||
|
x: hx,
|
||||||
|
y: hy,
|
||||||
|
label,
|
||||||
|
cidr: g.cidr,
|
||||||
|
color: meta.vlan_color || colorFor(state.subnetColors, g.cidr),
|
||||||
|
localR,
|
||||||
|
angle: a,
|
||||||
|
memberCount: g.members.length,
|
||||||
|
});
|
||||||
g.members.forEach((n, i) => {
|
g.members.forEach((n, i) => {
|
||||||
const la = (i / Math.max(g.members.length, 1)) * Math.PI * 2;
|
const la = (i / Math.max(g.members.length, 1)) * Math.PI * 2;
|
||||||
nodes.push({
|
nodes.push({
|
||||||
@@ -492,8 +505,9 @@
|
|||||||
}
|
}
|
||||||
} else if (mode === "clusters") {
|
} else if (mode === "clusters") {
|
||||||
for (const ch of state.clusterHubs || []) {
|
for (const ch of state.clusterHubs || []) {
|
||||||
|
const ring = (ch.localR || 52) + 10;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(ch.x, ch.y, 52, 0, Math.PI * 2);
|
ctx.arc(ch.x, ch.y, ring, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = (ch.color || "#00a8e8") + "33";
|
ctx.strokeStyle = (ch.color || "#00a8e8") + "33";
|
||||||
ctx.lineWidth = 1.2;
|
ctx.lineWidth = 1.2;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
@@ -503,10 +517,6 @@
|
|||||||
ctx.globalAlpha = 0.55;
|
ctx.globalAlpha = 0.55;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.fillStyle = canvasTheme().clusterLabel;
|
|
||||||
ctx.font = "600 9px IBM Plex Mono, monospace";
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.fillText((ch.label || "").slice(0, 18), ch.x, ch.y - 18);
|
|
||||||
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
|
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
|
||||||
}
|
}
|
||||||
} else if (mode === "lanes") {
|
} else if (mode === "lanes") {
|
||||||
@@ -549,6 +559,88 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawClusterLabelPipes(omeHub) {
|
||||||
|
const hubs = state.clusterHubs || [];
|
||||||
|
if (!hubs.length || !omeHub) return;
|
||||||
|
const th = canvasTheme();
|
||||||
|
const light = isLightTheme();
|
||||||
|
|
||||||
|
for (const ch of hubs) {
|
||||||
|
const label = String(ch.label || ch.cidr || "").trim();
|
||||||
|
if (!label) continue;
|
||||||
|
|
||||||
|
// Radial outward from OME → cluster, then past the spoke ring ("long pipe")
|
||||||
|
let dx = ch.x - omeHub.x;
|
||||||
|
let dy = ch.y - omeHub.y;
|
||||||
|
let len = Math.hypot(dx, dy);
|
||||||
|
if (len < 1) {
|
||||||
|
dx = 0;
|
||||||
|
dy = -1;
|
||||||
|
len = 1;
|
||||||
|
}
|
||||||
|
const ux = dx / len;
|
||||||
|
const uy = dy / len;
|
||||||
|
const ring = ch.localR || 52;
|
||||||
|
const pipeStart = 12;
|
||||||
|
const pipeEnd = ring + 36;
|
||||||
|
const sx = ch.x + ux * pipeStart;
|
||||||
|
const sy = ch.y + uy * pipeStart;
|
||||||
|
const ex = ch.x + ux * pipeEnd;
|
||||||
|
const ey = ch.y + uy * pipeEnd;
|
||||||
|
// Label sits a bit further out past the pipe tip
|
||||||
|
const lx = ch.x + ux * (pipeEnd + 14);
|
||||||
|
const ly = ch.y + uy * (pipeEnd + 14);
|
||||||
|
|
||||||
|
const color = ch.color || "#00a8e8";
|
||||||
|
const rgb = hexToRgb(color);
|
||||||
|
|
||||||
|
// Long pipe / leader
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.55)`;
|
||||||
|
ctx.lineWidth = 1.6;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(sx, sy);
|
||||||
|
ctx.lineTo(ex, ey);
|
||||||
|
ctx.stroke();
|
||||||
|
// tip tick
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ex, ey, 2.4, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.9)`;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Pill label (drawn last so it sits above spokes)
|
||||||
|
ctx.font = "600 10px IBM Plex Mono, monospace";
|
||||||
|
const tw = ctx.measureText(label).width;
|
||||||
|
const padX = 8;
|
||||||
|
const padY = 5;
|
||||||
|
const bw = tw + padX * 2;
|
||||||
|
const bh = 18;
|
||||||
|
const bx = lx - bw / 2;
|
||||||
|
const by = ly - bh / 2;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
const r = 6;
|
||||||
|
ctx.moveTo(bx + r, by);
|
||||||
|
ctx.arcTo(bx + bw, by, bx + bw, by + bh, r);
|
||||||
|
ctx.arcTo(bx + bw, by + bh, bx, by + bh, r);
|
||||||
|
ctx.arcTo(bx, by + bh, bx, by, r);
|
||||||
|
ctx.arcTo(bx, by, bx + bw, by, r);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = light ? "rgba(255,255,255,0.92)" : "rgba(6,14,24,0.88)";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.75)`;
|
||||||
|
ctx.lineWidth = 1.2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = th.clusterLabel;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(label, lx, ly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawLinks(hub, t) {
|
function drawLinks(hub, t) {
|
||||||
const mode = state.layoutMode;
|
const mode = state.layoutMode;
|
||||||
if (mode === "clusters") {
|
if (mode === "clusters") {
|
||||||
@@ -672,6 +764,7 @@
|
|||||||
drawLinks(hub, t);
|
drawLinks(hub, t);
|
||||||
drawHub(hub, t);
|
drawHub(hub, t);
|
||||||
drawNodes(t);
|
drawNodes(t);
|
||||||
|
if (state.layoutMode === "clusters") drawClusterLabelPipes(hub);
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -633,7 +633,7 @@
|
|||||||
|
|
||||||
<script src="/vendor/xterm/xterm.min.js?v=home1"></script>
|
<script src="/vendor/xterm/xterm.min.js?v=home1"></script>
|
||||||
<script src="/vendor/xterm/xterm-addon-fit.min.js?v=home1"></script>
|
<script src="/vendor/xterm/xterm-addon-fit.min.js?v=home1"></script>
|
||||||
<script src="/app.js?v=vlanall1"></script>
|
<script src="/app.js?v=clusterpipe1"></script>
|
||||||
<script src="/ops.js?v=opsfde2"></script>
|
<script src="/ops.js?v=opsfde2"></script>
|
||||||
<script src="/ssh.js?v=fabric4"></script>
|
<script src="/ssh.js?v=fabric4"></script>
|
||||||
<script src="/rdp.js?v=fabric4"></script>
|
<script src="/rdp.js?v=fabric4"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user