Compare commits
4 Commits
e6da8ec2c0
...
8d1028ab47
Author | SHA1 | Date | |
---|---|---|---|
|
8d1028ab47 | ||
|
e26c5defa4 | ||
|
61dbe8eed1 | ||
|
779dbe1632 |
@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@ -26,15 +26,48 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'select']);
|
||||
|
||||
const selectedNode = ref(props.value || props.modelValue);
|
||||
|
||||
const selectNode = (node) => {
|
||||
selectedNode.value = node;
|
||||
emit('update:modelValue', 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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -27,9 +27,10 @@
|
||||
class="form-control search-input"
|
||||
>
|
||||
<button
|
||||
v-if="modelValue && showList && showClearButton"
|
||||
v-if="shouldShowClearButton"
|
||||
class="btn-clear"
|
||||
@click.stop="clearSearch"
|
||||
@click.stop="handleClearButton"
|
||||
:title="showList ? 'Clear search' : 'Clear selected VM'"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@ -158,7 +159,8 @@ const emit = defineEmits([
|
||||
'toggle-list',
|
||||
'select-vm',
|
||||
'toggle-filter',
|
||||
'reset-filters'
|
||||
'reset-filters',
|
||||
'clear-vm'
|
||||
]);
|
||||
|
||||
// UI state refs
|
||||
@ -179,11 +181,23 @@ const appliedFilters = ref({
|
||||
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
|
||||
onMounted(() => {
|
||||
// Set up click outside handler
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
// Get the selected VM from localStorage
|
||||
const savedVMId = localStorage.getItem('selectedVMId');
|
||||
|
||||
// Restore search text from localStorage if available
|
||||
const savedSearchText = localStorage.getItem('vmSearchText');
|
||||
if (savedSearchText) {
|
||||
@ -194,9 +208,20 @@ onMounted(() => {
|
||||
loadFiltersFromLocalStorage();
|
||||
|
||||
// Initialize input value based on selected VM
|
||||
if (!props.showList && (props.selectedVM || props.vmFromUrl)) {
|
||||
inputValue.value = props.selectedVM || props.vmFromUrl;
|
||||
if (!props.showList) {
|
||||
// If list is closed, show the selected VM name in the input
|
||||
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) {
|
||||
// If list is open and there's a search query, show that
|
||||
inputValue.value = props.modelValue;
|
||||
searchText.value = props.modelValue;
|
||||
// Save to localStorage
|
||||
@ -204,7 +229,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// If the list is visible on mount, scroll to selected VM
|
||||
if (props.showList && (props.selectedVM || props.vmFromUrl)) {
|
||||
if (props.showList && (props.selectedVM || props.vmFromUrl || savedVMId)) {
|
||||
scrollToSelectedVM();
|
||||
}
|
||||
});
|
||||
@ -323,6 +348,17 @@ const toggleList = () => {
|
||||
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
|
||||
const clearSearch = () => {
|
||||
inputValue.value = '';
|
||||
@ -347,10 +383,28 @@ 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
|
||||
const handleVMSelect = (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
|
||||
if (props.modelValue) {
|
||||
searchText.value = props.modelValue;
|
||||
@ -358,6 +412,9 @@ const handleVMSelect = (vm) => {
|
||||
localStorage.setItem('vmSearchText', props.modelValue);
|
||||
}
|
||||
|
||||
// Save selected VM to localStorage
|
||||
localStorage.setItem('selectedVMId', vm.name);
|
||||
|
||||
emit('select-vm', vm);
|
||||
};
|
||||
|
||||
@ -539,7 +596,7 @@ const getStateClass = (state) => {
|
||||
watch(() => props.showList, (isVisible) => {
|
||||
if (!isVisible) {
|
||||
// When list is closed, show selected VM
|
||||
const effectiveVM = props.selectedVM || props.vmFromUrl;
|
||||
const effectiveVM = props.selectedVM || props.vmFromUrl || localStorage.getItem('selectedVMId');
|
||||
if (effectiveVM) {
|
||||
inputValue.value = effectiveVM;
|
||||
|
||||
@ -569,7 +626,7 @@ watch(() => props.showList, (isVisible) => {
|
||||
// Watch for changes in selectedVM or vmFromUrl
|
||||
watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl]) => {
|
||||
if (!props.showList) {
|
||||
const effectiveVM = selectedVM || vmFromUrl;
|
||||
const effectiveVM = selectedVM || vmFromUrl || localStorage.getItem('selectedVMId');
|
||||
if (effectiveVM) {
|
||||
inputValue.value = effectiveVM;
|
||||
}
|
||||
@ -579,7 +636,7 @@ watch([() => props.selectedVM, () => props.vmFromUrl], ([selectedVM, vmFromUrl])
|
||||
// Add a function to scroll to the selected VM
|
||||
const scrollToSelectedVM = () => {
|
||||
nextTick(() => {
|
||||
const selectedVMName = props.selectedVM || props.vmFromUrl;
|
||||
const selectedVMName = props.selectedVM || props.vmFromUrl || localStorage.getItem('selectedVMId');
|
||||
if (selectedVMName && vmListContainer.value) {
|
||||
const activeElement = vmListContainer.value.querySelector(`[data-vm-name="${selectedVMName}"]`);
|
||||
if (activeElement) {
|
||||
@ -659,6 +716,12 @@ const loadFiltersFromLocalStorage = () => {
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
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 {
|
||||
|
@ -153,6 +153,52 @@
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- No node selected message -->
|
||||
@ -204,7 +250,8 @@ const sections = ref({
|
||||
graphs: true,
|
||||
cpu: true,
|
||||
resources: true,
|
||||
vms: true
|
||||
vms: true,
|
||||
health: true
|
||||
});
|
||||
|
||||
// State for selected node and tab scrolling
|
||||
@ -417,8 +464,12 @@ const getDomainStateColor = (state) => {
|
||||
|
||||
// Initialize the component
|
||||
onMounted(() => {
|
||||
// Select the first node by default if available
|
||||
if (props.nodeData && props.nodeData.length > 0) {
|
||||
// Check if there's a saved node selection
|
||||
const savedNodeId = localStorage.getItem('selectedNodeId');
|
||||
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);
|
||||
}
|
||||
|
||||
@ -449,7 +500,57 @@ const availableNodes = computed(() => {
|
||||
|
||||
const handleNodeSelect = (node) => {
|
||||
selectedNode.value = node;
|
||||
// Any other node selection handling logic
|
||||
// Save to localStorage
|
||||
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>
|
||||
|
||||
@ -476,6 +577,12 @@ const handleNodeSelect = (node) => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.health-details-row {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive grid layouts */
|
||||
@media (min-width: 1201px) {
|
||||
.info-row {
|
||||
@ -493,6 +600,10 @@ const handleNodeSelect = (node) => {
|
||||
.resources-row-memory {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
.health-details-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 801px) and (max-width: 1200px) {
|
||||
@ -511,6 +622,10 @@ const handleNodeSelect = (node) => {
|
||||
.resources-row-memory {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.health-details-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
@ -520,6 +635,126 @@ const handleNodeSelect = (node) => {
|
||||
.resources-row-memory {
|
||||
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>
|
@ -15,7 +15,7 @@
|
||||
v-for="vmName in runningDomains"
|
||||
:key="vmName"
|
||||
class="vm-item"
|
||||
@click="selectVM(vmName)"
|
||||
@click="selectVMAndNavigate(vmName)"
|
||||
>
|
||||
{{ vmName }}
|
||||
</div>
|
||||
@ -26,6 +26,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { navigateToVM } from '../../../services/navigation';
|
||||
|
||||
const props = defineProps({
|
||||
vmData: {
|
||||
@ -69,18 +70,9 @@ const runningDomains = computed(() => {
|
||||
return nodeData.value.running_domains;
|
||||
});
|
||||
|
||||
// Navigate to VM details
|
||||
const selectVM = (vmName) => {
|
||||
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 }
|
||||
});
|
||||
// Navigate to VM details using the navigation service
|
||||
const selectVMAndNavigate = (vmName) => {
|
||||
navigateToVM(vmName, router);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
@clear="clearSearch"
|
||||
@toggle-list="toggleVMList"
|
||||
@select-vm="selectVM"
|
||||
@clear-vm="clearSelectedVM"
|
||||
/>
|
||||
|
||||
<!-- VM Details -->
|
||||
@ -147,7 +148,7 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
// Use ref and sync with route
|
||||
const selectedVM = ref(route.query.vm || '');
|
||||
const selectedVM = ref('');
|
||||
const searchQuery = ref('');
|
||||
const showVMList = ref(true);
|
||||
const searchActive = ref(false);
|
||||
@ -211,8 +212,10 @@ const selectVM = (vm) => {
|
||||
// Close the VM list
|
||||
showVMList.value = false;
|
||||
|
||||
// Update the URL
|
||||
router.push({ query: { vm: vm.name } });
|
||||
// Save to localStorage
|
||||
localStorage.setItem('selectedVMId', vm.name);
|
||||
|
||||
// No URL parameter update
|
||||
};
|
||||
|
||||
// Clear search
|
||||
@ -222,9 +225,33 @@ const clearSearch = () => {
|
||||
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
|
||||
onMounted(() => {
|
||||
console.log('VMOverview mounted, route.query.vm:', route.query.vm);
|
||||
console.log('VMOverview mounted');
|
||||
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;
|
||||
|
||||
// Restore previous search query from localStorage if available
|
||||
@ -233,64 +260,69 @@ onMounted(() => {
|
||||
searchQuery.value = savedSearch;
|
||||
}
|
||||
|
||||
if (vmFromQuery) {
|
||||
console.log('Setting selectedVM to:', vmFromQuery);
|
||||
selectedVM.value = vmFromQuery;
|
||||
isLoading.value = true;
|
||||
showVMList.value = false;
|
||||
let vmToSelect = null;
|
||||
|
||||
// If we have data, verify the VM exists
|
||||
if (props.vmData.length) {
|
||||
const vmExists = props.vmData.some(v => v.name === vmFromQuery);
|
||||
invalidVMSelected.value = !vmExists;
|
||||
// Determine which VM to select
|
||||
if (savedVMId) {
|
||||
console.log('VM from localStorage:', savedVMId);
|
||||
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
|
||||
router.replace({ path: '/vms' }, { replace: true });
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
isLoading.value = false;
|
||||
} else if (props.vmData.length > 0) {
|
||||
} else if (!vmToSelect && props.vmData.length > 0) {
|
||||
// No VM selected, show the list
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
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 for changes in the VM data
|
||||
watch(() => props.vmData, (newData) => {
|
||||
if (selectedVM.value && newData.length) {
|
||||
if (selectedVM.value && newData.length > 0) {
|
||||
// Verify the VM still exists
|
||||
const vmExists = newData.some(vm => vm.name === selectedVM.value);
|
||||
invalidVMSelected.value = !vmExists;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Add debug watcher
|
||||
watch(() => selectedVM.value, (newVal) => {
|
||||
console.log('VMOverview selectedVM changed:', newVal);
|
||||
}, { immediate: true });
|
||||
if (!vmExists) {
|
||||
// If VM no longer exists, clear localStorage
|
||||
localStorage.removeItem('selectedVMId');
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Helper functions
|
||||
const getStateColor = (state) => {
|
||||
|
39
pvc-vue/src/services/navigation.js
Normal file
39
pvc-vue/src/services/navigation.js
Normal file
@ -0,0 +1,39 @@
|
||||
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');
|
||||
}
|
32
pvc-vue/src/utils/vmSelection.js
Normal file
32
pvc-vue/src/utils/vmSelection.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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,6 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import PageTitle from '../components/general/PageTitle.vue';
|
||||
import NodeOverview from '../components/pages/nodes/NodeOverview.vue';
|
||||
|
||||
@ -30,6 +31,25 @@ const props = defineProps({
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -11,9 +11,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import PageTitle from '../components/general/PageTitle.vue';
|
||||
import VMOverview from '../components/pages/vms/VMOverview.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps({
|
||||
vmData: {
|
||||
type: Array,
|
||||
@ -35,6 +40,9 @@ const props = defineProps({
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// We'll handle the VM selection in the VMOverview component
|
||||
// This parent component just needs to pass the data
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
Loading…
x
Reference in New Issue
Block a user