Move remaining search components
This commit is contained in:
		@@ -1,27 +1,114 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="search-box">
 | 
					  <div class="controls-bar" ref="controlsBar">
 | 
				
			||||||
    <i class="fas fa-search search-icon"></i>
 | 
					    <div class="controls-row">
 | 
				
			||||||
    <input 
 | 
					      <button 
 | 
				
			||||||
      type="text" 
 | 
					        class="btn btn-outline-secondary list-toggle-btn" 
 | 
				
			||||||
      placeholder="Search VMs..." 
 | 
					        @click="toggleList"
 | 
				
			||||||
      :value="modelValue"
 | 
					        :class="{ 'active': showList }"
 | 
				
			||||||
      @input="handleSearch"
 | 
					      >
 | 
				
			||||||
      @focus="handleFocus"
 | 
					        <i class="fas fa-list"></i> List VMs
 | 
				
			||||||
      @blur="handleBlur"
 | 
					      </button>
 | 
				
			||||||
      class="form-control search-input"
 | 
					      
 | 
				
			||||||
    >
 | 
					      <div class="search-box">
 | 
				
			||||||
    <button 
 | 
					        <i class="fas fa-search search-icon"></i>
 | 
				
			||||||
      v-if="modelValue && showList && showClearButton" 
 | 
					        <input 
 | 
				
			||||||
      class="btn-clear" 
 | 
					          type="text" 
 | 
				
			||||||
      @click="clearSearch"
 | 
					          placeholder="Search VMs..." 
 | 
				
			||||||
    >
 | 
					          :value="!showList ? selectedVM : modelValue"
 | 
				
			||||||
      <i class="fas fa-times"></i>
 | 
					          @input="handleSearch"
 | 
				
			||||||
    </button>
 | 
					          @focus="handleFocus"
 | 
				
			||||||
 | 
					          @blur="handleBlur"
 | 
				
			||||||
 | 
					          class="form-control search-input"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					          v-if="modelValue && showList && showClearButton" 
 | 
				
			||||||
 | 
					          class="btn-clear" 
 | 
				
			||||||
 | 
					          @click="clearSearch"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <i class="fas fa-times"></i>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="filter-dropdown" ref="filterDropdown">
 | 
				
			||||||
 | 
					        <button 
 | 
				
			||||||
 | 
					          class="btn btn-outline-secondary dropdown-toggle" 
 | 
				
			||||||
 | 
					          :disabled="!showList"
 | 
				
			||||||
 | 
					          @click="toggleFilterMenu"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <i class="fas fa-filter"></i> Filters
 | 
				
			||||||
 | 
					          <span v-if="activeFiltersCount > 0" class="filter-badge">{{ activeFiltersCount }}</span>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <div class="filter-menu" v-show="showFilterMenu" ref="filterMenu">
 | 
				
			||||||
 | 
					          <div class="filter-section">
 | 
				
			||||||
 | 
					            <h6>Status</h6>
 | 
				
			||||||
 | 
					            <div class="filter-options-dropdown">
 | 
				
			||||||
 | 
					              <div class="filter-pills">
 | 
				
			||||||
 | 
					                <button 
 | 
				
			||||||
 | 
					                  v-for="state in availableStates" 
 | 
				
			||||||
 | 
					                  :key="state"
 | 
				
			||||||
 | 
					                  class="filter-pill"
 | 
				
			||||||
 | 
					                  :class="{ 'active': appliedFilters.states[state] }"
 | 
				
			||||||
 | 
					                  @click="handleFilterToggle('states', state)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {{ state }}
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="filter-section">
 | 
				
			||||||
 | 
					            <h6>Node</h6>
 | 
				
			||||||
 | 
					            <div class="filter-options-dropdown">
 | 
				
			||||||
 | 
					              <div class="filter-pills">
 | 
				
			||||||
 | 
					                <button 
 | 
				
			||||||
 | 
					                  v-for="node in availableNodes" 
 | 
				
			||||||
 | 
					                  :key="node"
 | 
				
			||||||
 | 
					                  class="filter-pill"
 | 
				
			||||||
 | 
					                  :class="{ 'active': appliedFilters.nodes[node] }"
 | 
				
			||||||
 | 
					                  @click="handleFilterToggle('nodes', node)"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {{ node }}
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="filter-actions">
 | 
				
			||||||
 | 
					            <button class="btn btn-sm btn-outline-secondary" @click="handleResetFilters">Reset All</button>
 | 
				
			||||||
 | 
					            <button class="btn btn-sm btn-primary" @click="showFilterMenu = false">Close</button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- VM List -->
 | 
				
			||||||
 | 
					    <div v-if="showList" class="vm-list-container">
 | 
				
			||||||
 | 
					      <div v-if="filteredVMs.length === 0" class="no-vms-message">
 | 
				
			||||||
 | 
					        <p>No VMs match your search criteria</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div v-else class="vm-list">
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          v-for="vm in filteredVMs"
 | 
				
			||||||
 | 
					          :key="vm.name"
 | 
				
			||||||
 | 
					          class="vm-list-item"
 | 
				
			||||||
 | 
					          :class="{ 'active': selectedVM === vm.name }"
 | 
				
			||||||
 | 
					          @click="handleVMSelect(vm)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="vm-item-content">
 | 
				
			||||||
 | 
					            <div class="vm-name">{{ vm.name }}</div>
 | 
				
			||||||
 | 
					            <div class="vm-status" :class="getStatusClass(vm.state)">
 | 
				
			||||||
 | 
					              <i class="fas fa-circle status-indicator"></i>
 | 
				
			||||||
 | 
					              <span>{{ vm.state }}</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup>
 | 
					<script setup>
 | 
				
			||||||
