Refactor cards to be generic and reusable

This commit is contained in:
Joshua Boniface 2025-02-28 21:20:24 -05:00
parent 08f00d9c62
commit 3d13a05c0f
3 changed files with 232 additions and 238 deletions

View File

@ -19,101 +19,53 @@
<div v-show="sections.info" class="section-content-wrapper"> <div v-show="sections.info" class="section-content-wrapper">
<div class="section-content"> <div class="section-content">
<div class="metrics-row"> <div class="metrics-row">
<!-- Card 1: Cluster Name --> <!-- Card 1: PVC Version -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="PVC Version"
<h6 class="card-title mb-0"> :value="clusterData.pvc_version || 'Unknown'"
<span class="metric-label">Cluster</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.cluster_name || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 2: Version --> <!-- Card 2: Primary Node -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Primary Node"
<h6 class="card-title mb-0"> :value="clusterData.primary_node || 'N/A'"
<span class="metric-label">Version</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.pvc_version || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 4: Nodes --> <!-- Card 3: Nodes -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Nodes"
<h6 class="card-title mb-0"> :value="clusterData.nodes?.total || 0"
<span class="metric-label">Nodes</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.nodes?.total || 0 }}</h4>
</div>
</div>
<!-- Card 5: Primary --> <!-- Card 4: VMs -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="VMs"
<h6 class="card-title mb-0"> :value="clusterData.vms?.total || 0"
<span class="metric-label">Primary Node</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.primary_node || 'N/A' }}</h4>
</div>
</div>
<!-- Card 6: VMs --> <!-- Card 5: Networks -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Networks"
<h6 class="card-title mb-0"> :value="clusterData.networks || 0"
<span class="metric-label">VMs</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.vms?.total || 0 }}</h4>
</div>
</div>
<!-- Card 7: OSDs --> <!-- Card 6: OSDs -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="OSDs"
<h6 class="card-title mb-0"> :value="clusterData.osds?.total || 0"
<span class="metric-label">OSDs</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.osds?.total || 0 }}</h4>
</div>
</div>
<!-- Card 8: Pools --> <!-- Card 7: Pools -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Pools"
<h6 class="card-title mb-0"> :value="clusterData.pools || 0"
<span class="metric-label">Pools</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.pools || 0 }}</h4>
</div>
</div>
<!-- Card 9: Volumes --> <!-- Card 8: Volumes -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Volumes"
<h6 class="card-title mb-0"> :value="clusterData.volumes || 0"
<span class="metric-label">Volumes</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.volumes || 0 }}</h4>
</div>
</div>
</div> </div>
</div> </div>
<div class="toggle-column expanded"> <div class="toggle-column expanded">
@ -333,6 +285,7 @@ import CPUChart from './charts/CPUChart.vue';
import MemoryChart from './charts/MemoryChart.vue'; import MemoryChart from './charts/MemoryChart.vue';
import StorageChart from './charts/StorageChart.vue'; import StorageChart from './charts/StorageChart.vue';
import HealthChart from './charts/HealthChart.vue'; import HealthChart from './charts/HealthChart.vue';
import ValueCard from './ValueCard.vue';
// Register Chart.js components // Register Chart.js components
ChartJS.register( ChartJS.register(

View File

@ -0,0 +1,91 @@
<template>
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">{{ title }}</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value" :class="valueClass">{{ value }}</h4>
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
},
value: {
type: [String, Number],
required: true
},
valueClass: {
type: String,
default: ''
}
});
</script>
<style scoped>
.metric-card {
min-width: 180px;
background: white;
border: 1px solid rgba(0,0,0,0.125);
border-radius: 0.25rem;
height: 100%;
display: flex;
flex-direction: column;
min-width: 0;
width: 100%;
overflow: hidden;
}
.metric-card .card-header {
background-color: rgba(0, 0, 0, 0.03);
padding: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
height: 38px;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h6 {
font-size: 0.95rem;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0;
}
.metric-card .card-body {
flex: 1;
padding: 0.5rem;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
overflow: visible;
justify-content: center;
align-items: center;
}
.metric-value {
font-size: 1.25rem;
font-weight: bold;
color: #333;
line-height: 1;
margin: 0;
text-align: center;
white-space: nowrap;
min-width: fit-content;
}
.metric-label {
color: #495057;
font-weight: 600;
}
</style>

View File

@ -46,100 +46,52 @@
<div class="section-content"> <div class="section-content">
<div class="metrics-row"> <div class="metrics-row">
<!-- Card 1: Daemon State --> <!-- Card 1: Daemon State -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Daemon State"
<h6 class="card-title mb-0"> :value="selectedNodeData.daemon_state || 'Unknown'"
<span class="metric-label">Daemon State</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.daemon_state || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 2: Coordinator State --> <!-- Card 2: Coordinator State -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Coordinator State"
<h6 class="card-title mb-0"> :value="selectedNodeData.coordinator_state || 'Unknown'"
<span class="metric-label">Coordinator State</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.coordinator_state || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 3: Domain State --> <!-- Card 3: Domain State -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Domain State"
<h6 class="card-title mb-0"> :value="selectedNodeData.domain_state || 'Unknown'"
<span class="metric-label">Domain State</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.domain_state || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 4: Domains Count --> <!-- Card 4: Domains Count -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Domains Count"
<h6 class="card-title mb-0"> :value="selectedNodeData.domains_count || 0"
<span class="metric-label">Domains Count</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.domains_count || 0 }}</h4>
</div>
</div>
<!-- Card 5: PVC Version --> <!-- Card 5: PVC Version -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="PVC Version"
<h6 class="card-title mb-0"> :value="selectedNodeData.pvc_version || 'Unknown'"
<span class="metric-label">PVC Version</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.pvc_version || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 6: Kernel Version --> <!-- Card 6: Kernel Version -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Kernel Version"
<h6 class="card-title mb-0"> :value="selectedNodeData.kernel || 'Unknown'"
<span class="metric-label">Kernel Version</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.kernel || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 7: Host CPU Count --> <!-- Card 7: Host CPU Count -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Host CPU Count"
<h6 class="card-title mb-0"> :value="selectedNodeData.cpu_count || 0"
<span class="metric-label">Host CPU Count</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.cpu_count || 0 }}</h4>
</div>
</div>
<!-- Card 8: Guest CPU Count --> <!-- Card 8: Guest CPU Count -->
<div class="metric-card"> <ValueCard
<div class="card-header"> title="Guest CPU Count"
<h6 class="card-title mb-0"> :value="selectedNodeData.vcpu?.allocated || 0"
<span class="metric-label">Guest CPU Count</span> />
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.vcpu?.allocated || 0 }}</h4>
</div>
</div>
</div> </div>
</div> </div>
<div class="toggle-column expanded"> <div class="toggle-column expanded">
@ -305,10 +257,11 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue'; import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import PageTitle from '../components/PageTitle.vue'; import PageTitle from '../components/PageTitle.vue';
import HealthChart from '../components/charts/HealthChart.vue';
import CPUChart from '../components/charts/CPUChart.vue'; import CPUChart from '../components/charts/CPUChart.vue';
import MemoryChart from '../components/charts/MemoryChart.vue'; import MemoryChart from '../components/charts/MemoryChart.vue';
import StorageChart from '../components/charts/StorageChart.vue'; import StorageChart from '../components/charts/StorageChart.vue';
import HealthChart from '../components/charts/HealthChart.vue'; import ValueCard from '../components/ValueCard.vue';
// Implement formatBytes function directly // Implement formatBytes function directly
function formatBytes(bytes, decimals = 2) { function formatBytes(bytes, decimals = 2) {
@ -359,13 +312,8 @@ const sections = ref({
resources: true resources: true
}); });
// Toggle section visibility
const toggleSection = (section) => {
sections.value[section] = !sections.value[section];
};
// State for selected node and tab scrolling // State for selected node and tab scrolling
const selectedNode = ref(''); const selectedNode = ref(localStorage.getItem('selectedNodeTab') || '');
const tabsContainer = ref(null); const tabsContainer = ref(null);
const showLeftScroll = ref(false); const showLeftScroll = ref(false);
const showRightScroll = ref(false); const showRightScroll = ref(false);
@ -379,83 +327,77 @@ const selectedNodeData = computed(() => {
// Select a node // Select a node
const selectNode = (nodeName) => { const selectNode = (nodeName) => {
selectedNode.value = nodeName; selectedNode.value = nodeName;
// Save to localStorage
localStorage.setItem('selectedNodeTab', nodeName);
// Scroll the selected tab into view // Scroll the selected tab into view
nextTick(() => { nextTick(() => {
const activeTab = tabsContainer.value?.querySelector('.node-tab.active'); const activeTab = document.querySelector('.node-tab.active');
if (activeTab) { if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
} }
// Check if scroll buttons should be shown
checkScrollButtons();
}); });
}; };
// Toggle section visibility
const toggleSection = (section) => {
sections.value[section] = !sections.value[section];
};
// Scroll the tabs
const scrollTabs = (direction) => {
if (!tabsContainer.value) return;
const container = tabsContainer.value;
const scrollAmount = 200; // Adjust as needed
if (direction === 'left') {
container.scrollLeft -= scrollAmount;
} else {
container.scrollLeft += scrollAmount;
}
// Update scroll button visibility after scrolling
checkScrollButtons();
};
// Check if scroll buttons should be shown // Check if scroll buttons should be shown
const checkScrollButtons = () => { const checkScrollButtons = () => {
if (!tabsContainer.value) return; if (!tabsContainer.value) return;
const { scrollLeft, scrollWidth, clientWidth } = tabsContainer.value; const container = tabsContainer.value;
showLeftScroll.value = container.scrollLeft > 0;
// Show left scroll button if not at the beginning showRightScroll.value = container.scrollLeft < (container.scrollWidth - container.clientWidth - 5); // 5px buffer
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 // Calculate CPU utilization
const scrollTabs = (direction) => { const calculateCpuUtilization = (node) => {
if (!tabsContainer.value) return; if (!node || !node.load || !node.cpu_count) return 0;
return Math.round((node.load / node.cpu_count) * 100);
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);
}; };
// Calculate CPU utilization (load / cpu_count * 100) // Calculate memory utilization
const calculateCpuUtilization = (nodeData) => { const calculateMemoryUtilization = (node) => {
if (!nodeData || !nodeData.load || !nodeData.cpu_count) return 0; if (!node || !node.memory) return 0;
const used = node.memory.used || 0;
const utilization = (nodeData.load / nodeData.cpu_count) * 100; const total = node.memory.total || 1; // Avoid division by zero
return Math.round(utilization); if (total === 0) return 0;
return Math.round((node.memory.used / node.memory.total) * 100);
}; };
// Calculate memory utilization (memory.used / memory.total * 100) // Calculate allocated memory
const calculateMemoryUtilization = (nodeData) => { const calculateAllocatedMemory = (node) => {
if (!nodeData || !nodeData.memory || !nodeData.memory.used || !nodeData.memory.total) return 0; if (!node || !node.memory) return 0;
const allocated = node.memory.allocated || 0;
const utilization = (nodeData.memory.used / nodeData.memory.total) * 100; const total = node.memory.total || 1; // Avoid division by zero
return Math.round(utilization); if (total === 0) return 0;
}; return Math.round((node.memory.allocated / node.memory.total) * 100);
// 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);
}; };
// Prepare chart data for the node // Prepare chart data for the node
const nodeHealthChartData = computed(() => { const nodeHealthChartData = computed(() => {
// Get node metrics history if available // Get node metrics history if available
const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value?.name];
if (nodeMetrics && nodeMetrics.health && nodeMetrics.health.data.length > 0) { if (nodeMetrics && nodeMetrics.health && nodeMetrics.health.data.length > 0) {
return { return {
@ -467,13 +409,13 @@ const nodeHealthChartData = computed(() => {
// Fallback to current value only // Fallback to current value only
return { return {
labels: ['Health'], labels: ['Health'],
data: [selectedNodeData.value.health || 0] data: [selectedNodeData.value?.health || 0]
}; };
}); });
const nodeCpuChartData = computed(() => { const nodeCpuChartData = computed(() => {
// Get node metrics history if available // Get node metrics history if available
const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value.name]; const nodeMetrics = props.metricsData.nodes?.[selectedNodeData.value?.name];
if (nodeMetrics && nodeMetrics.cpu && nodeMetrics.cpu.data.length > 0) { if (nodeMetrics && nodeMetrics.cpu && nodeMetrics.cpu.data.length > 0) {
return { return {
@ -531,8 +473,15 @@ const nodeAllocatedMemoryChartData = computed(() => {
// Initialize the component // Initialize the component
onMounted(() => { onMounted(() => {
// Select the first node by default if available // Check if the saved node exists in the current node data
if (props.nodeData && props.nodeData.length > 0) { const savedNode = localStorage.getItem('selectedNodeTab');
const nodeExists = props.nodeData.some(node => node.name === savedNode);
if (savedNode && nodeExists) {
// Use the saved node
selectedNode.value = savedNode;
} else if (props.nodeData && props.nodeData.length > 0) {
// Default to the first node if saved node doesn't exist
selectNode(props.nodeData[0].name); selectNode(props.nodeData[0].name);
} }
@ -542,9 +491,10 @@ onMounted(() => {
}); });
// Watch for changes in the nodes data // Watch for changes in the nodes data
watch(() => props.nodeData, () => { watch(() => props.nodeData, (newNodes) => {
// If the currently selected node no longer exists, select the first available node // 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)) { const nodeExists = newNodes.some(node => node.name === selectedNode.value);
if (newNodes && newNodes.length > 0 && !nodeExists) {
selectNode(props.nodeData[0].name); selectNode(props.nodeData[0].name);
} }