Compare commits

..

5 Commits

Author SHA1 Message Date
Joshua Boniface
41d9ef4057 Jump to current VM in full VM list 2025-03-01 14:34:50 -05:00
Joshua Boniface
dbb1dae0d9 Improve search and list behaviour 2025-03-01 14:31:01 -05:00
Joshua Boniface
1dd1387624 Enhance search bar and hide filters when unneeded 2025-03-01 14:26:59 -05:00
Joshua Boniface
21d98f1a98 Remove X from search bar when not opened 2025-03-01 14:21:42 -05:00
Joshua Boniface
4a9b48caa0 Reorder state filters so start is first 2025-03-01 14:19:26 -05:00

View File

@ -21,10 +21,11 @@
:value="showVMList ? searchQuery : (selectedVMData?.name || '')" :value="showVMList ? searchQuery : (selectedVMData?.name || '')"
@input="handleSearch" @input="handleSearch"
@focus="handleSearchFocus" @focus="handleSearchFocus"
@blur="handleSearchBlur"
class="form-control search-input" class="form-control search-input"
> >
<button <button
v-if="searchQuery" v-if="searchQuery && showVMList"
class="btn-clear" class="btn-clear"
@click="clearSearch" @click="clearSearch"
> >
@ -35,6 +36,7 @@
<div class="filter-dropdown"> <div class="filter-dropdown">
<button <button
class="btn btn-outline-secondary dropdown-toggle" class="btn btn-outline-secondary dropdown-toggle"
:disabled="!showVMList"
@click="toggleFilterMenu" @click="toggleFilterMenu"
> >
<i class="fas fa-filter"></i> Filters <i class="fas fa-filter"></i> Filters
@ -96,6 +98,7 @@
class="vm-list-item" class="vm-list-item"
:class="{ 'active': selectedVM === vm.name }" :class="{ 'active': selectedVM === vm.name }"
@click="selectVM(vm.name)" @click="selectVM(vm.name)"
:ref="el => { if (vm.name === selectedVM) selectedVMRef = el; }"
> >
<div class="vm-item-content"> <div class="vm-item-content">
<div class="vm-name">{{ vm.name }}</div> <div class="vm-name">{{ vm.name }}</div>
@ -246,7 +249,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import PageTitle from '../components/PageTitle.vue'; import PageTitle from '../components/PageTitle.vue';
import { useApiStore } from '../stores/api'; import { useApiStore } from '../stores/api';
@ -261,10 +264,12 @@ const selectedVM = ref('');
const searchQuery = ref(''); const searchQuery = ref('');
const showVMList = ref(true); const showVMList = ref(true);
const showFilterMenu = ref(false); const showFilterMenu = ref(false);
const searchActive = ref(false);
const appliedFilters = ref({ const appliedFilters = ref({
states: {}, states: {},
nodes: {} nodes: {}
}); });
const selectedVMRef = ref(null);
// Section visibility state // Section visibility state
const sections = ref({ const sections = ref({
@ -276,7 +281,20 @@ const sections = ref({
// Toggle VM list visibility // Toggle VM list visibility
const toggleVMList = () => { const toggleVMList = () => {
showVMList.value = !showVMList.value; if (showVMList.value) {
showVMList.value = false;
return;
}
showVMList.value = true;
searchActive.value = false;
// Scroll to selected VM after the list is shown
if (selectedVM.value) {
nextTick(() => {
scrollToSelectedVM();
});
}
}; };
// Toggle filter menu // Toggle filter menu
@ -295,13 +313,31 @@ const closeFilterMenuOnClickOutside = (event) => {
// Add event listener for clicks outside filter menu // Add event listener for clicks outside filter menu
onMounted(() => { onMounted(() => {
document.addEventListener('click', closeFilterMenuOnClickOutside); document.addEventListener('click', closeFilterMenuOnClickOutside);
// Add event listener for clicks outside the VM list area
document.addEventListener('click', handleClickOutside);
}); });
// Remove event listener when component is unmounted // Remove event listener when component is unmounted
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', closeFilterMenuOnClickOutside); document.removeEventListener('click', closeFilterMenuOnClickOutside);
document.removeEventListener('click', handleClickOutside);
}); });
// Handle clicks outside the VM list area
const handleClickOutside = (event) => {
// Only handle this if a VM is selected and the list is showing
if (selectedVM.value && showVMList.value) {
const vmControls = document.querySelector('.vm-controls-container');
const vmList = document.querySelector('.vm-list-fullpage');
// If click is outside both the controls and list, close the list
if ((!vmControls || !vmControls.contains(event.target)) &&
(!vmList || !vmList.contains(event.target))) {
showVMList.value = false;
}
}
};
// Toggle section visibility // Toggle section visibility
const toggleSection = (section) => { const toggleSection = (section) => {
sections.value[section] = !sections.value[section]; sections.value[section] = !sections.value[section];
@ -310,18 +346,19 @@ const toggleSection = (section) => {
// Clear search // Clear search
const clearSearch = () => { const clearSearch = () => {
searchQuery.value = ''; searchQuery.value = '';
searchActive.value = false;
filterVMs(); filterVMs();
}; };
// Toggle a filter on/off // Toggle a filter on/off
const toggleFilter = (type, value) => { const toggleFilter = (type, value) => {
appliedFilters.value[type][value] = !appliedFilters.value[type][value]; appliedFilters.value[type][value] = !appliedFilters.value[type][value];
searchActive.value = true;
filterVMs(); filterVMs();
}; };
// Reset filters // Reset all filters
const resetFilters = () => { const resetFilters = () => {
// Reset all filters and apply immediately
Object.keys(appliedFilters.value.states).forEach(state => { Object.keys(appliedFilters.value.states).forEach(state => {
appliedFilters.value.states[state] = false; appliedFilters.value.states[state] = false;
}); });
@ -330,14 +367,23 @@ const resetFilters = () => {
appliedFilters.value.nodes[node] = false; appliedFilters.value.nodes[node] = false;
}); });
searchActive.value = false;
filterVMs(); filterVMs();
}; };
// Count active filters // Count active filters
const activeFiltersCount = computed(() => { const activeFiltersCount = computed(() => {
const stateFiltersCount = Object.values(appliedFilters.value.states).filter(v => v).length; let count = 0;
const nodeFiltersCount = Object.values(appliedFilters.value.nodes).filter(v => v).length;
return stateFiltersCount + nodeFiltersCount; Object.values(appliedFilters.value.states).forEach(isActive => {
if (isActive) count++;
});
Object.values(appliedFilters.value.nodes).forEach(isActive => {
if (isActive) count++;
});
return count;
}); });
// Get available states from VM data // Get available states from VM data
@ -346,7 +392,24 @@ const availableStates = computed(() => {
vmData.value.forEach(vm => { vmData.value.forEach(vm => {
if (vm.state) states.add(vm.state); if (vm.state) states.add(vm.state);
}); });
return Array.from(states).sort(); // Get all states as an array
const statesArray = Array.from(states);
// Remove 'start' if it exists
const startIndex = statesArray.indexOf('start');
if (startIndex !== -1) {
statesArray.splice(startIndex, 1);
}
// Sort the remaining states
statesArray.sort();
// Add 'start' at the beginning if it was present
if (startIndex !== -1) {
statesArray.unshift('start');
}
return statesArray;
}); });
// Get available nodes from VM data // Get available nodes from VM data
@ -364,28 +427,31 @@ const filteredVMs = computed(() => {
let filtered = [...vmData.value]; let filtered = [...vmData.value];
// Apply search filter if (searchActive.value && searchQuery.value) {
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
filtered = filtered.filter(vm => filtered = filtered.filter(vm =>
vm.name.toLowerCase().includes(query) vm.name.toLowerCase().includes(query)
); );
} }
// Apply state filters if (searchActive.value) {
const hasStateFilters = Object.values(appliedFilters.value.states).some(v => v); // Apply state filters if any are active
if (hasStateFilters) { const activeStates = Object.entries(appliedFilters.value.states)
filtered = filtered.filter(vm => { .filter(([_, isActive]) => isActive)
return appliedFilters.value.states[vm.state] === true; .map(([state]) => state);
});
if (activeStates.length > 0) {
filtered = filtered.filter(vm => activeStates.includes(vm.state));
} }
// Apply node filters // Apply node filters if any are active
const hasNodeFilters = Object.values(appliedFilters.value.nodes).some(v => v); const activeNodes = Object.entries(appliedFilters.value.nodes)
if (hasNodeFilters) { .filter(([_, isActive]) => isActive)
filtered = filtered.filter(vm => { .map(([node]) => node);
return appliedFilters.value.nodes[vm.node] === true;
}); if (activeNodes.length > 0) {
filtered = filtered.filter(vm => activeNodes.includes(vm.node));
}
} }
return filtered; return filtered;
@ -399,19 +465,17 @@ const selectedVMData = computed(() => {
// Get status class based on VM state // Get status class based on VM state
const getStatusClass = (state) => { const getStatusClass = (state) => {
if (!state) return 'status-unknown'; if (!state) return '';
switch (state) { switch(state.toLowerCase()) {
case 'start': case 'start':
return 'status-running'; return 'status-running';
case 'shutdown': case 'stop':
return 'status-stopped'; return 'status-stopped';
case 'pause': case 'disable':
return 'status-paused'; return 'status-disabled';
case 'crash':
return 'status-error';
default: default:
return 'status-unknown'; return '';
} }
}; };
@ -451,6 +515,7 @@ const fetchVMData = async () => {
// Handle search input // Handle search input
const handleSearch = (event) => { const handleSearch = (event) => {
searchQuery.value = event.target.value; searchQuery.value = event.target.value;
searchActive.value = true;
filterVMs(); filterVMs();
}; };
@ -459,6 +524,34 @@ const handleSearchFocus = () => {
// Show VM list when search is focused // Show VM list when search is focused
if (!showVMList.value) { if (!showVMList.value) {
showVMList.value = true; showVMList.value = true;
// Scroll to selected VM after the list is shown
if (selectedVM.value) {
nextTick(() => {
scrollToSelectedVM();
});
}
}
searchActive.value = true;
};
// Handle search blur
const handleSearchBlur = (event) => {
// Don't close the list if clicking on another element within the list
const vmList = document.querySelector('.vm-list-fullpage');
if (vmList && vmList.contains(event.relatedTarget)) {
return;
}
// If a VM is selected and user clicks away from search, close the list
if (selectedVM.value && !event.relatedTarget) {
// Use setTimeout to allow click events to process first
setTimeout(() => {
// Only close if we're not clicking on a VM in the list
if (!document.activeElement || document.activeElement.tagName !== 'BUTTON' ||
!document.activeElement.classList.contains('vm-list-item')) {
showVMList.value = false;
}
}, 100);
} }
}; };
@ -475,6 +568,25 @@ const selectVM = (vmName) => {
showVMList.value = false; showVMList.value = false;
}; };
// Scroll to the selected VM in the list
const scrollToSelectedVM = () => {
if (selectedVMRef.value) {
// Get the VM list container
const vmList = document.querySelector('.vm-list');
if (!vmList) return;
// Calculate the scroll position
const vmElement = selectedVMRef.value;
const vmPosition = vmElement.offsetTop;
const listHeight = vmList.clientHeight;
const vmHeight = vmElement.clientHeight;
// Scroll to position the selected VM in the middle of the list if possible
const scrollPosition = vmPosition - (listHeight / 2) + (vmHeight / 2);
vmList.scrollTop = Math.max(0, scrollPosition);
}
};
onMounted(() => { onMounted(() => {
fetchVMData(); fetchVMData();
}); });
@ -491,12 +603,21 @@ watch(() => route.query.vm, (newVm) => {
showVMList.value = true; showVMList.value = 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(() => {
scrollToSelectedVM();
});
}
});
</script> </script>
<style scoped> <style scoped>
/* VM Controls Styles */ /* VM Controls */
.vm-controls-container { .vm-controls-container {
margin-bottom: 0.5rem;
background-color: white; background-color: white;
border: 1px solid rgba(0, 0, 0, 0.125); border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.25rem; border-radius: 0.25rem;
@ -677,12 +798,15 @@ watch(() => route.query.vm, (newVm) => {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: calc(100vh - 200px);
} }
.vm-list { .vm-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
overflow-y: auto;
max-height: calc(100vh - 200px);
} }
.vm-list-item { .vm-list-item {