summary history files

frontend/src/components/NamespacesList.vue
<!-- NamespacesList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesNamespace } from '../types/kubernetes';
import { formatAge } from '../lib/format'; 
import SearchBar from './SearchBar.vue';
import NamespacesCreateMethodSelector from './NamespacesCreateMethodSelector.vue';
import NamespacesCreateGuided from './NamespacesCreateGuided.vue';
import NamespacesCreateYaml from './NamespacesCreateYaml.vue';
import NamespacesEdit from './NamespacesEdit.vue';

export default defineComponent({
  name: 'NamespacesList',

  components: {
    SearchBar,
    NamespacesCreateMethodSelector,
    NamespacesCreateGuided,
    NamespacesCreateYaml,
    NamespacesEdit
  },

  props: {
    selectedCluster: {
      type: Object as PropType<KubernetesCluster>,
      required: false,
      default: null
    },
    namespaces: {
      type: Array as PropType<KubernetesNamespace[]>,
      required: false,
      default: () => []
    }
  },

  emits: ['namespace-selected', 'delete-namespace', 'create-namespace'],

  setup(props, { emit }) {
    // State
    const searchQuery = ref('');
    const selectedNamespace = ref<KubernetesNamespace | null>(null);
    const selectedContextNamespace = ref<KubernetesNamespace | null>(null);
    const showMenu = ref(false);
    const menuPosition = ref({ x: 0, y: 0 });

    const showMethodSelector = ref(false);
    const showCreateGuided = ref(false);
    const showCreateYaml = ref(false);
    const showEditYaml = ref(false);

    // Computed properties
    const filteredNamespaces = computed(() => {
      if (!searchQuery.value) {
        return props.namespaces;
      }

      const query = searchQuery.value.toLowerCase();

      const nameSpecificMatch = query.match(/^name:(.+)$/);
      if (nameSpecificMatch) {
        const nameQuery = nameSpecificMatch[1].trim();
        return props.namespaces.filter(namespace => {
          const name = namespace.metadata.name.toLowerCase();
          return name.includes(nameQuery);
        });
      }

      const statusSpecificMatch = query.match(/^status:(.+)$/);
      if (statusSpecificMatch) {
        const statusQuery = statusSpecificMatch[1].trim();
        return props.namespaces.filter(namespace => {
          const status = namespace.status?.phase?.toLowerCase() || '';
          return status.includes(statusQuery);
        });
      }

      const labelSpecificMatch = query.match(/^label:(.+)$/);
      if (labelSpecificMatch) {
        const labelQuery = labelSpecificMatch[1].trim();
        return props.namespaces.filter(namespace => {
          const labels = namespace.metadata.labels || {};
          for (const key in labels) {
            const value = labels[key].toLowerCase();
            const keyLower = key.toLowerCase();

            // Check for key=value format
            if (labelQuery.includes('=')) {
              const [queryKey, queryValue] = labelQuery.split('=');
              if (keyLower === queryKey.trim().toLowerCase() && 
                  value.includes(queryValue.trim().toLowerCase())) {
                return true;
              }
            } 
            // Check for just key
            else if (keyLower.includes(labelQuery)) {
              return true;
            }
          }
          return false;
        });
      }

      // Default behavior: search only in namespace names
      return props.namespaces.filter(namespace => {
        const name = namespace.metadata.name.toLowerCase();
        return name.includes(query);
      });
    });

    // Methods
    const hideContextMenu = () => {
      showMenu.value = false;
      document.removeEventListener('click', hideContextMenu);
    };

    const showContextMenu = (event: MouseEvent, namespace: KubernetesNamespace) => {
      event.preventDefault();

      menuPosition.value = {
        x: event.clientX,
        y: event.clientY
      };

      selectedContextNamespace.value = namespace;
      selectNamespace(namespace);
      showMenu.value = true;

      setTimeout(() => {
        document.addEventListener('click', hideContextMenu);
      }, 0);
    };

    const isSelected = (namespace: KubernetesNamespace): boolean => {
      if (!selectedNamespace.value) return false;
      return selectedNamespace.value.metadata.name === namespace.metadata.name &&
        selectedNamespace.value.metadata.uid === namespace.metadata.uid;
    };

    const selectNamespace = (namespace: KubernetesNamespace): void => {
      const namespaceToEmit = { ...namespace };
      if (!namespaceToEmit.kind) {
        namespaceToEmit.kind = 'Namespace';
      }
      selectedNamespace.value = namespaceToEmit;
      emit('namespace-selected', namespaceToEmit);
    };

    const deleteNamespace = (): void => {
      if (selectedContextNamespace.value && props.selectedCluster) {
        emit('delete-namespace', {
          cluster: props.selectedCluster,
          namespace: selectedNamespace.value
        });
      }
      hideContextMenu();
    };

    const getUniqueKey = (namespace: KubernetesNamespace): string => {
      return `${namespace.metadata.name}-${namespace.metadata.uid || ''}`;
    };

    const getNamespaceStatus = (namespace: KubernetesNamespace): string => {
      return namespace.status?.phase || 'Unknown';
    };

    const getNamespaceStatusClass = (namespace: KubernetesNamespace): string => {
      const status = getNamespaceStatus(namespace);
      switch (status) {
        case 'Active':
          return 'status-active';
        case 'Terminating':
          return 'status-terminating';
        default:
          return 'status-unknown';
      }
    };

    const editNamespace = (): void => {
      showEditYaml.value = true;
      hideContextMenu();
    };

    const createNamespace = (): void => {
      showMethodSelector.value = true;
      hideContextMenu();
    };

    const handleMethodSelected = (method: string): void => {
      showMethodSelector.value = false;
      if (method === 'guided') {
        showCreateGuided.value = true;
      } else if (method === 'yaml') {
        showCreateYaml.value = true;
      }
    };

    const handleUpdateNamespace = (): void => {
      if (!props.selectedCluster) {
        return
      }
      emit('load-resources')
    }

    const handleCreateNamespace = ({definition, yamlContent, isYaml}: { definition?: any, yamlContent?: string, isYaml?: boolean}): void => {
      if (!props.selectedCluster) {
        return
      }

      try {
        let opts = {
          cluster: props.selectedCluster,
          isYaml: isYaml,
        }
        if (yamlContent) {
          opts.yamlContent = yamlContent
          emit('create-namespace', opts)
        } else if (definition) {
          opts.definition = definition
          emit('create-namespace', opts)
        } else {
          throw new Error("Unable to emit create-namespace")
        }
      } catch (err: any) {
        console.error('Failed to create namespace:', err)
      } finally {
        showCreateGuided.value = false
        showCreateYaml.value = false
      }
    }

    // Lifecycle hooks
    onBeforeUnmount(() => {
      document.removeEventListener('click', hideContextMenu);
    });

    // Return all refs, computed properties, and methods
    return {
      // State
      showMethodSelector,
      searchQuery,
      selectedNamespace,
      selectedContextNamespace,
      showMenu,
      showEditYaml,
      menuPosition,
      showCreateGuided,
      showCreateYaml,

      // Computed
      filteredNamespaces,

      // Methods
      hideContextMenu,
      showContextMenu,
      isSelected,
      selectNamespace,
      deleteNamespace,
      getUniqueKey,
      getNamespaceStatus,
      getNamespaceStatusClass,
      formatAge,
      handleMethodSelected,
      createNamespace,
      editNamespace,
      handleCreateNamespace,
      handleUpdateNamespace
    };
  }
});
</script>

