diff --git a/pvc-vue/src/App.vue b/pvc-vue/src/App.vue index 415a62f..bd6cd05 100644 --- a/pvc-vue/src/App.vue +++ b/pvc-vue/src/App.vue @@ -113,6 +113,50 @@ const updateMetricsHistory = (timestamp, status) => { data }; }); + + // Track node-specific metrics + if (!metricsHistory.value.nodes) { + metricsHistory.value.nodes = {}; + } + + // Update metrics for each node + nodeData.value.forEach(node => { + const nodeName = node.name; + + if (!metricsHistory.value.nodes[nodeName]) { + metricsHistory.value.nodes[nodeName] = { + health: { labels: [], data: [] }, + cpu: { labels: [], data: [] }, + memory: { labels: [], data: [] }, + allocated: { labels: [], data: [] } + }; + } + + // Calculate node metrics + const nodeMetrics = { + health: node.health || 0, + cpu: node.load && node.cpu_count ? Math.round((node.load / node.cpu_count) * 100) : 0, + memory: node.memory?.used && node.memory?.total ? Math.round((node.memory.used / node.memory.total) * 100) : 0, + allocated: node.memory?.allocated && node.memory?.total ? Math.round((node.memory.allocated / node.memory.total) * 100) : 0 + }; + + // Update each metric for this node + Object.keys(nodeMetrics).forEach(metric => { + const nodeLabels = [...metricsHistory.value.nodes[nodeName][metric].labels, timestamp]; + const nodeData = [...metricsHistory.value.nodes[nodeName][metric].data, nodeMetrics[metric]]; + + // Keep only last 180 points + if (nodeLabels.length > 180) { + nodeLabels.shift(); + nodeData.shift(); + } + + metricsHistory.value.nodes[nodeName][metric] = { + labels: nodeLabels, + data: nodeData + }; + }); + }); }; const updateDashboard = async () => { diff --git a/pvc-vue/src/views/Nodes.vue b/pvc-vue/src/views/Nodes.vue index 690493a..383f71a 100644 --- a/pvc-vue/src/views/Nodes.vue +++ b/pvc-vue/src/views/Nodes.vue @@ -1,19 +1,1184 @@ - + + + + + + {{ node.name }} + + + + + + + + + + + + + + + + + + + + Node Information + + + + + + + + + + + + + + + + + Daemon State + + + + {{ selectedNodeData.daemon_state || 'Unknown' }} + + + + + + + + Coordinator State + + + + {{ selectedNodeData.coordinator_state || 'Unknown' }} + + + + + + + + Domain State + + + + {{ selectedNodeData.domain_state || 'Unknown' }} + + + + + + + + Domains Count + + + + {{ selectedNodeData.domains_count || 0 }} + + + + + + + + PVC Version + + + + {{ selectedNodeData.pvc_version || 'Unknown' }} + + + + + + + + Kernel Version + + + + {{ selectedNodeData.kernel || 'Unknown' }} + + + + + + + + Host CPU Count + + + + {{ selectedNodeData.cpu_count || 0 }} + + + + + + + + Guest CPU Count + + + + {{ selectedNodeData.vcpu?.allocated || 0 }} + + + + + + + + + + + + + + + + + + + Health & Utilization Graphs + + + + + + + + + + + + + + + + Node Health + + + + + {{ selectedNodeData.health || 0 }}% + + + + + + + + + + + + + CPU Utilization + + + + + + {{ calculateCpuUtilization(selectedNodeData) }}% + + + + + + + + + + + + + Memory Utilization + + + + + + {{ calculateMemoryUtilization(selectedNodeData) }}% + + + + + + + + + + + + + Allocated Memory + + + + + + {{ calculateAllocatedMemory(selectedNodeData) }}% + + + + + + + + + + + + + + + + + + + + + + + + Memory Resources + + + + + + + + + + + + + + + + + Total Memory + + + + {{ formatMemory(selectedNodeData.memory?.total) }} + + + + + + + + Used Memory + + + + {{ formatMemory(selectedNodeData.memory?.used) }} + + + + + + + + Free Memory + + + + {{ formatMemory(selectedNodeData.memory?.free) }} + + + + + + + + Allocated Memory + + + + {{ formatMemory(selectedNodeData.memory?.allocated) }} + + + + + + + + Provisioned Memory + + + + {{ formatMemory(selectedNodeData.memory?.provisioned) }} + + + + + + + + + + + + + + + + Select a node to view details + \ No newline at end of file + +// State for sections (expanded/collapsed) +const sections = ref({ + info: true, + graphs: true, + resources: true +}); + +// Toggle section visibility +const toggleSection = (section) => { + sections.value[section] = !sections.value[section]; +}; + +// State for selected node and tab scrolling +const selectedNode = ref(''); +const tabsContainer = ref(null); +const showLeftScroll = ref(false); +const showRightScroll = ref(false); + +// Get the selected node data +const selectedNodeData = computed(() => { + if (!selectedNode.value || !props.nodeData) return null; + return props.nodeData.find(node => node.name === selectedNode.value) || null; +}); + +// Select a node +const selectNode = (nodeName) => { + selectedNode.value = nodeName; + + // Scroll the selected tab into view + nextTick(() => { + const activeTab = tabsContainer.value?.querySelector('.node-tab.active'); + if (activeTab) { + activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); + } + + // Check if scroll buttons should be shown + checkScrollButtons(); + }); +}; + +// Check if scroll buttons should be shown +const checkScrollButtons = () => { + if (!tabsContainer.value) return; + + const { scrollLeft, scrollWidth, clientWidth } = tabsContainer.value; + + // Show left scroll button if not at the beginning + showLeftScroll.value = scrollLeft > 0; + + // Show right scroll button if not at the end + showRightScroll.value = scrollLeft + clientWidth < scrollWidth - 1; // -1 for rounding errors +}; + +// Scroll tabs left or right +const scrollTabs = (direction) => { + if (!tabsContainer.value) return; + + const scrollAmount = tabsContainer.value.clientWidth / 2; + const currentScroll = tabsContainer.value.scrollLeft; + + if (direction === 'left') { + tabsContainer.value.scrollTo({ + left: Math.max(0, currentScroll - scrollAmount), + behavior: 'smooth' + }); + } else { + tabsContainer.value.scrollTo({ + left: currentScroll + scrollAmount, + behavior: 'smooth' + }); + } + + // Check scroll buttons after scrolling + setTimeout(checkScrollButtons, 300); +}; + +// Format memory values from MB to GB +const formatMemory = (memoryMB) => { + if (memoryMB === undefined || memoryMB === null) return 'N/A'; + + const memoryGB = memoryMB / 1024; + return `${memoryGB.toFixed(2)} GB`; +}; + +// Calculate CPU utilization (load / cpu_count * 100) +const calculateCpuUtilization = (nodeData) => { + if (!nodeData || !nodeData.load || !nodeData.cpu_count) return 0; + + const utilization = (nodeData.load / nodeData.cpu_count) * 100; + return Math.round(utilization); +}; + +// Calculate memory utilization (memory.used / memory.total * 100) +const calculateMemoryUtilization = (nodeData) => { + if (!nodeData || !nodeData.memory || !nodeData.memory.used || !nodeData.memory.total) return 0; + + const utilization = (nodeData.memory.used / nodeData.memory.total) * 100; + return Math.round(utilization); +}; + +// Calculate allocated memory (memory.allocated / memory.total * 100) +const calculateAllocatedMemory = (nodeData) => { + if (!nodeData || !nodeData.memory || !nodeData.memory.allocated || !nodeData.memory.total) return 0; + + const utilization = (nodeData.memory.allocated / nodeData.memory.total) * 100; + return Math.round(utilization); +}; + +// Chart data +const healthChartData = computed(() => { + if (!selectedNodeData.value) { + return { + labels: ['Health'], + datasets: [{ + label: 'Node Health', + data: [0], + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Get node metrics history if available + const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; + + if (nodeMetrics && nodeMetrics.health && nodeMetrics.health.data.length > 0) { + return { + labels: nodeMetrics.health.labels, + datasets: [{ + label: 'Node Health', + data: nodeMetrics.health.data, + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Fallback to current value only + return { + labels: ['Health'], + datasets: [{ + label: 'Node Health', + data: [selectedNodeData.value.health || 0], + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; +}); + +const cpuChartData = computed(() => { + if (!selectedNodeData.value) { + return { + labels: ['CPU'], + datasets: [{ + label: 'CPU Utilization', + data: [0], + backgroundColor: 'rgba(75, 192, 192, 0.2)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Get node metrics history if available + const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; + + if (nodeMetrics && nodeMetrics.cpu && nodeMetrics.cpu.data.length > 0) { + return { + labels: nodeMetrics.cpu.labels, + datasets: [{ + label: 'CPU Utilization', + data: nodeMetrics.cpu.data, + backgroundColor: 'rgba(75, 192, 192, 0.2)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Fallback to current value only + return { + labels: ['CPU'], + datasets: [{ + label: 'CPU Utilization', + data: [calculateCpuUtilization(selectedNodeData.value)], + backgroundColor: 'rgba(75, 192, 192, 0.2)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; +}); + +const memoryChartData = computed(() => { + if (!selectedNodeData.value) { + return { + labels: ['Memory'], + datasets: [{ + label: 'Memory Utilization', + data: [0], + backgroundColor: 'rgba(153, 102, 255, 0.2)', + borderColor: 'rgba(153, 102, 255, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Get node metrics history if available + const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; + + if (nodeMetrics && nodeMetrics.memory && nodeMetrics.memory.data.length > 0) { + return { + labels: nodeMetrics.memory.labels, + datasets: [{ + label: 'Memory Utilization', + data: nodeMetrics.memory.data, + backgroundColor: 'rgba(153, 102, 255, 0.2)', + borderColor: 'rgba(153, 102, 255, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Fallback to current value only + return { + labels: ['Memory'], + datasets: [{ + label: 'Memory Utilization', + data: [calculateMemoryUtilization(selectedNodeData.value)], + backgroundColor: 'rgba(153, 102, 255, 0.2)', + borderColor: 'rgba(153, 102, 255, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; +}); + +const allocatedMemoryChartData = computed(() => { + if (!selectedNodeData.value) { + return { + labels: ['Allocated'], + datasets: [{ + label: 'Allocated Memory', + data: [0], + backgroundColor: 'rgba(255, 159, 64, 0.2)', + borderColor: 'rgba(255, 159, 64, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Get node metrics history if available + const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; + + if (nodeMetrics && nodeMetrics.allocated && nodeMetrics.allocated.data.length > 0) { + return { + labels: nodeMetrics.allocated.labels, + datasets: [{ + label: 'Allocated Memory', + data: nodeMetrics.allocated.data, + backgroundColor: 'rgba(255, 159, 64, 0.2)', + borderColor: 'rgba(255, 159, 64, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; + } + + // Fallback to current value only + return { + labels: ['Allocated'], + datasets: [{ + label: 'Allocated Memory', + data: [calculateAllocatedMemory(selectedNodeData.value)], + backgroundColor: 'rgba(255, 159, 64, 0.2)', + borderColor: 'rgba(255, 159, 64, 1)', + borderWidth: 2, + fill: true, + pointRadius: 0 + }] + }; +}); + +// Chart options +const chartOptions = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + max: 100, + grid: { + display: false + }, + ticks: { + display: false + } + }, + x: { + grid: { + display: false + }, + ticks: { + display: false + } + } + }, + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: false + } + }, + elements: { + line: { + tension: 0.4, + borderWidth: 2 + }, + point: { + radius: 0, + hitRadius: 10 + } + } +}; + +const healthChartOptions = { + ...chartOptions, + // Additional options specific to health chart if needed +}; + +// Initialize the component +onMounted(() => { + // Select the first node by default if available + if (props.nodeData && props.nodeData.length > 0) { + selectNode(props.nodeData[0].name); + } + + // Check if scroll buttons should be shown + window.addEventListener('resize', checkScrollButtons); + nextTick(checkScrollButtons); +}); + +// Watch for changes in the nodes data +watch(() => props.nodeData, () => { + // If the currently selected node no longer exists, select the first available node + if (props.nodeData && props.nodeData.length > 0 && !props.nodeData.find(node => node.name === selectedNode.value)) { + selectNode(props.nodeData[0].name); + } + + // Check if scroll buttons should be shown + nextTick(checkScrollButtons); +}, { deep: true }); + +// Clean up event listeners +onUnmounted(() => { + window.removeEventListener('resize', checkScrollButtons); +}); + + + \ No newline at end of file
Select a node to view details