import { defineProps, defineEmits } from 'vue';
 | 
					import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps({
 | 
					const props = defineProps({
 | 
				
			||||||
  modelValue: {
 | 
					  modelValue: {
 | 
				
			||||||
@@ -35,17 +122,165 @@ const props = defineProps({
 | 
				
			|||||||
  showClearButton: {
 | 
					  showClearButton: {
 | 
				
			||||||
    type: Boolean,
 | 
					    type: Boolean,
 | 
				
			||||||
    default: true
 | 
					    default: true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  vmList: {
 | 
				
			||||||
 | 
					    type: Array,
 | 
				
			||||||
 | 
					    required: true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  selectedVM: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: ''
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits(['update:modelValue', 'search', 'focus', 'blur', 'clear']);
 | 
					const emit = defineEmits([
 | 
				
			||||||
 | 
					  'update:modelValue',
 | 
				
			||||||
 | 
					  'search',
 | 
				
			||||||
 | 
					  'focus',
 | 
				
			||||||
 | 
					  'blur',
 | 
				
			||||||
 | 
					  'clear',
 | 
				
			||||||
 | 
					  'toggle-list',
 | 
				
			||||||
 | 
					  'select-vm',
 | 
				
			||||||
 | 
					  'toggle-filter',
 | 
				
			||||||
 | 
					  'reset-filters'
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showFilterMenu = ref(false);
 | 
				
			||||||
 | 
					const filterDropdown = ref(null);
 | 
				
			||||||
 | 
					const filterMenu = ref(null);
 | 
				
			||||||
 | 
					const controlsBar = ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add local state for filters
 | 
				
			||||||
 | 
					const appliedFilters = ref({
 | 
				
			||||||
 | 
					  states: {},
 | 
				
			||||||
 | 
					  nodes: {}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add new ref for tracking search active state
 | 
				
			||||||
 | 
					const searchActive = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Calculate available states from vmList
 | 
				
			||||||
 | 
					const availableStates = computed(() => {
 | 
				
			||||||
 | 
					  const states = new Set();
 | 
				
			||||||
 | 
					  props.vmList.forEach(vm => {
 | 
				
			||||||
 | 
					    if (vm.state) states.add(vm.state);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const statesArray = Array.from(states);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Remove 'start' if it exists
 | 
				
			||||||
 | 
					  const startIndex = statesArray.indexOf('start');
 | 
				
			||||||
 | 
					  if (startIndex !== -1) {
 | 
				
			||||||
 | 
					    statesArray.splice(startIndex, 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Sort remaining states
 | 
				
			||||||
 | 
					  statesArray.sort();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Add 'start' back at beginning if it was present
 | 
				
			||||||
 | 
					  if (startIndex !== -1) {
 | 
				
			||||||
 | 
					    statesArray.unshift('start');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return statesArray;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Calculate available nodes from vmList
 | 
				
			||||||
 | 
					const availableNodes = computed(() => {
 | 
				
			||||||
 | 
					  const nodes = new Set();
 | 
				
			||||||
 | 
					  props.vmList.forEach(vm => {
 | 
				
			||||||
 | 
					    if (vm.node) nodes.add(vm.node);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return Array.from(nodes).sort();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initialize filters when states/nodes change
 | 
				
			||||||
 | 
					watch([availableStates, availableNodes], ([states, nodes]) => {
 | 
				
			||||||
 | 
					  // Initialize any new states
 | 
				
			||||||
 | 
					  states.forEach(state => {
 | 
				
			||||||
 | 
					    if (!(state in appliedFilters.value.states)) {
 | 
				
			||||||
 | 
					      appliedFilters.value.states[state] = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Initialize any new nodes
 | 
				
			||||||
 | 
					  nodes.forEach(node => {
 | 
				
			||||||
 | 
					    if (!(node in appliedFilters.value.nodes)) {
 | 
				
			||||||
 | 
					      appliedFilters.value.nodes[node] = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update handleFilterToggle to work with local state
 | 
				
			||||||
 | 
					const handleFilterToggle = (type, value) => {
 | 
				
			||||||
 | 
					  searchActive.value = true;
 | 
				
			||||||
 | 
					  appliedFilters.value[type][value] = !appliedFilters.value[type][value];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update handleResetFilters
 | 
				
			||||||
 | 
					const handleResetFilters = () => {
 | 
				
			||||||
 | 
					  Object.keys(appliedFilters.value.states).forEach(state => {
 | 
				
			||||||
 | 
					    appliedFilters.value.states[state] = false;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  Object.keys(appliedFilters.value.nodes).forEach(node => {
 | 
				
			||||||
 | 
					    appliedFilters.value.nodes[node] = false;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  showFilterMenu.value = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Computed
 | 
				
			||||||
 | 
					const activeFiltersCount = computed(() => {
 | 
				
			||||||
 | 
					  let count = 0;
 | 
				
			||||||
 | 
					  Object.values(appliedFilters.value.states).forEach(isActive => {
 | 
				
			||||||
 | 
					    if (isActive) count++;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  Object.values(appliedFilters.value.nodes).forEach(isActive => {
 | 
				
			||||||
 | 
					    if (isActive) count++;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return count;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filteredVMs = computed(() => {
 | 
				
			||||||
 | 
					  let filtered = [...props.vmList];
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Only apply filters if search is active
 | 
				
			||||||
 | 
					  if (searchActive.value) {
 | 
				
			||||||
 | 
					    // Apply search filter
 | 
				
			||||||
 | 
					    if (props.modelValue) {
 | 
				
			||||||
 | 
					      const query = props.modelValue.toLowerCase();
 | 
				
			||||||
 | 
					      filtered = filtered.filter(vm => 
 | 
				
			||||||
 | 
					        vm.name.toLowerCase().includes(query)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Apply state filters
 | 
				
			||||||
 | 
					    const activeStates = Object.entries(appliedFilters.value.states)
 | 
				
			||||||
 | 
					      .filter(([_, isActive]) => isActive)
 | 
				
			||||||
 | 
					      .map(([state]) => state);
 | 
				
			||||||
 | 
					    if (activeStates.length > 0) {
 | 
				
			||||||
 | 
					      filtered = filtered.filter(vm => activeStates.includes(vm.state));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Apply node filters
 | 
				
			||||||
 | 
					    const activeNodes = Object.entries(appliedFilters.value.nodes)
 | 
				
			||||||
 | 
					      .filter(([_, isActive]) => isActive)
 | 
				
			||||||
 | 
					      .map(([node]) => node);
 | 
				
			||||||
 | 
					    if (activeNodes.length > 0) {
 | 
				
			||||||
 | 
					      filtered = filtered.filter(vm => activeNodes.includes(vm.node));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return filtered;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Methods
 | 
				
			||||||
const handleSearch = (event) => {
 | 
					const handleSearch = (event) => {
 | 
				
			||||||
 | 
					  searchActive.value = true;
 | 
				
			||||||
  emit('update:modelValue', event.target.value);
 | 
					  emit('update:modelValue', event.target.value);
 | 
				
			||||||
  emit('search', event.target.value);
 | 
					  emit('search', event.target.value);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const handleFocus = (event) => {
 | 
					const handleFocus = (event) => {
 | 
				
			||||||
 | 
					  searchActive.value = true;
 | 
				
			||||||
  emit('focus', event);
 | 
					  emit('focus', event);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,9 +292,79 @@ const clearSearch = () => {
 | 
				
			|||||||
  emit('update:modelValue', '');
 | 
					  emit('update:modelValue', '');
 | 
				
			||||||
  emit('clear');
 | 
					  emit('clear');
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toggleList = () => {
 | 
				
			||||||
 | 
					  searchActive.value = false;
 | 
				
			||||||
 | 
					  if (props.showList) {
 | 
				
			||||||
 | 
					    // If we're closing the list, clear the search
 | 
				
			||||||
 | 
					    emit('update:modelValue', '');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  emit('toggle-list');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleVMSelect = (vm) => {
 | 
				
			||||||
 | 
					  emit('select-vm', vm);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toggleFilterMenu = () => {
 | 
				
			||||||
 | 
					  showFilterMenu.value = !showFilterMenu.value;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStatusClass = (state) => {
 | 
				
			||||||
 | 
					  if (!state) return '';
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  switch(state.toLowerCase()) {
 | 
				
			||||||
 | 
					    case 'start':
 | 
				
			||||||
 | 
					      return 'status-running';
 | 
				
			||||||
 | 
					    case 'stop':
 | 
				
			||||||
 | 
					      return 'status-stopped';
 | 
				
			||||||
 | 
					    case 'disable':
 | 
				
			||||||
 | 
					      return 'status-disabled';
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return '';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update click outside handling to close both filter menu and list
 | 
				
			||||||
 | 
					const handleClickOutside = (event) => {
 | 
				
			||||||
 | 
					  // If click is outside the controls bar
 | 
				
			||||||
 | 
					  if (!controlsBar.value?.contains(event.target)) {
 | 
				
			||||||
 | 
					    if (props.showList) {
 | 
				
			||||||
 | 
					      emit('toggle-list'); // Close the list
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    showFilterMenu.value = false; // Close filter menu
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // If click is inside controls bar but outside filter dropdown
 | 
				
			||||||
 | 
					  else if (showFilterMenu.value && !filterDropdown.value?.contains(event.target)) {
 | 
				
			||||||
 | 
					    showFilterMenu.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  document.addEventListener('click', handleClickOutside);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
					  document.removeEventListener('click', handleClickOutside);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.controls-bar {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  border-radius: 0.25rem;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.controls-row {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-box {
 | 
					.search-box {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  flex: 1;
 | 
					  flex: 1;
 | 
				
			||||||
@@ -91,4 +396,195 @@ const clearSearch = () => {
 | 
				
			|||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  font-size: 0.875rem;
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-toggle-btn {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-toggle-btn.active {
 | 
				
			||||||
 | 
					  background-color: #6c757d;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-dropdown {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-badge {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  width: 18px;
 | 
				
			||||||
 | 
					  height: 18px;
 | 
				
			||||||
 | 
					  background-color: #0d6efd;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  font-size: 0.7rem;
 | 
				
			||||||
 | 
					  margin-left: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* VM List styles */
 | 
				
			||||||
 | 
					.vm-list-container {
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  border-radius: 0.25rem;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  margin-top: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-list {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-height: calc(100vh - 200px);
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-list-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 0.75rem 1rem;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  text-align: left;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-list-item:last-child {
 | 
				
			||||||
 | 
					  border-bottom: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-list-item:hover {
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.03);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-list-item.active {
 | 
				
			||||||
 | 
					  background-color: rgba(13, 110, 253, 0.1);
 | 
				
			||||||
 | 
					  border-left: 3px solid #0d6efd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-item-content {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-name {
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.vm-status {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.status-indicator {
 | 
				
			||||||
 | 
					  font-size: 0.625rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Status colors */
 | 
				
			||||||
 | 
					.status-running {
 | 
				
			||||||
 | 
					  color: #28a745;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.status-stopped {
 | 
				
			||||||
 | 
					  color: #6c757d;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.status-paused {
 | 
				
			||||||
 | 
					  color: #ffc107;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.status-error {
 | 
				
			||||||
 | 
					  color: #dc3545;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.status-unknown {
 | 
				
			||||||
 | 
					  color: #6c757d;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.no-vms-message {
 | 
				
			||||||
 | 
					  padding: 2rem;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  color: #6c757d;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Filter menu styles */
 | 
				
			||||||
 | 
					.filter-menu {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 100%;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  min-width: 250px;
 | 
				
			||||||
 | 
					  padding: 1rem;
 | 
				
			||||||
 | 
					  margin-top: 0.5rem;
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  border-radius: 0.25rem;
 | 
				
			||||||
 | 
					  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-section {
 | 
				
			||||||
 | 
					  margin-bottom: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-section h6 {
 | 
				
			||||||
 | 
					  margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #495057;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  padding-bottom: 0.25rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-options-dropdown {
 | 
				
			||||||
 | 
					  max-height: 150px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 0.25rem;
 | 
				
			||||||
 | 
					  padding-left: 0.25rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-pills {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  padding: 0.25rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-pill {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding: 0.25rem 0.75rem;
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					  border: 1px solid #dee2e6;
 | 
				
			||||||
 | 
					  border-radius: 1rem;
 | 
				
			||||||
 | 
					  font-size: 0.875rem;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: all 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-pill:hover {
 | 
				
			||||||
 | 
					  background-color: #e9ecef;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-pill.active {
 | 
				
			||||||
 | 
					  background-color: #0d6efd;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border-color: #0d6efd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-actions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  border-top: 1px solid rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  padding-top: 0.5rem;
 | 
				
			||||||
 | 
					  margin-top: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</style> 
 | 
					</style> 
 | 
				
			||||||
@@ -1,107 +1,21 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="overview-container">
 | 
					  <div class="overview-container">
 | 
				
			||||||
    <!-- VM Controls -->
 | 
					    <VMSearchBar
 | 
				
			||||||
    <div class="vm-controls-container">
 | 
					      v-model="searchQuery"
 | 
				
			||||||
      <div class="controls-row">
 | 
					      :show-list="showVMList"
 | 
				
			||||||
        <button 
 | 
					      :show-clear-button="true"
 | 
				
			||||||
          class="btn btn-outline-secondary list-toggle-btn" 
 | 
					      :vm-list="props.vmData"
 | 
				
			||||||
          @click="toggleVMList"
 | 
					      :selected-vm="selectedVM"
 | 
				
			||||||
          :class="{ 'active': showVMList }"
 | 
					      @search="handleSearch"
 | 
				
			||||||
        >
 | 
					      @focus="handleSearchFocus"
 | 
				
			||||||
          <i class="fas fa-list"></i> List VMs
 | 
					      @blur="handleSearchBlur"
 | 
				
			||||||
        </button>
 | 
					      @clear="clearSearch"
 | 
				
			||||||
        
 | 
					      @toggle-list="toggleVMList"
 | 
				
			||||||
        <VMSearchBar
 | 
					      @select-vm="selectVM"
 | 
				
			||||||
          :model-value="showVMList ? searchQuery : selectedVMData?.name || ''"
 | 
					    />
 | 
				
			||||||
          :show-list="showVMList"
 | 
					 | 
				
			||||||
          :show-clear-button="showVMList"
 | 
					 | 
				
			||||||
          @search="handleSearch"
 | 
					 | 
				
			||||||
          @focus="handleSearchFocus"
 | 
					 | 
				
			||||||
          @blur="handleSearchBlur"
 | 
					 | 
				
			||||||
          @clear="clearSearch"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        <div class="filter-dropdown">
 | 
					 | 
				
			||||||
          <button 
 | 
					 | 
				
			||||||
            class="btn btn-outline-secondary dropdown-toggle" 
 | 
					 | 
				
			||||||
            :disabled="!showVMList"
 | 
					 | 
				
			||||||
            @click="toggleFilterMenu"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <i class="fas fa-filter"></i> Filters
 | 
					 | 
				
			||||||
            <span v-if="activeFiltersCount > 0" class="filter-badge">{{ activeFiltersCount }}</span>
 | 
					 | 
				
			||||||
          </button>
 | 
					 | 
				
			||||||
          <div class="filter-menu" v-show="showFilterMenu">
 | 
					 | 
				
			||||||
            <div class="filter-section">
 | 
					 | 
				
			||||||
              <h6>Status</h6>
 | 
					 | 
				
			||||||
              <div class="filter-options-dropdown">
 | 
					 | 
				
			||||||
                <div class="filter-pills">
 | 
					 | 
				
			||||||
                  <button 
 | 
					 | 
				
			||||||
                    v-for="state in availableStates" 
 | 
					 | 
				
			||||||
                    :key="state"
 | 
					 | 
				
			||||||
                    class="filter-pill"
 | 
					 | 
				
			||||||
                    :class="{ 'active': appliedFilters.states[state] }"
 | 
					 | 
				
			||||||
                    @click="toggleFilter('states', state)"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ state }}
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            <div class="filter-section">
 | 
					 | 
				
			||||||
              <h6>Node</h6>
 | 
					 | 
				
			||||||
              <div class="filter-options-dropdown">
 | 
					 | 
				
			||||||
                <div class="filter-pills">
 | 
					 | 
				
			||||||
                  <button 
 | 
					 | 
				
			||||||
                    v-for="node in availableNodes" 
 | 
					 | 
				
			||||||
                    :key="node"
 | 
					 | 
				
			||||||
                    class="filter-pill"
 | 
					 | 
				
			||||||
                    :class="{ 'active': appliedFilters.nodes[node] }"
 | 
					 | 
				
			||||||
                    @click="toggleFilter('nodes', node)"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    {{ node }}
 | 
					 | 
				
			||||||
                  </button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            <div class="filter-actions">
 | 
					 | 
				
			||||||
              <button class="btn btn-sm btn-outline-secondary" @click="resetFilters">Reset All</button>
 | 
					 | 
				
			||||||
              <button class="btn btn-sm btn-primary" @click="toggleFilterMenu">Close</button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <!-- VM List -->
 | 
					 | 
				
			||||||
    <div v-if="showVMList" class="vm-list-container">
 | 
					 | 
				
			||||||
      <div v-if="filteredVMs.length === 0" class="no-vms-message">
 | 
					 | 
				
			||||||
        <p>No VMs match your search criteria</p>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div v-else class="vm-list">
 | 
					 | 
				
			||||||
        <button
 | 
					 | 
				
			||||||
          v-for="vm in filteredVMs"
 | 
					 | 
				
			||||||
          :key="vm.name"
 | 
					 | 
				
			||||||
          class="vm-list-item"
 | 
					 | 
				
			||||||
          :class="{ 'active': selectedVM === vm.name }"
 | 
					 | 
				
			||||||
          @click="selectVM(vm.name)"
 | 
					 | 
				
			||||||
          :ref="el => { if (vm.name === selectedVM) selectedVMRef = el; }"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <div class="vm-item-content">
 | 
					 | 
				
			||||||
            <div class="vm-name">{{ vm.name }}</div>
 | 
					 | 
				
			||||||
            <div class="vm-status" :class="getStatusClass(vm.state)">
 | 
					 | 
				
			||||||
              <i class="fas fa-circle status-indicator"></i>
 | 
					 | 
				
			||||||
              <span>{{ vm.state }}</span>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </button>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- VM Details -->
 | 
					    <!-- VM Details -->
 | 
				
			||||||
    <div v-if="selectedVMData && !showVMList" class="vm-details">
 | 
					    <div v-if="selectedVMData && !showVMList" class="content-container">
 | 
				
			||||||
      <!-- Basic Info Section -->
 | 
					 | 
				
			||||||
      <CollapsibleSection title="VM Information" :initially-expanded="sections.info">
 | 
					      <CollapsibleSection title="VM Information" :initially-expanded="sections.info">
 | 
				
			||||||
        <div class="info-grid">
 | 
					        <div class="info-grid">
 | 
				
			||||||
          <ValueCard 
 | 
					          <ValueCard 
 | 
				
			||||||
@@ -177,30 +91,28 @@
 | 
				
			|||||||
      </CollapsibleSection>
 | 
					      </CollapsibleSection>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- Loading State -->
 | 
					    <!-- Loading and no-selection states -->
 | 
				
			||||||
    <div v-if="!selectedVMData && !showVMList && isLoading" class="no-vm-selected">
 | 
					    <div v-if="!selectedVMData && !showVMList" class="content-container">
 | 
				
			||||||
      <div class="spinner-container">
 | 
					      <div v-if="isLoading" class="loading-container">
 | 
				
			||||||
        <div class="spinner-border text-primary" role="status">
 | 
					        <div class="spinner-border text-primary" role="status">
 | 
				
			||||||
          <span class="visually-hidden">Loading...</span>
 | 
					          <span class="visually-hidden">Loading...</span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <p class="mt-2 text-muted">Loading VM details...</p>
 | 
					        <p class="mt-2 text-muted">Loading VM details...</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					      <div v-else class="no-content-message">
 | 
				
			||||||
 | 
					        <div class="alert alert-info">
 | 
				
			||||||
    <!-- No VM Selected Message -->
 | 
					          <i class="fas fa-info-circle me-2"></i>
 | 
				
			||||||
    <div v-if="!selectedVMData && !showVMList && !isLoading" class="no-vm-selected">
 | 
					          <span v-if="invalidVMSelected">
 | 
				
			||||||
      <div class="alert alert-info">
 | 
					            Selected VM does not exist
 | 
				
			||||||
        <i class="fas fa-info-circle me-2"></i>
 | 
					          </span>
 | 
				
			||||||
        <span v-if="invalidVMSelected">
 | 
					          <span v-else>
 | 
				
			||||||
          Selected VM does not exist
 | 
					            Please select a VM from the list to view its details
 | 
				
			||||||
        </span>
 | 
					          </span>
 | 
				
			||||||
        <span v-else>
 | 
					        </div>
 | 
				
			||||||
          Please select a VM from the list to view its details
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template> 
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup>
 | 
					<script setup>
 | 
				
			||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
 | 
					import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
 | 
				
			||||||
@@ -234,15 +146,9 @@ const props = defineProps({
 | 
				
			|||||||
const selectedVM = ref('');
 | 
					const selectedVM = ref('');
 | 
				
			||||||
const searchQuery = ref('');
 | 
					const searchQuery = ref('');
 | 
				
			||||||
const showVMList = ref(true);
 | 
					const showVMList = ref(true);
 | 
				
			||||||
const showFilterMenu = ref(false);
 | 
					 | 
				
			||||||
const searchActive = ref(false);
 | 
					const searchActive = ref(false);
 | 
				
			||||||
const appliedFilters = ref({
 | 
					const invalidVMSelected = ref(false);
 | 
				
			||||||
  states: {},
 | 
					const isLoading = ref(false);
 | 
				
			||||||
  nodes: {}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
const selectedVMRef = ref(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Section visibility state - simplified since expansion is handled by CollapsibleSection
 | 
					 | 
				
			||||||
const sections = ref({
 | 
					const sections = ref({
 | 
				
			||||||
  info: true,
 | 
					  info: true,
 | 
				
			||||||
  graphs: true,
 | 
					  graphs: true,
 | 
				
			||||||
@@ -251,232 +157,48 @@ const sections = ref({
 | 
				
			|||||||
  storage: true
 | 
					  storage: true
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Add new state for tracking invalid VM selection
 | 
					 | 
				
			||||||
const invalidVMSelected = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Add loading state
 | 
					 | 
				
			||||||
const isLoading = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Toggle VM list visibility
 | 
					 | 
				
			||||||
const toggleVMList = () => {
 | 
					 | 
				
			||||||
  if (showVMList.value) {
 | 
					 | 
				
			||||||
    showVMList.value = false;
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  showVMList.value = true;
 | 
					 | 
				
			||||||
  searchActive.value = false;
 | 
					 | 
				
			||||||
  invalidVMSelected.value = false;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // Scroll to selected VM after the list is shown
 | 
					 | 
				
			||||||
  if (selectedVM.value) {
 | 
					 | 
				
			||||||
    nextTick(() => {
 | 
					 | 
				
			||||||
      scrollToSelectedVM();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Toggle filter menu
 | 
					 | 
				
			||||||
const toggleFilterMenu = () => {
 | 
					 | 
				
			||||||
  showFilterMenu.value = !showFilterMenu.value;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Toggle a filter on/off
 | 
					 | 
				
			||||||
const toggleFilter = (type, value) => {
 | 
					 | 
				
			||||||
  appliedFilters.value[type][value] = !appliedFilters.value[type][value];
 | 
					 | 
				
			||||||
  searchActive.value = true;
 | 
					 | 
				
			||||||
  filterVMs();
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Reset all filters
 | 
					 | 
				
			||||||
const resetFilters = () => {
 | 
					 | 
				
			||||||
  Object.keys(appliedFilters.value.states).forEach(state => {
 | 
					 | 
				
			||||||
    appliedFilters.value.states[state] = false;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  Object.keys(appliedFilters.value.nodes).forEach(node => {
 | 
					 | 
				
			||||||
    appliedFilters.value.nodes[node] = false;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  searchActive.value = false;
 | 
					 | 
				
			||||||
  filterVMs();
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Count active filters
 | 
					 | 
				
			||||||
const activeFiltersCount = computed(() => {
 | 
					 | 
				
			||||||
  let count = 0;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
const availableStates = computed(() => {
 | 
					 | 
				
			||||||
  const states = new Set();
 | 
					 | 
				
			||||||
  props.vmData.forEach(vm => {
 | 
					 | 
				
			||||||
    if (vm.state) states.add(vm.state);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  // 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
 | 
					 | 
				
			||||||
const availableNodes = computed(() => {
 | 
					 | 
				
			||||||
  const nodes = new Set();
 | 
					 | 
				
			||||||
  props.vmData.forEach(vm => {
 | 
					 | 
				
			||||||
    if (vm.node) nodes.add(vm.node);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  return Array.from(nodes).sort();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Filter VMs based on search query and active filter
 | 
					 | 
				
			||||||
const filteredVMs = computed(() => {
 | 
					 | 
				
			||||||
  console.log('Computing filteredVMs with:', {
 | 
					 | 
				
			||||||
    vmData: props.vmData,
 | 
					 | 
				
			||||||
    searchActive: searchActive.value,
 | 
					 | 
				
			||||||
    searchQuery: searchQuery.value,
 | 
					 | 
				
			||||||
    appliedFilters: appliedFilters.value
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!props.vmData) return [];
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  let filtered = [...props.vmData];
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  if (searchActive.value && searchQuery.value) {
 | 
					 | 
				
			||||||
    const query = searchQuery.value.toLowerCase();
 | 
					 | 
				
			||||||
    filtered = filtered.filter(vm => 
 | 
					 | 
				
			||||||
      vm.name.toLowerCase().includes(query)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  if (searchActive.value) {
 | 
					 | 
				
			||||||
    // Apply state filters if any are active
 | 
					 | 
				
			||||||
    const activeStates = Object.entries(appliedFilters.value.states)
 | 
					 | 
				
			||||||
      .filter(([_, isActive]) => isActive)
 | 
					 | 
				
			||||||
      .map(([state]) => state);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (activeStates.length > 0) {
 | 
					 | 
				
			||||||
      filtered = filtered.filter(vm => activeStates.includes(vm.state));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Apply node filters if any are active
 | 
					 | 
				
			||||||
    const activeNodes = Object.entries(appliedFilters.value.nodes)
 | 
					 | 
				
			||||||
      .filter(([_, isActive]) => isActive)
 | 
					 | 
				
			||||||
      .map(([node]) => node);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (activeNodes.length > 0) {
 | 
					 | 
				
			||||||
      filtered = filtered.filter(vm => activeNodes.includes(vm.node));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  console.log('Filtered VMs result:', filtered);
 | 
					 | 
				
			||||||
  return filtered;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Get the selected VM data
 | 
					// Get the selected VM data
 | 
				
			||||||
const selectedVMData = computed(() => {
 | 
					const selectedVMData = computed(() => {
 | 
				
			||||||
  if (!selectedVM.value || !props.vmData) return null;
 | 
					  if (!selectedVM.value || !props.vmData) return null;
 | 
				
			||||||
  return props.vmData.find(vm => vm.name === selectedVM.value) || null;
 | 
					  return props.vmData.find(vm => vm.name === selectedVM.value) || null;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get status class based on VM state
 | 
					 | 
				
			||||||
const getStatusClass = (state) => {
 | 
					 | 
				
			||||||
  if (!state) return '';
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  switch(state.toLowerCase()) {
 | 
					 | 
				
			||||||
    case 'start':
 | 
					 | 
				
			||||||
      return 'status-running';
 | 
					 | 
				
			||||||
    case 'stop':
 | 
					 | 
				
			||||||
      return 'status-stopped';
 | 
					 | 
				
			||||||
    case 'disable':
 | 
					 | 
				
			||||||
      return 'status-disabled';
 | 
					 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      return '';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Handle search input
 | 
					// Handle search input
 | 
				
			||||||
const handleSearch = (value) => {
 | 
					const handleSearch = (value) => {
 | 
				
			||||||
  searchQuery.value = value;
 | 
					  searchQuery.value = value;
 | 
				
			||||||
  searchActive.value = true;
 | 
					  searchActive.value = true;
 | 
				
			||||||
  filterVMs();
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Handle search focus
 | 
					// Handle search focus
 | 
				
			||||||
const handleSearchFocus = () => {
 | 
					const handleSearchFocus = () => {
 | 
				
			||||||
  // 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;
 | 
					  searchActive.value = true;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Handle search blur
 | 
					// Handle search blur
 | 
				
			||||||
const handleSearchBlur = (event) => {
 | 
					const handleSearchBlur = (event) => {
 | 
				
			||||||
  // Don't close the list if clicking on another element within the list
 | 
					  // Keep blur logic for handling clicks outside
 | 
				
			||||||
  const vmList = document.querySelector('.vm-list-container');
 | 
					  // ... existing blur logic ...
 | 
				
			||||||
  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);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Filter VMs based on current search and filters
 | 
					// Toggle VM list visibility
 | 
				
			||||||
const filterVMs = () => {
 | 
					const toggleVMList = () => {
 | 
				
			||||||
  // This is handled by the computed property
 | 
					  showVMList.value = !showVMList.value;
 | 
				
			||||||
  // But we need this function for event handlers
 | 
					  if (showVMList.value) {
 | 
				
			||||||
 | 
					    searchActive.value = false;
 | 
				
			||||||
 | 
					    invalidVMSelected.value = false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Select a VM
 | 
					// Select a VM
 | 
				
			||||||
const selectVM = (vmName) => {
 | 
					const selectVM = (vm) => {
 | 
				
			||||||
  isLoading.value = true;
 | 
					  isLoading.value = true;
 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // Use nextTick to ensure loading state is shown
 | 
					 | 
				
			||||||
  nextTick(() => {
 | 
					  nextTick(() => {
 | 
				
			||||||
    const vmExists = props.vmData.some(vm => vm.name === vmName);
 | 
					    const vmExists = props.vmData.some(vm => vm.name === vm.name);
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (vmExists) {
 | 
					    if (vmExists) {
 | 
				
			||||||
      selectedVM.value = vmName;
 | 
					      selectedVM.value = vm.name;
 | 
				
			||||||
      invalidVMSelected.value = false;
 | 
					      invalidVMSelected.value = false;
 | 
				
			||||||
      showVMList.value = false;
 | 
					      showVMList.value = false;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -484,66 +206,26 @@ const selectVM = (vmName) => {
 | 
				
			|||||||
      invalidVMSelected.value = true;
 | 
					      invalidVMSelected.value = true;
 | 
				
			||||||
      showVMList.value = false;
 | 
					      showVMList.value = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    router.push({ query: { vm: vm.name } });
 | 
				
			||||||
    router.push({ query: { vm: vmName } });
 | 
					 | 
				
			||||||
    isLoading.value = false;
 | 
					    isLoading.value = false;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Scroll to the selected VM in the list
 | 
					// Clear search
 | 
				
			||||||
const scrollToSelectedVM = () => {
 | 
					const clearSearch = () => {
 | 
				
			||||||
  if (selectedVMRef.value) {
 | 
					  searchQuery.value = '';
 | 
				
			||||||
    // Get the VM list container
 | 
					  searchActive.value = false;
 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Close filter menu when clicking outside
 | 
					 | 
				
			||||||
const closeFilterMenuOnClickOutside = (event) => {
 | 
					 | 
				
			||||||
  const filterDropdown = document.querySelector('.filter-dropdown');
 | 
					 | 
				
			||||||
  if (filterDropdown && !filterDropdown.contains(event.target)) {
 | 
					 | 
				
			||||||
    showFilterMenu.value = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 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-container');
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // 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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Lifecycle hooks
 | 
					// Lifecycle hooks
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  document.addEventListener('click', closeFilterMenuOnClickOutside);
 | 
					 | 
				
			||||||
  document.addEventListener('click', handleClickOutside);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Initialize with URL state if present
 | 
					  // Initialize with URL state if present
 | 
				
			||||||
  const vmFromQuery = route.query.vm;
 | 
					  const vmFromQuery = route.query.vm;
 | 
				
			||||||
  if (vmFromQuery) {
 | 
					  if (vmFromQuery) {
 | 
				
			||||||
    isLoading.value = true;
 | 
					    isLoading.value = true;
 | 
				
			||||||
    // Check if we already have data
 | 
					    // Check if we already have data
 | 
				
			||||||
    if (props.vmData.length) {
 | 
					    if (props.vmData.length) {
 | 
				
			||||||
      selectVM(vmFromQuery);
 | 
					      selectVM({ name: vmFromQuery });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // If no data yet, set the VM but mark as pending verification
 | 
					      // If no data yet, set the VM but mark as pending verification
 | 
				
			||||||
      selectedVM.value = vmFromQuery;
 | 
					      selectedVM.value = vmFromQuery;
 | 
				
			||||||
@@ -555,28 +237,16 @@ onMounted(() => {
 | 
				
			|||||||
    selectedVM.value = '';
 | 
					    selectedVM.value = '';
 | 
				
			||||||
    invalidVMSelected.value = false;
 | 
					    invalidVMSelected.value = false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Initialize filters with available states and nodes
 | 
					 | 
				
			||||||
  if (props.vmData.length) {
 | 
					 | 
				
			||||||
    availableStates.value.forEach(state => {
 | 
					 | 
				
			||||||
      appliedFilters.value.states[state] = false;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    availableNodes.value.forEach(node => {
 | 
					 | 
				
			||||||
      appliedFilters.value.nodes[node] = false;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onUnmounted(() => {
 | 
					onUnmounted(() => {
 | 
				
			||||||
  document.removeEventListener('click', closeFilterMenuOnClickOutside);
 | 
					  // Remove event listeners and clean up resources
 | 
				
			||||||
  document.removeEventListener('click', handleClickOutside);
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Watch for route changes to update selected VM
 | 
					// Watch for route changes to update selected VM
 | 
				
			||||||
watch(() => route.query.vm, (newVm) => {
 | 
					watch(() => route.query.vm, (newVm) => {
 | 
				
			||||||
  if (newVm) {
 | 
					  if (newVm) {
 | 
				
			||||||
    selectVM(newVm);
 | 
					    selectVM({ name: newVm });
 | 
				
			||||||
  } else if (props.vmData.length > 0) {
 | 
					  } else if (props.vmData.length > 0) {
 | 
				
			||||||
    selectedVM.value = '';
 | 
					    selectedVM.value = '';
 | 
				
			||||||
    showVMList.value = true;
 | 
					    showVMList.value = true;
 | 
				
			||||||
@@ -589,7 +259,7 @@ watch(() => showVMList.value, (isVisible) => {
 | 
				
			|||||||
  if (isVisible && selectedVM.value) {
 | 
					  if (isVisible && selectedVM.value) {
 | 
				
			||||||
    // Scroll to selected VM when the list becomes visible
 | 
					    // Scroll to selected VM when the list becomes visible
 | 
				
			||||||
    nextTick(() => {
 | 
					    nextTick(() => {
 | 
				
			||||||
      scrollToSelectedVM();
 | 
					      // Scroll to selected VM logic
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -628,14 +298,7 @@ const formatStorage = (sizeGB) => {
 | 
				
			|||||||
  if (sizeGB < 1024) return sizeGB + ' GB';
 | 
					  if (sizeGB < 1024) return sizeGB + ' GB';
 | 
				
			||||||
  return (sizeGB / 1024).toFixed(1) + ' TB';
 | 
					  return (sizeGB / 1024).toFixed(1) + ' TB';
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
// Clear search
 | 
					 | 
				
			||||||
const clearSearch = () => {
 | 
					 | 
				
			||||||
  searchQuery.value = '';
 | 
					 | 
				
			||||||
  searchActive.value = false;
 | 
					 | 
				
			||||||
  filterVMs();
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script> 
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
.overview-container {
 | 
					.overview-container {
 | 
				
			||||||
@@ -645,153 +308,26 @@ const clearSearch = () => {
 | 
				
			|||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.vm-controls-container {
 | 
					.content-container {
 | 
				
			||||||
  background-color: white;
 | 
					  background-color: white;
 | 
				
			||||||
  border-radius: 0.25rem;
 | 
					  border-radius: 0.25rem;
 | 
				
			||||||
  padding: 1rem;
 | 
					  padding: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.controls-row {
 | 
					.loading-container {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  flex-direction: column;
 | 
				
			||||||
  gap: 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.list-toggle-btn {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  gap: 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.list-toggle-btn.active {
 | 
					 | 
				
			||||||
  background-color: #6c757d;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-dropdown {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-badge {
 | 
					 | 
				
			||||||
  display: inline-flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  width: 18px;
 | 
					 | 
				
			||||||
  height: 18px;
 | 
					 | 
				
			||||||
  background-color: #0d6efd;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  border-radius: 50%;
 | 
					 | 
				
			||||||
  font-size: 0.7rem;
 | 
					 | 
				
			||||||
  margin-left: 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* VM List styles */
 | 
					 | 
				
			||||||
.vm-list-container {
 | 
					 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  border-radius: 0.25rem;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-list {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  max-height: calc(100vh - 200px);
 | 
					 | 
				
			||||||
  overflow-y: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-list-item {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  padding: 0.75rem 1rem;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 | 
					 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  text-align: left;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  transition: background-color 0.2s;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-list-item:last-child {
 | 
					 | 
				
			||||||
  border-bottom: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-list-item:hover {
 | 
					 | 
				
			||||||
  background-color: rgba(0, 0, 0, 0.03);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-list-item.active {
 | 
					 | 
				
			||||||
  background-color: rgba(13, 110, 253, 0.1);
 | 
					 | 
				
			||||||
  border-left: 3px solid #0d6efd;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-item-content {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-name {
 | 
					 | 
				
			||||||
  font-weight: 500;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.vm-status {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  gap: 0.5rem;
 | 
					 | 
				
			||||||
  font-size: 0.875rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-indicator {
 | 
					 | 
				
			||||||
  font-size: 0.625rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Status colors */
 | 
					 | 
				
			||||||
.status-running {
 | 
					 | 
				
			||||||
  color: #28a745;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-stopped {
 | 
					 | 
				
			||||||
  color: #6c757d;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-paused {
 | 
					 | 
				
			||||||
  color: #ffc107;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-error {
 | 
					 | 
				
			||||||
  color: #dc3545;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status-unknown {
 | 
					 | 
				
			||||||
  color: #6c757d;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.no-vms-message {
 | 
					 | 
				
			||||||
  padding: 2rem;
 | 
					  padding: 2rem;
 | 
				
			||||||
  text-align: center;
 | 
					 | 
				
			||||||
  color: #6c757d;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.no-vm-selected {
 | 
					.no-content-message {
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  padding: 2rem;
 | 
					  padding: 2rem;
 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  border-radius: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* VM Details */
 | 
					 | 
				
			||||||
.vm-details {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  gap: 1rem;
 | 
					 | 
				
			||||||
  padding: 1rem;
 | 
					 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  border-radius: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Keep only the styles needed for VM details display */
 | 
				
			||||||
.info-grid {
 | 
					.info-grid {
 | 
				
			||||||
  display: grid;
 | 
					  display: grid;
 | 
				
			||||||
  gap: 1rem;
 | 
					  gap: 1rem;
 | 
				
			||||||
@@ -829,88 +365,4 @@ const clearSearch = () => {
 | 
				
			|||||||
    grid-template-columns: 1fr;
 | 
					    grid-template-columns: 1fr;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Filter menu styles */
 | 
					 | 
				
			||||||
.filter-menu {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 100%;
 | 
					 | 
				
			||||||
  right: 0;
 | 
					 | 
				
			||||||
  z-index: 1000;
 | 
					 | 
				
			||||||
  min-width: 250px;
 | 
					 | 
				
			||||||
  padding: 1rem;
 | 
					 | 
				
			||||||
  margin-top: 0.5rem;
 | 
					 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  border-radius: 0.25rem;
 | 
					 | 
				
			||||||
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-section {
 | 
					 | 
				
			||||||
  margin-bottom: 1rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-section h6 {
 | 
					 | 
				
			||||||
  margin-bottom: 0.5rem;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  color: #495057;
 | 
					 | 
				
			||||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
  padding-bottom: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-options-dropdown {
 | 
					 | 
				
			||||||
  max-height: 150px;
 | 
					 | 
				
			||||||
  overflow-y: auto;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  gap: 0.25rem;
 | 
					 | 
				
			||||||
  padding-left: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-pills {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-wrap: wrap;
 | 
					 | 
				
			||||||
  gap: 0.5rem;
 | 
					 | 
				
			||||||
  padding: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-pill {
 | 
					 | 
				
			||||||
  display: inline-flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  padding: 0.25rem 0.75rem;
 | 
					 | 
				
			||||||
  background-color: #f8f9fa;
 | 
					 | 
				
			||||||
  border: 1px solid #dee2e6;
 | 
					 | 
				
			||||||
  border-radius: 1rem;
 | 
					 | 
				
			||||||
  font-size: 0.875rem;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  transition: all 0.2s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-pill:hover {
 | 
					 | 
				
			||||||
  background-color: #e9ecef;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-pill.active {
 | 
					 | 
				
			||||||
  background-color: #0d6efd;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  border-color: #0d6efd;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.filter-actions {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  border-top: 1px solid rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
  padding-top: 0.5rem;
 | 
					 | 
				
			||||||
  margin-top: 0.5rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.spinner-container {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
  padding: 2rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.spinner-container p {
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style> 
 | 
					</style> 
 | 
				
			||||||
		Reference in New Issue
	
	Block a user