summary history files

frontend/src/components/PersistentVolumeClaimsList.vue
<!-- PersistentVolumeClaimsList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesPersistentVolumeClaim } from '../types/kubernetes';
import { formatAge } from '../lib/format'; 
import SearchBar from './SearchBar.vue';
import { PersistentVolumeClaimUpdateOptions } from '../types/custom';

export default defineComponent({
  name: 'PersistentVolumeClaimsList',

  components: {
    SearchBar
  },

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

  emits: ['persistentVolumeClaim-selected', 'delete-persistentvolumeclaim', 'create-persistentvolumeclaim'],

  setup(props, { emit }) {
    // State
    const showCreate = ref(false);
    const isSubmitting = ref(false);
    const searchQuery = ref('');
    const selectedPersistentVolumeClaim = ref<KubernetesPersistentVolumeClaim | null>(null);
    const selectedContextPersistentVolumeClaim = ref<KubernetesPersistentVolumeClaim | null>(null);
    const showMenu = ref(false);
    const menuPosition = ref({ x: 0, y: 0 });

    const formName = ref('');
    const formNamespace = ref('');
    const formStorageClass = ref('local-path');
    const formAccessModes = ref(['ReadWriteOnce']);
    const formCapacity = ref('10');
    const formCapacityUnit = ref('Gi');
    const formError = ref('');

    const resetForm = () => {
      formName.value = '';
      formNamespace.value = '';
      formStorageClass.value = '';
      formError.value = '';
      formAccessModes.value = ['ReadWriteOnce'];
      formCapacity.value = '';
      formCapacityUnit.value = '';
      isSubmitting.value = false;
    };

    const cancelForm = (): void => {
      resetForm();
      closeModal();
    }

    const createPersistentVolumeClaim = (): void => {
      if (!formName.value.trim()) {
        formError.value = 'Name is required';
        return;
      }

      if (!formNamespace.value.trim()) {
        formError.value = 'Namespace is required';
        return;
      }

      if (!formStorageClass.value.trim()) {
        formError.value = 'Storage Class is required';
        return;
      }

      if (!formCapacity.value) {
        formError.value = 'Capacity is required';
        return;
      }

      if (formCapacity.value <= 0) {
        formError.value = 'Capacity must be 1 or greater';
        return;
      }

      if (formAccessModes.value.length === 0) {
        formError.value = 'Access Modes is required';
        return;
      }

      const persistentVolumeClaim: KubernetesPersistentVolumeClaim = {
        apiVersion: 'v1',
        kind: 'PersistentVolumeClaim',
        metadata: {
          name: formName.value.trim(),
          namespace: formNamespace.value.trim(),
        },
        spec: {
          storageClassName: formStorageClass.value.trim(),
          accessModes: formAccessModes.value,
          resources: {
            requests: {
              storage: `${formCapacity.value}${formCapacityUnit.value}`,
            }
          }
        }
      };

      isSubmitting.value = true;
      resetForm();
      closeModal();
      hideContextMenu();
      if (props.selectedCluster) {
        try {
          emit('create-persistentvolumeclaim', {
            cluster: props.selectedCluster,
            persistentVolumeClaim: persistentVolumeClaim
          });
        } catch (error) {
          alert(error);
        }
      } else {
        alert("Failed to get selected cluster");
      }
      resetForm();
      closeModal();
      hideContextMenu();
    };

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

      const query = searchQuery.value.toLowerCase();

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

      const namespaceSpecificMatch = query.match(/^namespace:(.+)$/);
      if (namespaceSpecificMatch) {
        const namespaceQuery = namespaceSpecificMatch[1].trim();
        return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
          const namespace = persistentvolumeclaim.metadata.namespace.toLowerCase();
          return namespace.includes(namespaceQuery);
        });
      }

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

      const labelSpecificMatch = query.match(/^label:(.+)$/);
      if (labelSpecificMatch) {
        const labelQuery = labelSpecificMatch[1].trim();
        return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
          const labels = persistentvolumeclaim.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 persistentvolumeclaim names
      return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
        const name = persistentvolumeclaim.metadata.name.toLowerCase();
        return name.includes(query);
      });
    });

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

    const showContextMenu = (event: MouseEvent, persistentvolumeclaim: KubernetesPersistentVolumeClaim) => {
      event.preventDefault();

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

      selectedContextPersistentVolumeClaim.value = persistentvolumeclaim;
      selectPersistentVolumeClaim(persistentvolumeclaim);
      showMenu.value = true;

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

    const closeModal = () => {
      showCreate.value = false;
    };

    const isSelected = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): boolean => {
      if (!selectedPersistentVolumeClaim.value) return false;
      return selectedPersistentVolumeClaim.value.metadata.name === persistentvolumeclaim.metadata.name &&
        selectedPersistentVolumeClaim.value.metadata.uid === persistentvolumeclaim.metadata.uid;
    };

    const selectPersistentVolumeClaim = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): void => {
      try {
        const payload = { ...persistentVolumeClaim };
        if (!payload.kind) {
          payload.kind = 'PersistentVolumeClaim';
        }
        selectedPersistentVolumeClaim.value = payload;
        emit('persistentVolumeClaim-selected', payload);
      } catch (error) {
        alert("Failed to select Persistent Volume Claim:", error);
      }
    };

    const deletePersistentVolumeClaim = (): void => {
      if (selectedContextPersistentVolumeClaim.value && props.selectedCluster) {
        try {
          emit('delete-persistentvolumeclaim', {
            cluster: props.selectedCluster,
            persistentVolumeClaim: selectedContextPersistentVolumeClaim.value
          });
        } catch (error) {
          alert(error);
        }
      }
      hideContextMenu();
    };

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

    const getPersistentVolumeClaimStorageClass = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
      return persistentVolumeClaim.spec?.storageClassName || 'default';
    };

    const getPersistentVolumeClaimAccessModes = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
      const accessModes = persistentVolumeClaim.spec?.accessModes?.join(', ') || 'Unknown';
      switch (accessModes) {
        case "ReadWriteOnce":
          return "RWO";
        default:
          return accessModes;
      }
    };

    const getPersistentVolumeClaimStatus = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): string => {
      return persistentvolumeclaim.status?.phase || 'Unknown';
    };

    const getPersistentVolumeClaimStatusClass = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): string => {
      const status = getPersistentVolumeClaimStatus(persistentvolumeclaim);
      switch (status) {
        case 'Bound':
          return 'status-active';
        case 'Pending':
          return 'status-pending';
        case 'Lost':
          return 'status-error';
        default:
          return 'status-unknown';
      }
    };

    const getPersistentVolumeClaimCapacity = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
      return persistentVolumeClaim.spec?.resources?.requests?.storage || 'Unknown';
    };


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

    return {

      // State
      showCreate,
      searchQuery,
      showMenu,
      menuPosition,
      cancelForm,
      isSubmitting,

      // Form
      formError,
      formName,
      formNamespace,
      formStorageClass,
      formCapacity,
      formCapacityUnit,
      formAccessModes,

      // Computed
      filteredPersistentVolumeClaims,

      // Methods
      hideContextMenu,
      showContextMenu,
      isSelected,
      selectPersistentVolumeClaim,
      deletePersistentVolumeClaim,
      createPersistentVolumeClaim,
      getUniqueKey,
      getPersistentVolumeClaimStorageClass,
      getPersistentVolumeClaimAccessModes,
      getPersistentVolumeClaimStatus,
      getPersistentVolumeClaimStatusClass,
      getPersistentVolumeClaimCapacity,
      formatAge,
    };
  }
});
</script>

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

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

    <div v-else class="table-scroll-container"> 
      <table>
        <thead>
          <tr>
            <th>Namespace</th>
            <th>Name</th>
            <th>Status</th>
            <th>Volume</th>
            <th>Capacity</th>
            <th>Access Modes</th>
            <th>Storage Class</th>
            <th>Age</th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="persistentVolumeClaim in filteredPersistentVolumeClaims" 
            :key="getUniqueKey(persistentVolumeClaim)"
            :class="{ selected: isSelected(persistentVolumeClaim) }"
            @click="selectPersistentVolumeClaim(persistentVolumeClaim)"
            @contextmenu="showContextMenu($event, persistentVolumeClaim)"
          >
            <td>{{ persistentVolumeClaim.metadata.namespace }}</td>
            <td>{{ persistentVolumeClaim.metadata.name }}</td>
            <td :class="getPersistentVolumeClaimStatusClass(persistentVolumeClaim)">{{ getPersistentVolumeClaimStatus(persistentVolumeClaim) }}</td>
            <td>{{ persistentVolumeClaim.spec?.volumeName || '-' }}</td>
            <td>{{ getPersistentVolumeClaimCapacity(persistentVolumeClaim) }}</td>
            <td>{{ getPersistentVolumeClaimAccessModes(persistentVolumeClaim) }}</td>
            <td>{{ getPersistentVolumeClaimStorageClass(persistentVolumeClaim) }}</td>
            <td>{{ formatAge(persistentVolumeClaim.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="showCreate = true; hideContextMenu()">
          Create
        </div>
        <div class="menu-item" @click="deletePersistentVolumeClaim">
          Delete
        </div>
      </div>

      <div v-if="showCreate" class="modal-overlay">
        <div class="modal-content" @click.stop>
          <div class="modal-header">
            <h3>Create Persistent Volume Claim</h3>
            <button class="close-button" @click="cancelForm">x</button>
          </div>

          <div class="modal-body">
            <div v-if="formError" class="error-message">{{ formError }}</div>

            <div class="form-group">
              <label for="name">Name:</label>
              <input id="name" v-model="formName" type="text" placeholder="test-claim-1" :disabled="isSubmitting">
            </div>

            <div class="form-group">
              <label for="namespace">Namespace:</label>
              <input id="namespace" v-model="formNamespace" type="text" placeholder="default" :disabled="isSubmitting">
            </div>

            <div class="form-group">
              <label for="storageClass">Storage Class:</label>
              <select id="storageClass" v-model="formStorageClass" :disabled="isSubmitting">
                <option value="local-path">local-path</option>
              </select>
            </div>

            <div class="form-group">
              <label for="capacity">Capacity (Gi):</label>
              <input id="capacity" v-model="formCapacity" type="number" min="1" placeholder="10" :disabled="isSubmitting"/>
            </div>

            <div class="form-group">
              <label for="accessModes">Access Modes:</label>
              <div>
                <label class="checkbox-label">
                  <input type="checkbox" v-model="formAccessModes" value="ReadWriteOnce" :disabled="isSubmitting" />
                  ReadWriteOnce
                </label>
              </div>
            </div>

          </div>

          <div class="modal-footer">
            <button class="cancel-button" @click="cancelForm" :disabled="isSubmitting">Cancel</button>
            <button class="create-button" @click="createPersistentVolumeClaim" :disabled="isSubmitting">{{ isSubmitting ? 'Creating...' : 'Create' }}</button>
          </div>
        </div>

      </div>

    </div>
  </div>
</template>

<style src="@/assets/css/CreateResource.css" scoped></style>
<style src="@/assets/css/ListResource.css" scoped></style>
<style src="@/assets/css/CreatePodMethodSelector.css" scoped></style>