version 0.7 - node labels, os details, master is purple
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m5s
Details
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m5s
Details
This commit is contained in:
parent
339d79d9f0
commit
e73e5257c9
38
app.py
38
app.py
|
|
@ -94,6 +94,37 @@ def cluster_data():
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
node_name = node.metadata.name
|
node_name = node.metadata.name
|
||||||
|
|
||||||
|
internal_ip = "N/A"
|
||||||
|
if node.status and getattr(node.status, 'addresses', None):
|
||||||
|
for addr in node.status.addresses:
|
||||||
|
addr_type = getattr(addr, 'type', None) or (addr.get('type') if isinstance(addr, dict) else None)
|
||||||
|
addr_val = getattr(addr, 'address', None) or (addr.get('address') if isinstance(addr, dict) else None)
|
||||||
|
if addr_type == 'InternalIP':
|
||||||
|
internal_ip = addr_val
|
||||||
|
break
|
||||||
|
|
||||||
|
os_image = "N/A"
|
||||||
|
if node.status and getattr(node.status, 'node_info', None):
|
||||||
|
node_info = node.status.node_info
|
||||||
|
if isinstance(node_info, dict):
|
||||||
|
os_image = node_info.get('osImage') or node_info.get('os_image') or "N/A"
|
||||||
|
else:
|
||||||
|
os_image = getattr(node_info, 'os_image', None) or getattr(node_info, 'os_image', "N/A")
|
||||||
|
|
||||||
|
labels = node.metadata.labels or {}
|
||||||
|
roles = []
|
||||||
|
for k in labels:
|
||||||
|
if k.startswith("node-role.kubernetes.io/"):
|
||||||
|
role_name = k.split("/", 1)[1]
|
||||||
|
if role_name:
|
||||||
|
roles.append(role_name)
|
||||||
|
|
||||||
|
is_control_plane = any(r in ["control-plane", "master"] for r in roles) or ("control-plane" in node_name) or ("master" in node_name)
|
||||||
|
if is_control_plane and not roles:
|
||||||
|
roles = ["control-plane" if "control-plane" in node_name else "master"]
|
||||||
|
elif not roles:
|
||||||
|
roles = ["worker"]
|
||||||
|
|
||||||
cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0'))
|
cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0'))
|
||||||
mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0'))
|
mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0'))
|
||||||
|
|
||||||
|
|
@ -103,6 +134,13 @@ def cluster_data():
|
||||||
node_data = {
|
node_data = {
|
||||||
"name": node_name,
|
"name": node_name,
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
"roles": roles,
|
||||||
|
"is_control_plane": is_control_plane,
|
||||||
|
"isControlPlane": is_control_plane,
|
||||||
|
"internal_ip": internal_ip,
|
||||||
|
"internalIp": internal_ip,
|
||||||
|
"os_image": os_image,
|
||||||
|
"osImage": os_image,
|
||||||
"cpu_allocatable": cpu_allocatable,
|
"cpu_allocatable": cpu_allocatable,
|
||||||
"mem_allocatable": mem_allocatable,
|
"mem_allocatable": mem_allocatable,
|
||||||
"cpu_usage": node_usage_cpu,
|
"cpu_usage": node_usage_cpu,
|
||||||
|
|
|
||||||
344
static/app.js
344
static/app.js
|
|
@ -1,3 +1,83 @@
|
||||||
|
// Patch Three.js r128 Sprite raycast camera space vs world space bug
|
||||||
|
(function() {
|
||||||
|
const _worldScale = new THREE.Vector3();
|
||||||
|
const _mvPosition = new THREE.Vector3();
|
||||||
|
const _vA = new THREE.Vector3();
|
||||||
|
const _vB = new THREE.Vector3();
|
||||||
|
const _vC = new THREE.Vector3();
|
||||||
|
const _uvC = new THREE.Vector2();
|
||||||
|
const _intersectPoint = new THREE.Vector3();
|
||||||
|
const _modelViewMatrix = new THREE.Matrix4();
|
||||||
|
|
||||||
|
THREE.Sprite.prototype.raycast = function (raycaster, intersects) {
|
||||||
|
if (!raycaster.camera) return;
|
||||||
|
|
||||||
|
_worldScale.setFromMatrixScale(this.matrixWorld);
|
||||||
|
_modelViewMatrix.multiplyMatrices(raycaster.camera.matrixWorldInverse, this.matrixWorld);
|
||||||
|
_mvPosition.setFromMatrixPosition(_modelViewMatrix);
|
||||||
|
|
||||||
|
if (raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false) {
|
||||||
|
_worldScale.multiplyScalar(-_mvPosition.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotation = this.material.rotation;
|
||||||
|
let sin = 0, cos = 1;
|
||||||
|
if (rotation !== 0) {
|
||||||
|
cos = Math.cos(rotation);
|
||||||
|
sin = Math.sin(rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = this.center;
|
||||||
|
|
||||||
|
const transformVertex = (v, mvPosition, center, scale, sin, cos) => {
|
||||||
|
v.x -= center.x;
|
||||||
|
v.y -= center.y;
|
||||||
|
|
||||||
|
let rx = v.x * scale.x;
|
||||||
|
let ry = v.y * scale.y;
|
||||||
|
|
||||||
|
if (sin !== 0) {
|
||||||
|
const x = rx;
|
||||||
|
const y = ry;
|
||||||
|
rx = cos * x - sin * y;
|
||||||
|
ry = sin * x + cos * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
v.x = rx + mvPosition.x;
|
||||||
|
v.y = ry + mvPosition.y;
|
||||||
|
v.z = mvPosition.z;
|
||||||
|
|
||||||
|
// Transform from Camera space to World space
|
||||||
|
v.applyMatrix4(raycaster.camera.matrixWorld);
|
||||||
|
};
|
||||||
|
|
||||||
|
transformVertex(_vA.set(-0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
||||||
|
transformVertex(_vB.set(0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
||||||
|
transformVertex(_vC.set(0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
||||||
|
|
||||||
|
let intersect = raycaster.ray.intersectTriangle(_vA, _vB, _vC, false, _intersectPoint);
|
||||||
|
|
||||||
|
if (intersect === null) {
|
||||||
|
transformVertex(_vB.set(-0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
|
||||||
|
intersect = raycaster.ray.intersectTriangle(_vA, _vC, _vB, false, _intersectPoint);
|
||||||
|
if (intersect === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = raycaster.ray.origin.distanceTo(_intersectPoint);
|
||||||
|
if (distance < raycaster.near || distance > raycaster.far) return;
|
||||||
|
|
||||||
|
intersects.push({
|
||||||
|
distance: distance,
|
||||||
|
point: _intersectPoint.clone(),
|
||||||
|
uv: _uvC.clone(),
|
||||||
|
face: null,
|
||||||
|
object: this
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
let currentMetric = 'cpu';
|
let currentMetric = 'cpu';
|
||||||
let clusterData = null;
|
let clusterData = null;
|
||||||
|
|
||||||
|
|
@ -188,24 +268,31 @@ async function fetchData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to create 3D Canvas Text Sprite for Node Labels
|
// Helper to create 3D Canvas Text Sprite for Node Labels
|
||||||
function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)') {
|
function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)', isBold = false, isControlPlane = false) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
canvas.width = 512;
|
|
||||||
canvas.height = 128;
|
// Allocate higher resolution canvas for crisp text
|
||||||
|
canvas.width = isControlPlane ? 640 : 512;
|
||||||
|
canvas.height = isControlPlane ? 160 : 128;
|
||||||
|
|
||||||
|
const pad = 16;
|
||||||
|
const w = canvas.width - pad * 2;
|
||||||
|
const h = canvas.height - pad * 2;
|
||||||
|
|
||||||
// Pill background
|
// Pill background
|
||||||
ctx.fillStyle = bgColor;
|
ctx.fillStyle = bgColor;
|
||||||
ctx.roundRect(16, 16, canvas.width - 32, canvas.height - 32, 24);
|
ctx.roundRect(pad, pad, w, h, isControlPlane ? 32 : 24);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Border
|
// Border
|
||||||
ctx.strokeStyle = 'rgba(59, 130, 246, 0.6)';
|
ctx.strokeStyle = isControlPlane ? 'rgba(168, 85, 247, 0.95)' : 'rgba(59, 130, 246, 0.6)';
|
||||||
ctx.lineWidth = 6;
|
ctx.lineWidth = isControlPlane ? 8 : 6;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Text
|
// Text - use 900 weight for control plane / master, 700 for regular
|
||||||
ctx.font = `700 ${fontSize}px Inter, sans-serif`;
|
const fontWeight = isControlPlane ? '900' : (isBold ? '800' : '700');
|
||||||
|
ctx.font = `${fontWeight} ${fontSize}px Inter, sans-serif`;
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
@ -214,7 +301,21 @@ function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor =
|
||||||
const texture = new THREE.CanvasTexture(canvas);
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true });
|
||||||
const sprite = new THREE.Sprite(spriteMaterial);
|
const sprite = new THREE.Sprite(spriteMaterial);
|
||||||
sprite.scale.set(45, 11.25, 1);
|
|
||||||
|
// Differentiate scale: control plane sprite is larger
|
||||||
|
if (isControlPlane) {
|
||||||
|
sprite.scale.set(58, 14.5, 1);
|
||||||
|
} else {
|
||||||
|
sprite.scale.set(45, 11.25, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add invisible Mesh plane for 100% reliable raycasting
|
||||||
|
const hitGeo = new THREE.PlaneGeometry(1, 1);
|
||||||
|
const hitMat = new THREE.MeshBasicMaterial({ visible: false, depthWrite: false });
|
||||||
|
const hitMesh = new THREE.Mesh(hitGeo, hitMat);
|
||||||
|
hitMesh.name = 'labelHitMesh';
|
||||||
|
sprite.add(hitMesh);
|
||||||
|
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,9 +383,13 @@ function build3DCluster(data) {
|
||||||
const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2;
|
const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2;
|
||||||
const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2;
|
const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2;
|
||||||
|
|
||||||
|
const isControlPlane = n.data.isControlPlane || n.data.is_control_plane ||
|
||||||
|
(n.data.roles && (n.data.roles.includes('control-plane') || n.data.roles.includes('master'))) ||
|
||||||
|
n.data.name.includes('control-plane') || n.data.name.includes('master');
|
||||||
|
|
||||||
const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh);
|
const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh);
|
||||||
const platformMat = new THREE.MeshStandardMaterial({
|
const platformMat = new THREE.MeshStandardMaterial({
|
||||||
color: 0x1e293b,
|
color: isControlPlane ? 0x1e1b4b : 0x1e293b,
|
||||||
roughness: 0.5,
|
roughness: 0.5,
|
||||||
metalness: 0.3,
|
metalness: 0.3,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
|
|
@ -294,18 +399,38 @@ function build3DCluster(data) {
|
||||||
platformMesh.position.set(centerX, 0.75, centerZ);
|
platformMesh.position.set(centerX, 0.75, centerZ);
|
||||||
platformMesh.receiveShadow = true;
|
platformMesh.receiveShadow = true;
|
||||||
|
|
||||||
|
const nodeUserData = {
|
||||||
|
type: 'node',
|
||||||
|
name: n.data.name,
|
||||||
|
isControlPlane: isControlPlane,
|
||||||
|
roles: n.data.roles || (isControlPlane ? ['control-plane'] : ['worker']),
|
||||||
|
internalIp: n.data.internalIp || n.data.internal_ip || 'N/A',
|
||||||
|
osImage: n.data.osImage || n.data.os_image || 'N/A'
|
||||||
|
};
|
||||||
|
platformMesh.userData = nodeUserData;
|
||||||
|
|
||||||
// Platform Border Frame
|
// Platform Border Frame
|
||||||
const edges = new THREE.EdgesGeometry(platformGeo);
|
const edges = new THREE.EdgesGeometry(platformGeo);
|
||||||
const lineMat = new THREE.LineBasicMaterial({ color: 0x3b82f6, linewidth: 2 });
|
const frameColor = isControlPlane ? 0xa855f7 : 0x3b82f6;
|
||||||
|
const lineMat = new THREE.LineBasicMaterial({ color: frameColor, linewidth: isControlPlane ? 3 : 2 });
|
||||||
const wireframe = new THREE.LineSegments(edges, lineMat);
|
const wireframe = new THREE.LineSegments(edges, lineMat);
|
||||||
platformMesh.add(wireframe);
|
platformMesh.add(wireframe);
|
||||||
|
|
||||||
scene.add(platformMesh);
|
scene.add(platformMesh);
|
||||||
nodeMeshes.push(platformMesh);
|
nodeMeshes.push(platformMesh);
|
||||||
|
|
||||||
// Node Label Sprite
|
// Node Label Sprite (Larger, bold font for control-plane/master nodes)
|
||||||
const sprite = createTextSprite(n.data.name, 26, '#f8fafc');
|
const fontSize = isControlPlane ? 34 : 24;
|
||||||
sprite.position.set(centerX, 12, centerZ - nh / 2 - 4);
|
const textColor = isControlPlane ? '#f3e8ff' : '#f8fafc';
|
||||||
|
const bgColor = isControlPlane ? 'rgba(46, 16, 101, 0.92)' : 'rgba(15, 23, 42, 0.85)';
|
||||||
|
|
||||||
|
const sprite = createTextSprite(n.data.name, fontSize, textColor, bgColor, true, isControlPlane);
|
||||||
|
sprite.position.set(centerX, isControlPlane ? 16 : 14, centerZ - nh / 2 - 2);
|
||||||
|
sprite.userData = nodeUserData;
|
||||||
|
const labelHit = sprite.getObjectByName('labelHitMesh');
|
||||||
|
if (labelHit) {
|
||||||
|
labelHit.userData = nodeUserData;
|
||||||
|
}
|
||||||
scene.add(sprite);
|
scene.add(sprite);
|
||||||
nodeMeshes.push(sprite);
|
nodeMeshes.push(sprite);
|
||||||
});
|
});
|
||||||
|
|
@ -432,106 +557,133 @@ function onPointerMove(event) {
|
||||||
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
|
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
|
||||||
|
|
||||||
raycaster.setFromCamera(mouse, camera);
|
raycaster.setFromCamera(mouse, camera);
|
||||||
const intersects = raycaster.intersectObjects(podMeshes);
|
const intersects = raycaster.intersectObjects([...podMeshes, ...nodeMeshes], true);
|
||||||
|
|
||||||
const tooltip = document.getElementById('tooltip');
|
const tooltip = document.getElementById('tooltip');
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
const hitMesh = intersects[0].object;
|
const hitMesh = intersects[0].object;
|
||||||
|
const data = (hitMesh.userData && hitMesh.userData.type) ? hitMesh.userData : (hitMesh.parent && hitMesh.parent.userData ? hitMesh.parent.userData : {});
|
||||||
|
|
||||||
if (hoveredPod !== hitMesh) {
|
if (data && data.type) {
|
||||||
// Reset previous hovered pod
|
container.style.cursor = 'pointer';
|
||||||
if (hoveredPod) {
|
|
||||||
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
|
if (hoveredPod !== hitMesh) {
|
||||||
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
|
// Reset previous hovered pod
|
||||||
|
if (hoveredPod && hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
|
||||||
|
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
|
||||||
|
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredPod = hitMesh;
|
||||||
|
// Highlight hovered pod tower
|
||||||
|
if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
|
||||||
|
hoveredPod.material.emissive.setHex(0x60a5fa);
|
||||||
|
hoveredPod.material.emissiveIntensity = 0.6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hoveredPod = hitMesh;
|
// Display Tooltip
|
||||||
// Highlight hovered pod tower
|
tooltip.classList.remove('hidden');
|
||||||
hoveredPod.material.emissive.setHex(0x60a5fa);
|
|
||||||
hoveredPod.material.emissiveIntensity = 0.6;
|
// Boundary handling
|
||||||
|
const tWidth = 260;
|
||||||
|
let left = event.pageX + 15;
|
||||||
|
let top = event.pageY + 15;
|
||||||
|
|
||||||
|
if (left + tWidth > window.innerWidth) {
|
||||||
|
left = event.pageX - tWidth - 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.style.left = left + 'px';
|
||||||
|
tooltip.style.top = top + 'px';
|
||||||
|
|
||||||
|
if (data.type === 'node') {
|
||||||
|
const isCP = data.isControlPlane || (data.roles && (data.roles.includes('control-plane') || data.roles.includes('master')));
|
||||||
|
const badgeLabel = data.roles && data.roles.length ? data.roles.join(', ') : (isCP ? 'control-plane' : 'Node');
|
||||||
|
tooltip.innerHTML = `
|
||||||
|
<div class="tooltip-title">
|
||||||
|
<span>${data.name}</span>
|
||||||
|
<span style="font-size: 0.7rem; background: ${isCP ? 'rgba(168, 85, 247, 0.35)' : 'rgba(59, 130, 246, 0.3)'}; color: ${isCP ? '#e9d5ff' : '#60a5fa'}; padding: 2px 6px; border-radius: 4px; font-weight: 700;">${badgeLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Internal IP:</span>
|
||||||
|
<span class="tooltip-value">${data.internalIp || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">OS Image:</span>
|
||||||
|
<span class="tooltip-value">${data.osImage || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (data.type === 'unused') {
|
||||||
|
const valCpu = formatCpu(data.cpu_raw);
|
||||||
|
const valMem = formatBytes(data.memory_raw);
|
||||||
|
tooltip.innerHTML = `
|
||||||
|
<div class="tooltip-title">
|
||||||
|
<span>Available Capacity</span>
|
||||||
|
<span class="legend-dot unused"></span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Node:</span>
|
||||||
|
<span class="tooltip-value">${data.nodeName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Unused CPU:</span>
|
||||||
|
<span class="tooltip-value">${valCpu}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Unused Memory:</span>
|
||||||
|
<span class="tooltip-value">${valMem}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const valCpu = formatCpu(data.cpu_raw);
|
||||||
|
const valMem = formatBytes(data.memory_raw);
|
||||||
|
const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444'));
|
||||||
|
tooltip.innerHTML = `
|
||||||
|
<div class="tooltip-title">
|
||||||
|
<span>${data.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Namespace:</span>
|
||||||
|
<span class="tooltip-value">${data.namespace}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Node:</span>
|
||||||
|
<span class="tooltip-value">${data.nodeName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Status:</span>
|
||||||
|
<span class="tooltip-value" style="color: ${statusColor}; font-weight: 700;">${data.status}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">CPU Usage:</span>
|
||||||
|
<span class="tooltip-value ${currentMetric === 'cpu' ? 'active-metric' : ''}">${valCpu}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-row">
|
||||||
|
<span class="tooltip-label">Memory Usage:</span>
|
||||||
|
<span class="tooltip-value ${currentMetric === 'memory' ? 'active-metric' : ''}">${valMem}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display Tooltip
|
container.style.cursor = 'grab';
|
||||||
tooltip.classList.remove('hidden');
|
if (hoveredPod) {
|
||||||
|
if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
|
||||||
// Boundary handling
|
|
||||||
const tWidth = 260;
|
|
||||||
let left = event.pageX + 15;
|
|
||||||
let top = event.pageY + 15;
|
|
||||||
|
|
||||||
if (left + tWidth > window.innerWidth) {
|
|
||||||
left = event.pageX - tWidth - 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip.style.left = left + 'px';
|
|
||||||
tooltip.style.top = top + 'px';
|
|
||||||
|
|
||||||
const data = hitMesh.userData;
|
|
||||||
const valCpu = formatCpu(data.cpu_raw);
|
|
||||||
const valMem = formatBytes(data.memory_raw);
|
|
||||||
|
|
||||||
if (data.type === 'unused') {
|
|
||||||
tooltip.innerHTML = `
|
|
||||||
<div class="tooltip-title">
|
|
||||||
<span>Available Capacity</span>
|
|
||||||
<span class="legend-dot unused"></span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Node:</span>
|
|
||||||
<span class="tooltip-value">${data.nodeName}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Unused CPU:</span>
|
|
||||||
<span class="tooltip-value">${valCpu}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Unused Memory:</span>
|
|
||||||
<span class="tooltip-value">${valMem}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444'));
|
|
||||||
tooltip.innerHTML = `
|
|
||||||
<div class="tooltip-title">
|
|
||||||
<span>${data.name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Namespace:</span>
|
|
||||||
<span class="tooltip-value">${data.namespace}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Node:</span>
|
|
||||||
<span class="tooltip-value">${data.nodeName}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Status:</span>
|
|
||||||
<span class="tooltip-value" style="color: ${statusColor}; font-weight: 700;">${data.status}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">CPU Usage:</span>
|
|
||||||
<span class="tooltip-value ${currentMetric === 'cpu' ? 'active-metric' : ''}">${valCpu}</span>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip-row">
|
|
||||||
<span class="tooltip-label">Memory Usage:</span>
|
|
||||||
<span class="tooltip-value ${currentMetric === 'memory' ? 'active-metric' : ''}">${valMem}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (hoveredPod) {
|
|
||||||
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
|
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
|
||||||
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
|
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
|
||||||
hoveredPod = null;
|
|
||||||
}
|
}
|
||||||
tooltip.classList.add('hidden');
|
hoveredPod = null;
|
||||||
}
|
}
|
||||||
|
tooltip.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click to focus camera on selected Pod
|
// Click to focus camera on selected Pod
|
||||||
function onPointerClick(event) {
|
function onPointerClick(event) {
|
||||||
if (!hoveredPod) return;
|
if (!hoveredPod || hoveredPod.userData.type === 'node') return;
|
||||||
|
|
||||||
let targetPos = hoveredPod.position.clone();
|
let targetPos = hoveredPod.position.clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
version = 0.6
|
version = 0.7
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue