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 class="section-content">
<div class="metrics-row">
<!-- Card 1: Cluster Name -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Cluster</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.cluster_name || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 1: PVC Version -->
<ValueCard
title="PVC Version"
:value="clusterData.pvc_version || 'Unknown'"
/>
<!-- Card 2: Version -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Version</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.pvc_version || 'Unknown' }}</h4>
</div>
</div>
<!-- Card 2: Primary Node -->
<ValueCard
title="Primary Node"
:value="clusterData.primary_node || 'N/A'"
/>
<!-- Card 4: Nodes -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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 3: Nodes -->
<ValueCard
title="Nodes"
:value="clusterData.nodes?.total || 0"
/>
<!-- Card 5: Primary -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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 4: VMs -->
<ValueCard
title="VMs"
:value="clusterData.vms?.total || 0"
/>
<!-- Card 6: VMs -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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 5: Networks -->
<ValueCard
title="Networks"
:value="clusterData.networks || 0"
/>
<!-- Card 7: OSDs -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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 6: OSDs -->
<ValueCard
title="OSDs"
:value="clusterData.osds?.total || 0"
/>
<!-- Card 8: Pools -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Pools</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.pools || 0 }}</h4>
</div>
</div>
<!-- Card 7: Pools -->
<ValueCard
title="Pools"
:value="clusterData.pools || 0"
/>
<!-- Card 9: Volumes -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Volumes</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ clusterData.volumes || 0 }}</h4>
</div>
</div>
<!-- Card 8: Volumes -->
<ValueCard
title="Volumes"
:value="clusterData.volumes || 0"
/>
</div>
</div>
<div class="toggle-column expanded">
@ -333,6 +285,7 @@ import CPUChart from './charts/CPUChart.vue';
import MemoryChart from './charts/MemoryChart.vue';
import StorageChart from './charts/StorageChart.vue';
import HealthChart from './charts/HealthChart.vue';
import ValueCard from './ValueCard.vue';
// Register Chart.js components
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="metrics-row">
<!-- Card 1: Daemon State -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Daemon State</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.daemon_state || 'Unknown' }}</h4>
</div>
</div>
<ValueCard
title="Daemon State"
:value="selectedNodeData.daemon_state || 'Unknown'"
/>
<!-- Card 2: Coordinator State -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Coordinator State</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.coordinator_state || 'Unknown' }}</h4>
</div>
</div>
<ValueCard
title="Coordinator State"
:value="selectedNodeData.coordinator_state || 'Unknown'"
/>
<!-- Card 3: Domain State -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Domain State</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.domain_state || 'Unknown' }}</h4>
</div>
</div>
<ValueCard
title="Domain State"
:value="selectedNodeData.domain_state || 'Unknown'"
/>
<!-- Card 4: Domains Count -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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>
<ValueCard
title="Domains Count"
:value="selectedNodeData.domains_count || 0"
/>
<!-- Card 5: PVC Version -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">PVC Version</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.pvc_version || 'Unknown' }}</h4>
</div>
</div>
<ValueCard
title="PVC Version"
:value="selectedNodeData.pvc_version || 'Unknown'"
/>
<!-- Card 6: Kernel Version -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-0">
<span class="metric-label">Kernel Version</span>
</h6>
</div>
<div class="card-body">
<h4 class="metric-value">{{ selectedNodeData.kernel || 'Unknown' }}</h4>
</div>
</div>
<ValueCard
title="Kernel Version"
:value="selectedNodeData.kernel || 'Unknown'"
/>
<!-- Card 7: Host CPU Count -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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>
<ValueCard
title="Host CPU Count"
:value="selectedNodeData.cpu_count || 0"
/>
<!-- Card 8: Guest CPU Count -->
<div class="metric-card">
<div class="card-header">
<h6 class="card-title mb-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>
<ValueCard
title="Guest CPU Count"
:value="selectedNodeData.vcpu?.allocated || 0"
/>
</div>
</div>
<div class="toggle-column expanded">
@ -305,10 +257,11 @@
<script setup>
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import PageTitle from '../components/PageTitle.vue';
import HealthChart from '../components/charts/HealthChart.vue';
import CPUChart from '../components/charts/CPUChart.vue';
import MemoryChart from '../components/charts/MemoryChart.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
function formatBytes(bytes, decimals = 2) {
@ -359,13 +312,8 @@ const sections = ref({
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 selectedNode = ref(localStorage.getItem('selectedNodeTab') || '');
const tabsContainer = ref(null);
const showLeftScroll = ref(false);
const showRightScroll = ref(false);
@ -379,101 +327,95 @@ const selectedNodeData = computed(() => {
// Select a node
const selectNode = (nodeName) => {
selectedNode.value = nodeName;
// Save to localStorage
localStorage.setItem('selectedNodeTab', nodeName);
// Scroll the selected tab into view
nextTick(() => {
const activeTab = tabsContainer.value?.querySelector('.node-tab.active');
const activeTab = document.querySelector('.node-tab.active');
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
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
const container = tabsContainer.value;
showLeftScroll.value = container.scrollLeft > 0;
showRightScroll.value = container.scrollLeft < (container.scrollWidth - container.clientWidth - 5); // 5px buffer
};
// 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);
// Calculate CPU utilization
const calculateCpuUtilization = (node) => {
if (!node || !node.load || !node.cpu_count) return 0;
return Math.round((node.load / node.cpu_count) * 100);
};
// 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
const calculateMemoryUtilization = (node) => {
if (!node || !node.memory) return 0;
const used = node.memory.used || 0;
const total = node.memory.total || 1; // Avoid division by zero
if (total === 0) return 0;
return Math.round((node.memory.used / node.memory.total) * 100);
};
// 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);
// Calculate allocated memory
const calculateAllocatedMemory = (node) => {
if (!node || !node.memory) return 0;
const allocated = node.memory.allocated || 0;
const total = node.memory.total || 1; // Avoid division by zero
if (total === 0) return 0;
return Math.round((node.memory.allocated / node.memory.total) * 100);
};
// Prepare chart data for the node
const nodeHealthChartData = computed(() => {
// 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) {
return {
labels: nodeMetrics.health.labels,
data: nodeMetrics.health.data
};
}
// Fallback to current value only
return {
labels: ['Health'],
data: [selectedNodeData.value.health || 0]
data: [selectedNodeData.value?.health || 0]
};
});
const nodeCpuChartData = computed(() => {
// 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) {
return {
@ -531,8 +473,15 @@ const nodeAllocatedMemoryChartData = computed(() => {
// Initialize the component
onMounted(() => {
// Select the first node by default if available
if (props.nodeData && props.nodeData.length > 0) {
// Check if the saved node exists in the current node data
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);
}
@ -542,9 +491,10 @@ onMounted(() => {
});
// 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 (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);
}