Move remaining search components
This commit is contained in:
		@@ -1,10 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="controls-bar" ref="controlsBar">
 | 
			
		||||
    <div class="controls-row">
 | 
			
		||||
      <button 
 | 
			
		||||
        class="btn btn-outline-secondary list-toggle-btn" 
 | 
			
		||||
        @click="toggleList"
 | 
			
		||||
        :class="{ 'active': showList }"
 | 
			
		||||
      >
 | 
			
		||||
        <i class="fas fa-list"></i> List VMs
 | 
			
		||||
      </button>
 | 
			
		||||
      
 | 
			
		||||
      <div class="search-box">
 | 
			
		||||
        <i class="fas fa-search search-icon"></i>
 | 
			
		||||
        <input 
 | 
			
		||||
          type="text" 
 | 
			
		||||
          placeholder="Search VMs..." 
 | 
			
		||||
      :value="modelValue"
 | 
			
		||||
          :value="!showList ? selectedVM : modelValue"
 | 
			
		||||
          @input="handleSearch"
 | 
			
		||||
          @focus="handleFocus"
 | 
			
		||||
          @blur="handleBlur"
 | 
			
		||||
@@ -18,10 +28,87 @@
 | 
			
		||||
          <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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { defineProps, defineEmits } from 'vue';
 | 
			
		||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  modelValue: {
 | 
			
		||||
@@ -35,17 +122,165 @@ const props = defineProps({
 | 
			
		||||
  showClearButton: {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    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) => {
 | 
			
		||||
  searchActive.value = true;
 | 
			
		||||
  emit('update:modelValue', event.target.value);
 | 
			
		||||
  emit('search', event.target.value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleFocus = (event) => {
 | 
			
		||||
  searchActive.value = true;
 | 
			
		||||
  emit('focus', event);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -57,9 +292,79 @@ const clearSearch = () => {
 | 
			
		||||
  emit('update:modelValue', '');
 | 
			
		||||
  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>
 | 
			
		||||
 | 
			
		||||
<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 {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
@@ -91,4 +396,195 @@ const clearSearch = () => {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  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> 
 | 
			
		||||
@@ -1,107 +1,21 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="overview-container">
 | 
			
		||||
    <!-- VM Controls -->
 | 
			
		||||
    <div class="vm-controls-container">
 | 
			
		||||
      <div class="controls-row">
 | 
			
		||||
        <button 
 | 
			
		||||
          class="btn btn-outline-secondary list-toggle-btn" 
 | 
			
		||||
          @click="toggleVMList"
 | 
			
		||||
          :class="{ 'active': showVMList }"
 | 
			
		||||
        >
 | 
			
		||||
          <i class="fas fa-list"></i> List VMs
 | 
			
		||||
        </button>
 | 
			
		||||
        
 | 
			
		||||
    <VMSearchBar
 | 
			
		||||
          :model-value="showVMList ? searchQuery : selectedVMData?.name || ''"
 | 
			
		||||
      v-model="searchQuery"
 | 
			
		||||
      :show-list="showVMList"
 | 
			
		||||
          :show-clear-button="showVMList"
 | 
			
		||||
      :show-clear-button="true"
 | 
			
		||||
      :vm-list="props.vmData"
 | 
			
		||||
      :selected-vm="selectedVM"
 | 
			
		||||
      @search="handleSearch"
 | 
			
		||||
      @focus="handleSearchFocus"
 | 
			
		||||
      @blur="handleSearchBlur"
 | 
			
		||||
      @clear="clearSearch"
 | 
			
		||||
      @toggle-list="toggleVMList"
 | 
			
		||||
      @select-vm="selectVM"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
        <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 -->
 | 
			
		||||
    <div v-if="selectedVMData && !showVMList" class="vm-details">
 | 
			
		||||
      <!-- Basic Info Section -->
 | 
			
		||||
    <div v-if="selectedVMData && !showVMList" class="content-container">
 | 
			
		||||
      <CollapsibleSection title="VM Information" :initially-expanded="sections.info">
 | 
			
		||||
        <div class="info-grid">
 | 
			
		||||
          <ValueCard 
 | 
			
		||||
@@ -177,18 +91,15 @@
 | 
			
		||||
      </CollapsibleSection>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Loading State -->
 | 
			
		||||
    <div v-if="!selectedVMData && !showVMList && isLoading" class="no-vm-selected">
 | 
			
		||||
      <div class="spinner-container">
 | 
			
		||||
    <!-- Loading and no-selection states -->
 | 
			
		||||
    <div v-if="!selectedVMData && !showVMList" class="content-container">
 | 
			
		||||
      <div v-if="isLoading" class="loading-container">
 | 
			
		||||
        <div class="spinner-border text-primary" role="status">
 | 
			
		||||
          <span class="visually-hidden">Loading...</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p class="mt-2 text-muted">Loading VM details...</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- No VM Selected Message -->
 | 
			
		||||
    <div v-if="!selectedVMData && !showVMList && !isLoading" class="no-vm-selected">
 | 
			
		||||
      <div v-else class="no-content-message">
 | 
			
		||||
        <div class="alert alert-info">
 | 
			
		||||
          <i class="fas fa-info-circle me-2"></i>
 | 
			
		||||
          <span v-if="invalidVMSelected">
 | 
			
		||||
@@ -200,6 +111,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
@@ -234,15 +146,9 @@ const props = defineProps({
 | 
			
		||||
const selectedVM = ref('');
 | 
			
		||||
const searchQuery = ref('');
 | 
			
		||||
const showVMList = ref(true);
 | 
			
		||||
const showFilterMenu = ref(false);
 | 
			
		||||
const searchActive = ref(false);
 | 
			
		||||
const appliedFilters = ref({
 | 
			
		||||
  states: {},
 | 
			
		||||
  nodes: {}
 | 
			
		||||
});
 | 
			
		||||
const selectedVMRef = ref(null);
 | 
			
		||||
 | 
			
		||||
// Section visibility state - simplified since expansion is handled by CollapsibleSection
 | 
			
		||||
const invalidVMSelected = ref(false);
 | 
			
		||||
const isLoading = ref(false);
 | 
			
		||||
const sections = ref({
 | 
			
		||||
  info: true,
 | 
			
		||||
  graphs: true,
 | 
			
		||||
@@ -251,232 +157,48 @@ const sections = ref({
 | 
			
		||||
  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
 | 
			
		||||
const selectedVMData = computed(() => {
 | 
			
		||||
  if (!selectedVM.value || !props.vmData) return 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
 | 
			
		||||
const handleSearch = (value) => {
 | 
			
		||||
  searchQuery.value = value;
 | 
			
		||||
  searchActive.value = true;
 | 
			
		||||
  filterVMs();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Handle search focus
 | 
			
		||||
const handleSearchFocus = () => {
 | 
			
		||||
  // Show VM list when search is focused
 | 
			
		||||
  if (!showVMList.value) {
 | 
			
		||||
    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-container');
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
  // Keep blur logic for handling clicks outside
 | 
			
		||||
  // ... existing blur logic ...
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Filter VMs based on current search and filters
 | 
			
		||||
const filterVMs = () => {
 | 
			
		||||
  // This is handled by the computed property
 | 
			
		||||
  // But we need this function for event handlers
 | 
			
		||||
// Toggle VM list visibility
 | 
			
		||||
const toggleVMList = () => {
 | 
			
		||||
  showVMList.value = !showVMList.value;
 | 
			
		||||
  if (showVMList.value) {
 | 
			
		||||
    searchActive.value = false;
 | 
			
		||||
    invalidVMSelected.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Select a VM
 | 
			
		||||
const selectVM = (vmName) => {
 | 
			
		||||
const selectVM = (vm) => {
 | 
			
		||||
  isLoading.value = true;
 | 
			
		||||
  
 | 
			
		||||
  // Use nextTick to ensure loading state is shown
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    const vmExists = props.vmData.some(vm => vm.name === vmName);
 | 
			
		||||
    
 | 
			
		||||
    const vmExists = props.vmData.some(vm => vm.name === vm.name);
 | 
			
		||||
    if (vmExists) {
 | 
			
		||||
      selectedVM.value = vmName;
 | 
			
		||||
      selectedVM.value = vm.name;
 | 
			
		||||
      invalidVMSelected.value = false;
 | 
			
		||||
      showVMList.value = false;
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -484,66 +206,26 @@ const selectVM = (vmName) => {
 | 
			
		||||
      invalidVMSelected.value = true;
 | 
			
		||||
      showVMList.value = false;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    router.push({ query: { vm: vmName } });
 | 
			
		||||
    router.push({ query: { vm: vm.name } });
 | 
			
		||||
    isLoading.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);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
// Clear search
 | 
			
		||||
const clearSearch = () => {
 | 
			
		||||
  searchQuery.value = '';
 | 
			
		||||
  searchActive.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Lifecycle hooks
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  document.addEventListener('click', closeFilterMenuOnClickOutside);
 | 
			
		||||
  document.addEventListener('click', handleClickOutside);
 | 
			
		||||
 | 
			
		||||
  // Initialize with URL state if present
 | 
			
		||||
  const vmFromQuery = route.query.vm;
 | 
			
		||||
  if (vmFromQuery) {
 | 
			
		||||
    isLoading.value = true;
 | 
			
		||||
    // Check if we already have data
 | 
			
		||||
    if (props.vmData.length) {
 | 
			
		||||
      selectVM(vmFromQuery);
 | 
			
		||||
      selectVM({ name: vmFromQuery });
 | 
			
		||||
    } else {
 | 
			
		||||
      // If no data yet, set the VM but mark as pending verification
 | 
			
		||||
      selectedVM.value = vmFromQuery;
 | 
			
		||||
@@ -555,28 +237,16 @@ onMounted(() => {
 | 
			
		||||
    selectedVM.value = '';
 | 
			
		||||
    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(() => {
 | 
			
		||||
  document.removeEventListener('click', closeFilterMenuOnClickOutside);
 | 
			
		||||
  document.removeEventListener('click', handleClickOutside);
 | 
			
		||||
  // Remove event listeners and clean up resources
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Watch for route changes to update selected VM
 | 
			
		||||
watch(() => route.query.vm, (newVm) => {
 | 
			
		||||
  if (newVm) {
 | 
			
		||||
    selectVM(newVm);
 | 
			
		||||
    selectVM({ name: newVm });
 | 
			
		||||
  } else if (props.vmData.length > 0) {
 | 
			
		||||
    selectedVM.value = '';
 | 
			
		||||
    showVMList.value = true;
 | 
			
		||||
@@ -589,7 +259,7 @@ watch(() => showVMList.value, (isVisible) => {
 | 
			
		||||
  if (isVisible && selectedVM.value) {
 | 
			
		||||
    // Scroll to selected VM when the list becomes visible
 | 
			
		||||
    nextTick(() => {
 | 
			
		||||
      scrollToSelectedVM();
 | 
			
		||||
      // Scroll to selected VM logic
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@@ -628,13 +298,6 @@ const formatStorage = (sizeGB) => {
 | 
			
		||||
  if (sizeGB < 1024) return sizeGB + ' GB';
 | 
			
		||||
  return (sizeGB / 1024).toFixed(1) + ' TB';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Clear search
 | 
			
		||||
const clearSearch = () => {
 | 
			
		||||
  searchQuery.value = '';
 | 
			
		||||
  searchActive.value = false;
 | 
			
		||||
  filterVMs();
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
@@ -645,153 +308,26 @@ const clearSearch = () => {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vm-controls-container {
 | 
			
		||||
.content-container {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  border-radius: 0.25rem;
 | 
			
		||||
  padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.controls-row {
 | 
			
		||||
.loading-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  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;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-vm-selected {
 | 
			
		||||
.no-content-message {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  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 {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
@@ -829,88 +365,4 @@ const clearSearch = () => {
 | 
			
		||||
    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> 
 | 
			
		||||
		Reference in New Issue
	
	Block a user