refactor(NetworkMapUnifi): improve layout computation by grouping server types and enhancing curve point calculations for better visualization
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s
This commit is contained in:
@@ -9,23 +9,47 @@ const UNIFI_TEXT = '#f1f5f9';
|
||||
const UNIFI_TEXT_MUTED = '#94a3b8';
|
||||
const NODE_WIDTH = 88;
|
||||
const NODE_HEIGHT = 52;
|
||||
const CIRCLE_RADIUS = 240;
|
||||
const STORAGE_KEY = 'network-map-unifi-positions';
|
||||
|
||||
function computeCircleLayout(servers, size) {
|
||||
const n = servers.length;
|
||||
if (n === 0) return {};
|
||||
const cx = size.w / 2;
|
||||
const cy = size.h / 2;
|
||||
const r = Math.min(CIRCLE_RADIUS, Math.min(size.w, size.h) * 0.4);
|
||||
function computeInitialLayout(servers, size) {
|
||||
const positions = {};
|
||||
servers.forEach((s, i) => {
|
||||
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
|
||||
positions[String(s.ip)] = {
|
||||
x: cx + r * Math.cos(angle),
|
||||
y: cy + r * Math.sin(angle),
|
||||
};
|
||||
if (!servers.length) return positions;
|
||||
|
||||
// Группируем: сначала home, затем jumphost, затем exit, затем остальные
|
||||
const homes = servers.filter((s) => String(s.type || '').toLowerCase() === 'home');
|
||||
const jumphosts = servers.filter((s) => String(s.type || '').toLowerCase() === 'jumphost');
|
||||
const exits = servers.filter((s) => String(s.type || '').toLowerCase() === 'exit');
|
||||
const others = servers.filter((s) => {
|
||||
const t = String(s.type || '').toLowerCase();
|
||||
return t !== 'home' && t !== 'jumphost' && t !== 'exit';
|
||||
});
|
||||
|
||||
const columns = [homes, jumphosts, exits, others].filter((col) => col.length > 0);
|
||||
const colCount = columns.length || 1;
|
||||
const paddingX = 80;
|
||||
const usableWidth = Math.max(200, size.w - paddingX * 2);
|
||||
const colStep = usableWidth / Math.max(1, colCount - 1);
|
||||
const baseX = paddingX;
|
||||
|
||||
columns.forEach((col, colIdx) => {
|
||||
// В пределах колонки сортируем по dns/ip для стабильности
|
||||
const sorted = [...col].sort((a, b) => {
|
||||
const la = (a.dns || a.ip || '').toString();
|
||||
const lb = (b.dns || b.ip || '').toString();
|
||||
return la.localeCompare(lb);
|
||||
});
|
||||
const n = sorted.length;
|
||||
const x = baseX + colStep * colIdx;
|
||||
const topPadding = 60;
|
||||
const bottomPadding = 60;
|
||||
const usableHeight = Math.max(120, size.h - topPadding - bottomPadding);
|
||||
sorted.forEach((s, i) => {
|
||||
const t = n > 1 ? i / (n - 1) : 0.5;
|
||||
const y = topPadding + usableHeight * t;
|
||||
positions[String(s.ip)] = { x, y };
|
||||
});
|
||||
});
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
@@ -62,14 +86,14 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) saved = JSON.parse(raw) || {};
|
||||
} catch {}
|
||||
const circle = computeCircleLayout(servers, size);
|
||||
const initial = computeInitialLayout(servers, size);
|
||||
const merged = {};
|
||||
servers.forEach((s, i) => {
|
||||
const ip = String(s.ip);
|
||||
if (saved[ip] && typeof saved[ip].x === 'number' && typeof saved[ip].y === 'number') {
|
||||
merged[ip] = { x: saved[ip].x, y: saved[ip].y };
|
||||
} else {
|
||||
merged[ip] = circle[ip] ?? { x: size.w / 2, y: size.h / 2 };
|
||||
merged[ip] = initial[ip] ?? { x: size.w / 2, y: size.h / 2 };
|
||||
}
|
||||
});
|
||||
setPositions(merged);
|
||||
@@ -117,10 +141,10 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
}, [drag, getSvgCoords]);
|
||||
|
||||
const resetLayout = useCallback(() => {
|
||||
const circle = computeCircleLayout(servers, size);
|
||||
setPositions(circle);
|
||||
const initial = computeInitialLayout(servers, size);
|
||||
setPositions(initial);
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(circle));
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(initial));
|
||||
} catch {}
|
||||
}, [servers, size]);
|
||||
|
||||
@@ -158,36 +182,60 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
}).filter((e) => e.p1 && e.p2);
|
||||
}, [connections, positions, pingMap]);
|
||||
|
||||
const pathD = useCallback((e) => {
|
||||
// Вычисляем старт/финиш и контрольную точку так, чтобы рёбра выходили
|
||||
// с «боков» карточек и красиво изгибались (как в UniFi).
|
||||
const computeCurvePoints = useCallback((e) => {
|
||||
const { p1, p2 } = e;
|
||||
const mx = (p1.x + p2.x) / 2;
|
||||
const my = (p1.y + p2.y) / 2;
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const perpX = (-dy / len) * 35;
|
||||
const perpY = (dx / len) * 35;
|
||||
const adx = Math.abs(dx);
|
||||
const ady = Math.abs(dy);
|
||||
|
||||
let sx = p1.x;
|
||||
let sy = p1.y;
|
||||
let ex = p2.x;
|
||||
let ey = p2.y;
|
||||
|
||||
if (adx >= ady) {
|
||||
const sign = dx >= 0 ? 1 : -1;
|
||||
sx = p1.x + sign * (NODE_WIDTH / 2);
|
||||
sy = p1.y;
|
||||
ex = p2.x - sign * (NODE_WIDTH / 2);
|
||||
ey = p2.y;
|
||||
} else {
|
||||
const sign = dy >= 0 ? 1 : -1;
|
||||
sx = p1.x;
|
||||
sy = p1.y + sign * (NODE_HEIGHT / 2);
|
||||
ex = p2.x;
|
||||
ey = p2.y - sign * (NODE_HEIGHT / 2);
|
||||
}
|
||||
|
||||
const mx = (sx + ex) / 2;
|
||||
const my = (sy + ey) / 2;
|
||||
const ddx = ex - sx;
|
||||
const ddy = ey - sy;
|
||||
const len = Math.hypot(ddx, ddy) || 1;
|
||||
const baseOffset = 40;
|
||||
const perpX = (-ddy / len) * baseOffset;
|
||||
const perpY = (ddx / len) * baseOffset;
|
||||
const cpx = mx + perpX;
|
||||
const cpy = my + perpY;
|
||||
return `M ${p1.x} ${p1.y} Q ${cpx} ${cpy} ${p2.x} ${p2.y}`;
|
||||
|
||||
return { sx, sy, ex, ey, cpx, cpy };
|
||||
}, []);
|
||||
|
||||
const pathD = useCallback((e) => {
|
||||
const { sx, sy, ex, ey, cpx, cpy } = computeCurvePoints(e);
|
||||
return `M ${sx} ${sy} Q ${cpx} ${cpy} ${ex} ${ey}`;
|
||||
}, [computeCurvePoints]);
|
||||
|
||||
const labelPoint = useCallback((e) => {
|
||||
const { p1, p2 } = e;
|
||||
const mx = (p1.x + p2.x) / 2;
|
||||
const my = (p1.y + p2.y) / 2;
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const len = Math.hypot(dx, dy) || 1;
|
||||
const perpX = (-dy / len) * 35;
|
||||
const perpY = (dx / len) * 35;
|
||||
const cpx = mx + perpX;
|
||||
const cpy = my + perpY;
|
||||
const { sx, sy, ex, ey, cpx, cpy } = computeCurvePoints(e);
|
||||
const t = 0.5;
|
||||
const lx = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * cpx + t * t * p2.x;
|
||||
const ly = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * cpy + t * t * p2.y;
|
||||
const lx = (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cpx + t * t * ex;
|
||||
const ly = (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cpy + t * t * ey;
|
||||
return { x: lx, y: ly };
|
||||
}, []);
|
||||
}, [computeCurvePoints]);
|
||||
|
||||
if (servers.length === 0) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user