summary history files

frontend/src/components/PodsList.vue
<script lang="ts">
import { defineComponent, PropType, ref, computed } from 'vue';
import { KubernetesCluster, KubernetesPod } from '../types/kubernetes';
import { formatAge } from '../lib/format';
import SearchBar from './SearchBar.vue';
import CreatePodGuided from './CreatePodGuided.vue';
import CreatePodMethodSelector from './CreatePodMethodSelector.vue';
import CreatePodYaml from './CreatePodYaml.vue';
import EditPodYaml from './EditPodYaml.vue';

export default defineComponent({
  name: 'PodsList',

  components: {
    SearchBar,
    CreatePodGuided,
    CreatePodMethodSelector,
    CreatePodYaml,
    EditPodYaml
  },

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

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

  setup(props) {
    // Search functionality
    const searchQuery = ref('');

    const filteredPods = computed(() => {
      if (!searchQuery.value) {
        return props.pods;
      }

      const query = searchQuery.value.toLowerCase();

      // Check if the query is in the format "name:pod-name"
      const nameSpecificMatch = query.match(/^name:(.+)$/);
      if (nameSpecificMatch) {
        const nameQuery = nameSpecificMatch[1].trim();
        return props.pods.filter(pod => {
          const name = pod.metadata.name.toLowerCase();
          return name.includes(nameQuery);
        });
      }

      // Check if the query is in the format "namespace:namespace-name"
      const namespaceSpecificMatch = query.match(/^namespace:(.+)$/);
      if (namespaceSpecificMatch) {
        const namespaceQuery = namespaceSpecificMatch[1].trim();
        return props.pods.filter(pod => {
          const namespace = pod.metadata.namespace?.toLowerCase() || '';
          return namespace.includes(namespaceQuery);
        });
      }

      // Check if the query is in the format "label:key=value" or "label:key"
      const labelSpecificMatch = query.match(/^label:(.+)$/);
      if (labelSpecificMatch) {
        const labelQuery = labelSpecificMatch[1].trim();
        return props.pods.filter(pod => {
          const labels = pod.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;
        });
      }

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

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


    return {
      searchQuery,
      filteredPods,
      formatAge
    };
  },

  data() {
    return {
      selectedPod: null as KubernetesPod | null,
      showMenu: false,
      menuPosition: { x: 0, y: 0 },
      selectedContextPod: null as KubernetesPod | null,
      showMethodSelector: false,
      showCreateGuided: false,
      showCreateYaml: false,
      showEditYaml: false,
      currentNamespace: 'default'
    };
  },

  beforeUnmount() {
    document.removeEventListener('click', this.hideContextMenu);
  },

  watch: {
    pods: {
      handler(newPods) {
        console.log('Pods changed in PodsList:', newPods);
      },
      deep: true
    }
  },

  methods: {

    editPod(): void {
      if (this.selectedContextPod) {
        this.showEditYaml = true;
      }
      this.hideContextMenu();
    },

    handlePodUpdated(result: any): void {
      if (this.selectedCluster) {
        this.$emit('refresh-pods', {
          cluster: this.selectedCluster
        });
      }
    },

    handleMethodSelected(method: string): void {
      this.showMethodSelector = false;

      if (method === 'guided') {
        this.showCreateGuided = true;
      } else if (method === 'yaml') {
        this.showCreateYaml = true;
      }
    },

    handleCreatePod({podDefinition, yamlContent, isYaml}: { podDefinition?: any, yamlContent?: string, isYaml?: boolean}) {
      if (!this.selectedCluster) {
        return
      }

      let opts = {
        cluster: this.selectedCluster,
        isYaml: isYaml,
      }

      if (yamlContent) {
        opts.yamlContent = yamlContent
        this.$emit('create-pod', opts)
      } else if (podDefinition) {
        opts.podDefinition = podDefinition
        this.$emit('create-pod', opts)
      } else {
        alert(`Unable to emit create-pod`)
      }

      this.showCreateGuided = false;
      this.showCreateYaml = false;
    },

    selectPod(pod: KubernetesPod): void {
      const podToEmit = { ...pod };
      if (!podToEmit.kind) {
        podToEmit.kind = 'Pod';
      }
      this.selectedPod = podToEmit;
      this.$emit('pod-selected', podToEmit);
    },

    isSelected(pod: KubernetesPod): boolean {
      return this.selectedPod?.metadata.name === pod.metadata.name;
    },

    getPodStatus(pod: KubernetesPod): string {
      if (pod.status && pod.status.phase) {
        return pod.status.phase;
      }
      return 'Unknown';
    },

    getPodStatusClass(pod: KubernetesPod): string {
      const status = this.getPodStatus(pod);
      switch (status) {
        case 'Running':
          return 'status-running';
        case 'Pending':
          return 'status-pending';
        case 'Succeeded':
          return 'status-succeeded';
        case 'Failed':
          return 'status-failed';
        default:
          return 'status-unknown';
      }
    },

    // Context menu methods
    showContextMenu(event: MouseEvent, pod: KubernetesPod): void {
      // Prevent the default context menu
      event.preventDefault();

      // Position the menu
      this.menuPosition = {
        x: event.clientX,
        y: event.clientY
      };

      // Store the pod that was right-clicked
      this.selectedContextPod = pod;

      // Select the pod when right-clicking
      this.selectPod(pod);

      // Show the menu
      this.showMenu = true;

      // Add a click event listener to hide the menu when clicking outside
      // Use setTimeout to avoid the current click event from immediately closing the menu
      setTimeout(() => {
        document.addEventListener('click', this.hideContextMenu);
      }, 0);
    },

    hideContextMenu(): void {
      this.showMenu = false;
      document.removeEventListener('click', this.hideContextMenu);
    },

    deletePod(): void {
      if (this.selectedContextPod && this.selectedCluster) {
        this.$emit('delete-pod', {
          cluster: this.selectedCluster,
          pod: this.selectedContextPod
        });
      }
      this.hideContextMenu();
    },

    createPod(): void {
      this.currentNamespace = this.selectedContextPod?.metadata.namespace || 'default';
      this.showMethodSelector = true;
      this.hideContextMenu();
    },

    getUniqueKey(pod: KubernetesPod): string {
      const namespace = pod.metadata.namespace || 'default';
      return `${namespace}-${pod.metadata.name}-${pod.metadata.uid || ''}`;
    }
  }
});
</script>

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

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

    <div v-else class="table-scroll-container">
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Namespace</th>
            <th>Ready</th>
            <th>Status</th>
            <th>Restarts</th>
            <th>Age</th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="pod in filteredPods" 
            :key="getUniqueKey(pod)"
            :class="{ selected: isSelected(pod) }"
            @click="selectPod(pod)"
            @contextmenu="showContextMenu($event, pod)"
          >
            <td>{{ pod.metadata.name }}</td>
            <td>{{ pod.metadata.namespace }}</td>
            <td>{{ `${pod.status && pod.status.containerStatuses ? pod.status.containerStatuses.filter(cs => cs.ready).length : 0}/${pod.spec && pod.spec.containers ? pod.spec.containers.length : 0}` }}</td>
            <td :class="getPodStatusClass(pod)">{{ getPodStatus(pod) }}</td>
            <td>{{ pod.status && pod.status.containerStatuses ? pod.status.containerStatuses.reduce((sum, cs) => sum + cs.restartCount, 0) : 0 }}</td>
            <td>{{ formatAge(pod.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="createPod">
          Create
        </div>
        <div class="menu-item" @click="editPod">
          Edit
        </div>
        <div class="menu-item" @click="deletePod">
          Delete
        </div>
      </div>
    </div>

    <!-- Method selector dialog -->
    <CreatePodMethodSelector 
      :show="showMethodSelector" 
      :namespace="currentNamespace"
      @close="showMethodSelector = false"
      @method-selected="handleMethodSelected"
    />

    <!-- Guided creation form -->
    <CreatePodGuided 
      :show="showCreateGuided" 
      :namespace="currentNamespace"
      @close="showCreateGuided = false"
      @create-pod="handleCreatePod"
    />

    <!-- YAML creation form -->
    <CreatePodYaml 
      :show="showCreateYaml" 
      :namespace="currentNamespace"
      @close="showCreateYaml = false"
      @create-pod="handleCreatePod"
    />

    <EditPodYaml
      :show="showEditYaml"
      :pod="selectedContextPod"
      :cluster="selectedCluster"
      @close="showEditYaml = false"
      @pod-updated="handlePodUpdated"
    />

  </div>
</template>

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