Compare commits
No commits in common. "8d1028ab47f27e5962db0cb56f149e8c45542198" and "e6da8ec2c0b23bf182fc567318a31c23690bce79" have entirely different histories.
8d1028ab47
...
e6da8ec2c0
@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@ -26,48 +26,15 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'select']);
|
const emit = defineEmits(['update:modelValue', 'select']);
|
||||||
|
|
||||||
const selectedNode = ref(props.value || props.modelValue);
|
|
||||||
|
|
||||||
const selectNode = (node) => {
|
const selectNode = (node) => {
|
||||||
selectedNode.value = node;
|
|
||||||
emit('update:modelValue', node);
|
emit('update:modelValue', node);
|
||||||
emit('select', node);
|
emit('select', node);
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('selectedNodeId', node);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Restore selected node from localStorage if available
|
|
||||||
const savedNodeId = localStorage.getItem('selectedNodeId');
|
|
||||||
if (savedNodeId && props.nodes.includes(savedNodeId)) {
|
|
||||||
// Only select if the node exists in the available nodes
|
|
||||||
selectedNode.value = savedNodeId;
|
|
||||||
emit('update:modelValue', savedNodeId);
|
|
||||||
emit('select', savedNodeId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => props.value, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
selectedNode.value = newValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
selectedNode.value = newValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -27,10 +27,9 @@
|
|||||||
class="form-control search-input"
|
class="form-control search-input"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="shouldShowClearButton"
|
v-if="modelValue && showList && showClearButton"
|
||||||
class="btn-clear"
|
class="btn-clear"
|
||||||
@click.stop="handleClearButton"
|
@click.stop="clearSearch"
|
||||||
:title="showList ? 'Clear search' : 'Clear selected VM'"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -159,8 +158,7 @@ const emit = defineEmits([
|
|||||||
'toggle-list',
|
'toggle-list',
|
||||||
'select-vm',
|
'select-vm',
|
||||||
'toggle-filter',
|
'toggle-filter',
|
||||||
'reset-filters',
|
'reset-filters'
|
||||||
'clear-vm'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// UI state refs
|
// UI state refs
|
||||||
@ -181,23 +179,11 @@ const appliedFilters = ref({
|
|||||||
nodes: {}
|
nodes: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a computed property to determine if the clear button should be shown
|
|
||||||
const shouldShowClearButton = computed(() => {
|
|
||||||
// Show when:
|
|
||||||
// 1. List is open and there's a search query
|
|
||||||
// 2. List is closed and there's a selected VM
|
|
||||||
return (props.showList && props.modelValue && props.showClearButton) ||
|
|
||||||
(!props.showList && (props.selectedVM || localStorage.getItem('selectedVMId')));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the component
|
// Initialize the component
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Set up click outside handler
|
// Set up click outside handler
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
|
||||||
// Get the selected VM from localStorage
|
|
||||||
const savedVMId = localStorage.getItem('selectedVMId');
|
|
||||||
|
|
||||||
// Restore search text from localStorage if available
|
// Restore search text from localStorage if available
|
||||||
const savedSearchText = localStorage.getItem('vmSearchText');
|
const savedSearchText = localStorage.getItem('vmSearchText');
|
||||||
if (savedSearchText) {
|
if (savedSearchText) {
|
||||||
@ -208,20 +194,9 @@ onMounted(() => {
|
|||||||
loadFiltersFromLocalStorage();
|
loadFiltersFromLocalStorage();
|
||||||
|
|
||||||
// Initialize input value based on selected VM
|
// Initialize input value based on selected VM
|
||||||
if (!props.showList) {
|
if (!props.showList && (props.selectedVM || props.vmFromUrl)) {
|
||||||
// If list is closed, show the selected VM name in the input
|
inputValue.value = props.selectedVM || props.vmFromUrl;
|
||||||
const effectiveVM = props.selectedVM || props.vmFromUrl || savedVMId;
|
|
||||||
if (effectiveVM) {
|
|
||||||
// Find the VM in the list to get its full name
|
|
||||||
const vm = props.vmList.find(v => v.name === effectiveVM);
|
|
||||||
if (vm) {
|
|
||||||
inputValue.value = vm.name;
|
|
||||||
} else {
|
|
||||||
inputValue.value = effectiveVM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (props.showList && props.modelValue) {
|
} else if (props.showList && props.modelValue) {
|
||||||
// If list is open and there's a search query, show that
|
|
||||||
inputValue.value = props.modelValue;
|
inputValue.value = props.modelValue;
|
||||||
searchText.value = props.modelValue;
|
searchText.value = props.modelValue;
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
@ -229,7 +204,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the list is visible on mount, scroll to selected VM
|
// If the list is visible on mount, scroll to selected VM
|
||||||
if (props.showList && (props.selectedVM || props.vmFromUrl || savedVMId)) {
|
if (props.showList && (props.selectedVM || props.vmFromUrl)) {
|
||||||
scrollToSelectedVM();
|
scrollToSelectedVM();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -348,17 +323,6 @@ const toggleList = () => {
|
|||||||
emit('toggle-list');
|
emit('toggle-list');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle clear button click
|
|
||||||
const handleClearButton = () => {
|
|
||||||
if (props.showList) {
|
|
||||||
// If the list is open, clear the search
|
|
||||||
clearSearch();
|
|
||||||
} else {
|
|
||||||
// If the list is closed, clear the selected VM
|
|
||||||
clearSelectedVM();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear search
|
// Clear search
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
inputValue.value = '';
|
inputValue.value = '';
|
||||||
@ -383,28 +347,10 @@ const clearSearch = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear selected VM
|
|
||||||
const clearSelectedVM = () => {
|
|
||||||
// Clear the input value
|
|
||||||
inputValue.value = '';
|
|
||||||
|
|
||||||
// Clear from localStorage
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
|
|
||||||
// Show the VM list
|
|
||||||
emit('toggle-list');
|
|
||||||
|
|
||||||
// Emit event to parent component
|
|
||||||
emit('clear-vm');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select a VM
|
// Select a VM
|
||||||
const handleVMSelect = (vm) => {
|
const handleVMSelect = (vm) => {
|
||||||
console.log('Selecting VM:', vm);
|
console.log('Selecting VM:', vm);
|
||||||
|
|
||||||
// Update the input value to show the selected VM
|
|
||||||
inputValue.value = vm.name;
|
|
||||||
|
|
||||||
// Save the current search text before selecting a VM
|
// Save the current search text before selecting a VM
|
||||||
if (props.modelValue) {
|
if (props.modelValue) {
|
||||||
searchText.value = props.modelValue;
|
searchText.value = props.modelValue;
|
||||||
@ -412,9 +358,6 @@ const handleVMSelect = (vm) => {
|
|||||||
localStorage.setItem('vmSearchText', props.modelValue);
|
localStorage.setItem('vmSearchText', props.modelValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save selected VM to localStorage
|
|
||||||
localStorage.setItem('selectedVMId', vm.name);
|
|
||||||
|
|
||||||
emit('select-vm', vm);
|
emit('select-vm', vm);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -596,7 +539,7 @@ const getStateClass = (state) => {
|
|||||||
watch(() => props.showList, (isVisible) => {
|
watch(() => props.showList, (isVisible) => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
// When list is closed, show selected VM
|
// When list is closed, show selected VM
|
||||||
const effectiveVM = props.selectedVM || props.vmFromUrl || localStorage.getItem('selectedVMId');
|
const effectiveVM = props.selectedVM || props.vmFromUrl;
|
||||||
if (effectiveVM) {
|
if (effectiveVM) {
|
||||||
inputValue.value = effectiveVM;
|
inputValue.value = effectiveVM;
|
||||||
|
|
||||||
@ -626,7 +569,7 @@ watch(() => props.showList, (isVisible) => {
|
|||||||
// Watch for changes in selectedVM or vmFromUrl
|
// Watch for changes in selectedVM or vmFromUrl
|
||||||
watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl]) => {
|
watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl]) => {
|
||||||
if (!props.showList) {
|
if (!props.showList) {
|
||||||
const effectiveVM = selectedVM || vmFromUrl || localStorage.getItem('selectedVMId');
|
const effectiveVM = selectedVM || vmFromUrl;
|
||||||
if (effectiveVM) {
|
if (effectiveVM) {
|
||||||
inputValue.value = effectiveVM;
|
inputValue.value = effectiveVM;
|
||||||
}
|
}
|
||||||
@ -636,7 +579,7 @@ watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl])
|
|||||||
// Add a function to scroll to the selected VM
|
// Add a function to scroll to the selected VM
|
||||||
const scrollToSelectedVM = () => {
|
const scrollToSelectedVM = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const selectedVMName = props.selectedVM || props.vmFromUrl || localStorage.getItem('selectedVMId');
|
const selectedVMName = props.selectedVM || props.vmFromUrl;
|
||||||
if (selectedVMName && vmListContainer.value) {
|
if (selectedVMName && vmListContainer.value) {
|
||||||
const activeElement = vmListContainer.value.querySelector(`[data-vm-name="${selectedVMName}"]`);
|
const activeElement = vmListContainer.value.querySelector(`[data-vm-name="${selectedVMName}"]`);
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
@ -716,12 +659,6 @@ const loadFiltersFromLocalStorage = () => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
transition: color 0.2s;
|
|
||||||
z-index: 5; /* Ensure it's above other elements */
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-clear:hover {
|
|
||||||
color: #dc3545; /* Red color on hover */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-toggle-btn {
|
.list-toggle-btn {
|
||||||
|
@ -153,52 +153,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
<!-- Health Details Section -->
|
|
||||||
<CollapsibleSection title="Health Details" :initially-expanded="sections.health">
|
|
||||||
<div class="health-details-row">
|
|
||||||
<!-- Health messages card -->
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h6 class="card-title mb-0 metric-label">Health Messages</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="messages-list">
|
|
||||||
<template v-if="nodeHealthMessages.length">
|
|
||||||
<div
|
|
||||||
v-for="(msg, idx) in nodeHealthMessages"
|
|
||||||
:key="idx"
|
|
||||||
:class="[
|
|
||||||
'health-message',
|
|
||||||
getHealthMessageClass(msg),
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="message-header">
|
|
||||||
<i class="fas" :class="getMessageIcon(msg)"></i>
|
|
||||||
<span class="message-id">{{ msg.name }}</span>
|
|
||||||
<span v-if="msg.health_delta > 0" class="health-delta">
|
|
||||||
(-{{ msg.health_delta }}%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">
|
|
||||||
{{ getMessageContent(msg) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="health-message healthy">
|
|
||||||
<div class="message-header">
|
|
||||||
<i class="fas fa-circle-check me-1"></i>
|
|
||||||
<span class="message-id">Node healthy</span>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">
|
|
||||||
Node is at full health with no faults
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleSection>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No node selected message -->
|
<!-- No node selected message -->
|
||||||
@ -250,8 +204,7 @@ const sections = ref({
|
|||||||
graphs: true,
|
graphs: true,
|
||||||
cpu: true,
|
cpu: true,
|
||||||
resources: true,
|
resources: true,
|
||||||
vms: true,
|
vms: true
|
||||||
health: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// State for selected node and tab scrolling
|
// State for selected node and tab scrolling
|
||||||
@ -464,12 +417,8 @@ const getDomainStateColor = (state) => {
|
|||||||
|
|
||||||
// Initialize the component
|
// Initialize the component
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Check if there's a saved node selection
|
// Select the first node by default if available
|
||||||
const savedNodeId = localStorage.getItem('selectedNodeId');
|
if (props.nodeData && props.nodeData.length > 0) {
|
||||||
if (savedNodeId && props.nodeData.some(node => node.name === savedNodeId)) {
|
|
||||||
selectNode(savedNodeId);
|
|
||||||
} else if (props.nodeData && props.nodeData.length > 0) {
|
|
||||||
// Fall back to selecting the first node
|
|
||||||
selectNode(props.nodeData[0].name);
|
selectNode(props.nodeData[0].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,57 +449,7 @@ const availableNodes = computed(() => {
|
|||||||
|
|
||||||
const handleNodeSelect = (node) => {
|
const handleNodeSelect = (node) => {
|
||||||
selectedNode.value = node;
|
selectedNode.value = node;
|
||||||
// Save to localStorage
|
// Any other node selection handling logic
|
||||||
localStorage.setItem('selectedNodeId', node);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process node health messages
|
|
||||||
const nodeHealthMessages = computed(() => {
|
|
||||||
if (!selectedNodeData.value || !selectedNodeData.value.health_details) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const healthDetails = selectedNodeData.value.health_details;
|
|
||||||
// Make sure we're getting an array of health details
|
|
||||||
if (Array.isArray(healthDetails)) {
|
|
||||||
return healthDetails;
|
|
||||||
} else if (typeof healthDetails === 'object') {
|
|
||||||
// If it's an object, convert to array
|
|
||||||
return Object.values(healthDetails);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to determine message class based on health status
|
|
||||||
const getHealthMessageClass = (msg) => {
|
|
||||||
if (msg.health_delta >= 25) {
|
|
||||||
return 'delta-high'; // Red for critical issues (>=25%)
|
|
||||||
}
|
|
||||||
if (msg.health_delta > 0) {
|
|
||||||
return 'delta-medium';
|
|
||||||
}
|
|
||||||
return 'delta-low'; // Green for healthy items
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to get appropriate icon for health message
|
|
||||||
const getMessageIcon = (msg) => {
|
|
||||||
if (msg.health_delta >= 25) {
|
|
||||||
return 'fa-circle-exclamation me-1'; // Warning icon for significant issues
|
|
||||||
}
|
|
||||||
if (msg.health_delta > 0) {
|
|
||||||
return 'fa-info-circle me-1'; // Info icon for minor issues
|
|
||||||
}
|
|
||||||
return 'fa-circle-check me-1'; // Checkmark for healthy items
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to format message content
|
|
||||||
const getMessageContent = (msg) => {
|
|
||||||
return msg.message || 'No details available';
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectVM = (vmId) => {
|
|
||||||
// Store the VM ID in localStorage so it will be selected when the VMs page loads
|
|
||||||
localStorage.setItem('selectedVMId', vmId);
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -577,12 +476,6 @@ const selectVM = (vmId) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-details-row {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive grid layouts */
|
/* Responsive grid layouts */
|
||||||
@media (min-width: 1201px) {
|
@media (min-width: 1201px) {
|
||||||
.info-row {
|
.info-row {
|
||||||
@ -600,10 +493,6 @@ const selectVM = (vmId) => {
|
|||||||
.resources-row-memory {
|
.resources-row-memory {
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-details-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 801px) and (max-width: 1200px) {
|
@media (min-width: 801px) and (max-width: 1200px) {
|
||||||
@ -622,10 +511,6 @@ const selectVM = (vmId) => {
|
|||||||
.resources-row-memory {
|
.resources-row-memory {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-details-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
@ -635,126 +520,6 @@ const selectVM = (vmId) => {
|
|||||||
.resources-row-memory {
|
.resources-row-memory {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-details-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-message {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
position: relative;
|
|
||||||
height: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
/* Default styling - will be overridden by delta classes */
|
|
||||||
background: rgba(108, 117, 125, 0.15);
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-id {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-line; /* Allow line breaks in content */
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-message.healthy {
|
|
||||||
background: rgba(40, 167, 69, 0.1);
|
|
||||||
color: #0d5524;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delta-low {
|
|
||||||
background: rgba(40, 167, 69, 0.15); /* Green background */
|
|
||||||
color: #0d5524;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delta-medium {
|
|
||||||
background: rgba(255, 193, 7, 0.15); /* Yellow background */
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delta-high {
|
|
||||||
background: rgba(220, 53, 69, 0.15); /* Red background */
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delta-info {
|
|
||||||
background: rgba(13, 110, 253, 0.15); /* Blue background */
|
|
||||||
color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-delta {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h6 {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card .card-body {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@ -15,7 +15,7 @@
|
|||||||
v-for="vmName in runningDomains"
|
v-for="vmName in runningDomains"
|
||||||
:key="vmName"
|
:key="vmName"
|
||||||
class="vm-item"
|
class="vm-item"
|
||||||
@click="selectVMAndNavigate(vmName)"
|
@click="selectVM(vmName)"
|
||||||
>
|
>
|
||||||
{{ vmName }}
|
{{ vmName }}
|
||||||
</div>
|
</div>
|
||||||
@ -26,7 +26,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { navigateToVM } from '../../../services/navigation';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vmData: {
|
vmData: {
|
||||||
@ -70,9 +69,18 @@ const runningDomains = computed(() => {
|
|||||||
return nodeData.value.running_domains;
|
return nodeData.value.running_domains;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigate to VM details using the navigation service
|
// Navigate to VM details
|
||||||
const selectVMAndNavigate = (vmName) => {
|
const selectVM = (vmName) => {
|
||||||
navigateToVM(vmName, router);
|
console.log('Navigating to VM:', vmName);
|
||||||
|
|
||||||
|
// Find the full VM data if available
|
||||||
|
const vmData = props.vmData.find(vm => vm.name === vmName);
|
||||||
|
console.log('Found VM data:', vmData);
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
path: '/vms',
|
||||||
|
query: { vm: vmName }
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
@clear="clearSearch"
|
@clear="clearSearch"
|
||||||
@toggle-list="toggleVMList"
|
@toggle-list="toggleVMList"
|
||||||
@select-vm="selectVM"
|
@select-vm="selectVM"
|
||||||
@clear-vm="clearSelectedVM"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- VM Details -->
|
<!-- VM Details -->
|
||||||
@ -148,7 +147,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use ref and sync with route
|
// Use ref and sync with route
|
||||||
const selectedVM = ref('');
|
const selectedVM = ref(route.query.vm || '');
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const showVMList = ref(true);
|
const showVMList = ref(true);
|
||||||
const searchActive = ref(false);
|
const searchActive = ref(false);
|
||||||
@ -212,10 +211,8 @@ const selectVM = (vm) => {
|
|||||||
// Close the VM list
|
// Close the VM list
|
||||||
showVMList.value = false;
|
showVMList.value = false;
|
||||||
|
|
||||||
// Save to localStorage
|
// Update the URL
|
||||||
localStorage.setItem('selectedVMId', vm.name);
|
router.push({ query: { vm: vm.name } });
|
||||||
|
|
||||||
// No URL parameter update
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear search
|
// Clear search
|
||||||
@ -225,33 +222,9 @@ const clearSearch = () => {
|
|||||||
localStorage.removeItem('vmSearchQuery');
|
localStorage.removeItem('vmSearchQuery');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add a method to clear the selected VM
|
|
||||||
const clearSelectedVM = () => {
|
|
||||||
// Clear the selected VM
|
|
||||||
selectedVM.value = '';
|
|
||||||
invalidVMSelected.value = false;
|
|
||||||
|
|
||||||
// Show the VM list
|
|
||||||
showVMList.value = true;
|
|
||||||
|
|
||||||
// Clear from localStorage
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
|
|
||||||
// Clear URL parameter if it exists
|
|
||||||
if (route.query.vm) {
|
|
||||||
router.replace({ path: '/vms' }, { replace: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
// Lifecycle hooks
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('VMOverview mounted');
|
console.log('VMOverview mounted, route.query.vm:', route.query.vm);
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
// Check sources in priority order:
|
|
||||||
// 1. localStorage (for returning to the page or navigating from other pages)
|
|
||||||
// 2. URL query parameter (for backward compatibility)
|
|
||||||
const savedVMId = localStorage.getItem('selectedVMId');
|
|
||||||
const vmFromQuery = route.query.vm;
|
const vmFromQuery = route.query.vm;
|
||||||
|
|
||||||
// Restore previous search query from localStorage if available
|
// Restore previous search query from localStorage if available
|
||||||
@ -260,69 +233,64 @@ onMounted(() => {
|
|||||||
searchQuery.value = savedSearch;
|
searchQuery.value = savedSearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
let vmToSelect = null;
|
if (vmFromQuery) {
|
||||||
|
console.log('Setting selectedVM to:', vmFromQuery);
|
||||||
// Determine which VM to select
|
selectedVM.value = vmFromQuery;
|
||||||
if (savedVMId) {
|
isLoading.value = true;
|
||||||
console.log('VM from localStorage:', savedVMId);
|
showVMList.value = false;
|
||||||
vmToSelect = savedVMId;
|
|
||||||
} else if (vmFromQuery) {
|
|
||||||
console.log('VM from URL query:', vmFromQuery);
|
|
||||||
vmToSelect = vmFromQuery;
|
|
||||||
// Save to localStorage for persistence
|
|
||||||
localStorage.setItem('selectedVMId', vmFromQuery);
|
|
||||||
|
|
||||||
// Remove the VM ID from the URL without reloading the page
|
// If we have data, verify the VM exists
|
||||||
router.replace({ path: '/vms' }, { replace: true });
|
if (props.vmData.length) {
|
||||||
}
|
const vmExists = props.vmData.some(v => v.name === vmFromQuery);
|
||||||
|
invalidVMSelected.value = !vmExists;
|
||||||
// If we have a VM to select and data is available
|
|
||||||
if (vmToSelect && props.vmData.length > 0) {
|
|
||||||
// Check if the VM exists
|
|
||||||
const vmExists = props.vmData.some(v => v.name === vmToSelect);
|
|
||||||
|
|
||||||
if (vmExists) {
|
|
||||||
selectedVM.value = vmToSelect;
|
|
||||||
showVMList.value = false;
|
|
||||||
|
|
||||||
// Set the search query to the VM name for display in the search bar
|
|
||||||
searchQuery.value = vmToSelect;
|
|
||||||
} else {
|
|
||||||
console.log('Selected VM does not exist:', vmToSelect);
|
|
||||||
invalidVMSelected.value = true;
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
}
|
}
|
||||||
} else if (!vmToSelect && props.vmData.length > 0) {
|
isLoading.value = false;
|
||||||
// No VM selected, show the list
|
} else if (props.vmData.length > 0) {
|
||||||
showVMList.value = true;
|
showVMList.value = true;
|
||||||
}
|
invalidVMSelected.value = false;
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch for changes in the selected VM
|
|
||||||
watch(selectedVM, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
// Save to localStorage whenever it changes
|
|
||||||
localStorage.setItem('selectedVMId', newValue);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for changes in the VM data
|
onUnmounted(() => {
|
||||||
|
// Remove event listeners and clean up resources
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for route changes to update selectedVM
|
||||||
|
watch(() => route.query.vm, (newVm) => {
|
||||||
|
// Explicitly update the ref when route changes
|
||||||
|
selectedVM.value = newVm || '';
|
||||||
|
if (newVm) {
|
||||||
|
// Just update UI state, don't trigger another route change
|
||||||
|
invalidVMSelected.value = false;
|
||||||
|
showVMList.value = false;
|
||||||
|
} else if (props.vmData.length > 0) {
|
||||||
|
showVMList.value = true;
|
||||||
|
invalidVMSelected.value = false;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Watch for changes in the VM list visibility
|
||||||
|
watch(() => showVMList.value, (isVisible) => {
|
||||||
|
if (isVisible && selectedVM.value) {
|
||||||
|
// Scroll to selected VM when the list becomes visible
|
||||||
|
nextTick(() => {
|
||||||
|
// Scroll to selected VM logic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add watcher for vmData to handle loading state
|
||||||
watch(() => props.vmData, (newData) => {
|
watch(() => props.vmData, (newData) => {
|
||||||
if (selectedVM.value && newData.length > 0) {
|
if (selectedVM.value && newData.length) {
|
||||||
// Verify the VM still exists
|
|
||||||
const vmExists = newData.some(vm => vm.name === selectedVM.value);
|
const vmExists = newData.some(vm => vm.name === selectedVM.value);
|
||||||
invalidVMSelected.value = !vmExists;
|
invalidVMSelected.value = !vmExists;
|
||||||
|
|
||||||
if (!vmExists) {
|
|
||||||
// If VM no longer exists, clear localStorage
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Add debug watcher
|
||||||
|
watch(() => selectedVM.value, (newVal) => {
|
||||||
|
console.log('VMOverview selectedVM changed:', newVal);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const getStateColor = (state) => {
|
const getStateColor = (state) => {
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a VM without using URL parameters
|
|
||||||
* @param {string} vmId - The ID of the VM to navigate to
|
|
||||||
* @param {import('vue-router').Router} router - Vue Router instance
|
|
||||||
*/
|
|
||||||
export function navigateToVM(vmId, router) {
|
|
||||||
// Save the VM ID to localStorage
|
|
||||||
localStorage.setItem('selectedVMId', vmId);
|
|
||||||
|
|
||||||
// Navigate to the VMs page
|
|
||||||
router.push('/vms');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a node without using URL parameters
|
|
||||||
* @param {string} nodeId - The ID of the node to navigate to
|
|
||||||
* @param {import('vue-router').Router} router - Vue Router instance
|
|
||||||
*/
|
|
||||||
export function navigateToNode(nodeId, router) {
|
|
||||||
// Save the node ID to localStorage
|
|
||||||
localStorage.setItem('selectedNodeId', nodeId);
|
|
||||||
|
|
||||||
// Navigate to the Nodes page
|
|
||||||
router.push('/nodes');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the selected VM and return to the default VM page view
|
|
||||||
* @param {import('vue-router').Router} router - Vue Router instance
|
|
||||||
*/
|
|
||||||
export function clearSelectedVM(router) {
|
|
||||||
// Clear from localStorage
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
|
|
||||||
// Navigate to the VMs page without query parameters
|
|
||||||
router.push('/vms');
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Selects a VM and saves the selection to localStorage
|
|
||||||
* @param {string} vmId - The ID of the VM to select
|
|
||||||
* @param {import('vue-router').Router} router - Vue Router instance (optional)
|
|
||||||
*/
|
|
||||||
export function selectVM(vmId, router = null) {
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem('selectedVMId', vmId);
|
|
||||||
|
|
||||||
// Optionally update URL if router is provided
|
|
||||||
if (router) {
|
|
||||||
router.push({
|
|
||||||
path: '/vms',
|
|
||||||
query: { vm: vmId }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the currently selected VM from localStorage
|
|
||||||
* @returns {string|null} The selected VM ID or null if none is selected
|
|
||||||
*/
|
|
||||||
export function getSelectedVM() {
|
|
||||||
return localStorage.getItem('selectedVMId');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the selected VM from localStorage
|
|
||||||
*/
|
|
||||||
export function clearSelectedVM() {
|
|
||||||
localStorage.removeItem('selectedVMId');
|
|
||||||
}
|
|
@ -10,7 +10,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
|
||||||
import PageTitle from '../components/general/PageTitle.vue';
|
import PageTitle from '../components/general/PageTitle.vue';
|
||||||
import NodeOverview from '../components/pages/nodes/NodeOverview.vue';
|
import NodeOverview from '../components/pages/nodes/NodeOverview.vue';
|
||||||
|
|
||||||
@ -31,25 +30,6 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedNode = ref(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// Restore selected node from localStorage if available
|
|
||||||
const savedNodeId = localStorage.getItem('selectedNodeId');
|
|
||||||
if (savedNodeId) {
|
|
||||||
selectedNode.value = savedNodeId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(selectedNode, (newValue) => {
|
|
||||||
// Save selected node to localStorage whenever it changes
|
|
||||||
if (newValue) {
|
|
||||||
localStorage.setItem('selectedNodeId', newValue);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('selectedNodeId');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -11,14 +11,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
|
||||||
import PageTitle from '../components/general/PageTitle.vue';
|
import PageTitle from '../components/general/PageTitle.vue';
|
||||||
import VMOverview from '../components/pages/vms/VMOverview.vue';
|
import VMOverview from '../components/pages/vms/VMOverview.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vmData: {
|
vmData: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -40,9 +35,6 @@ const props = defineProps({
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// We'll handle the VM selection in the VMOverview component
|
|
||||||
// This parent component just needs to pass the data
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user