<template>
  <div class="namespaces-container">
    <div class="search-bar-container">
      <SearchBar
        :value="searchQuery"
        @update:value="searchQuery = $event"
        placeholder="Search namespaces..."
      />
    </div>

    <div v-if="filteredNamespaces.length === 0" class="no-namespaces">
      <p v-if="searchQuery">No namespaces found matching "{{ searchQuery }}"</p>
      <p v-else>No namespaces found.</p>
    </div>

    <div v-else class="table-scroll-container"> 
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Status</th>
            <th>Age</th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="namespace in filteredNamespaces" 
            :key="getUniqueKey(namespace)"
            :class="{ selected: isSelected(namespace) }"
            @click="selectNamespace(namespace)"
            @contextmenu="showContextMenu($event, namespace)"
          >
            <td>{{ namespace.metadata.name }}</td>
            <td :class="getNamespaceStatusClass(namespace)">{{ getNamespaceStatus(namespace) }}</td>
            <td>{{ formatAge(namespace.metadata.creationTimestamp) }}</td>
          </tr>
        </tbody>
      </table>
      <div 
        v-if="showMenu" 
        class="context-menu" 
        :style="{ top: menuPosition.y + 'px', left: menuPosition.x + 'px' }"
        @click.stop
      >
        <div class="menu-item" @click="createNamespace">
          Create
        </div>
        <div class="menu-item" @click="editNamespace">
          Edit
        </div>
        <div class="menu-item" @click="deleteNamespace">
          Delete
        </div>
      </div>
    </div>

    <NamespacesCreateMethodSelector
        :show="showMethodSelector"
        @close="showMethodSelector = false"
        @method-selected="handleMethodSelected"
        />

    <NamespacesCreateGuided
        :show="showCreateGuided"
        @close="showCreateGuided = false"
        @create-namespace="handleCreateNamespace"
        />

    <NamespacesCreateYaml
        :show="showCreateYaml"
        @close="showCreateYaml = false"
        @create-namespace="handleCreateNamespace"
        />

    <NamespacesEdit
        :show="showEditYaml"
        :namespace="selectedContextNamespace"
        :cluster="selectedCluster"
        @close="showEditYaml = false"
        @namespace-updated="handleUpdateNamespace"
        />

  </div>
</template>

<style src="@/assets/css/NamespacesList.css" scoped></style>