summary history files

frontend/src/components/SecretsList.vue
<!-- SecretsList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesSecret } from '../types/kubernetes';
import { formatAge } from '../lib/format'; 
import SearchBar from './SearchBar.vue';
import SecretsCreateMethodSelector from './SecretsCreateMethodSelector.vue';
import SecretsCreateYaml from './SecretsCreateYaml.vue';
import SecretsCreateGuided from './SecretsCreateGuided.vue';
import SecretsEdit from './SecretsEdit.vue';
import { SecretUpdateOptions } from '../types/custom';

export default defineComponent({
  name: 'SecretsList',

  components: {
    SearchBar,
    SecretsCreateMethodSelector,
    SecretsCreateYaml,
    SecretsCreateGuided,
    SecretsEdit
  },

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

  emits: ['secret-selected', 'delete-secret', 'create-secret', 'update-secret'],

  setup(props, { emit }) {
    // State
    const searchQuery = ref('');
    const selectedSecret = ref<KubernetesSecret | null>(null);
    const selectedContextSecret = ref<KubernetesSecret | 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 filteredSecrets = computed(() => {
      if (!searchQuery.value) {
        return props.secrets;
      }

      const query = searchQuery.value.toLowerCase();

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

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

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

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

    const showContextMenu = (event: MouseEvent, secret: KubernetesSecret) => {
      event.preventDefault();

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

      selectedContextSecret.value = secret;
      selectSecret(secret);
      showMenu.value = true;

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

    const isSelected = (secret: KubernetesSecret): boolean => {
      if (!selectedSecret.value) return false;
      return selectedSecret.value.metadata.name === secret.metadata.name &&
        selectedSecret.value.metadata.uid === secret.metadata.uid;
    };

    const selectSecret = (secret: KubernetesSecret): void => {
      try {
        const secretToEmit = { ...secret };
        if (!secretToEmit.kind) {
          secretToEmit.kind = 'Secret';
        }
        selectedSecret.value = secretToEmit;
        emit('secret-selected', secretToEmit);
      } catch (error) {
        console.error("Failed to select secret:", error);
      }
    };

    const deleteSecret = (): void => {
      if (selectedContextSecret.value && props.selectedCluster) {
        try {
          emit('delete-secret', {
            cluster: props.selectedCluster,
            secret: selectedContextSecret.value
          });
        } catch (error) {
          alert(error);
        }
      }
      hideContextMenu();
    };

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

    const getSecretStatus = (secret: KubernetesSecret): string => {
      return secret.status?.phase || 'Unknown';
    };

    const getSecretStatusClass = (secret: KubernetesSecret): string => {
      const status = getSecretStatus(secret);
      switch (status) {
        case 'Active':
          return 'status-active';
        case 'Terminating':
          return 'status-terminating';
        default:
          return 'status-unknown';
      }
    };

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

    const createSecret = (): 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 handleUpdateSecret = (opts: SecretUpdateOptions): void => {
      try {
        emit('update-secret', opts)
      } catch (error: any) {
        alert(error)
      }
      return
    }

    const handleCreateSecret = (opts: SecretCreateOptions): void => {
      try {
        emit('create-secret', opts)
      } catch (err: any) {
        console.error('Failed to create Secret:', 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,
      selectedSecret,
      selectedContextSecret,
      showMenu,
      showEditYaml,
      menuPosition,
      showCreateGuided,
      showCreateYaml,

      // Computed
      filteredSecrets,

      // Methods
      hideContextMenu,
      showContextMenu,
      isSelected,
      selectSecret,
      deleteSecret,
      getUniqueKey,
      getSecretStatus,
      getSecretStatusClass,
      formatAge,
      handleMethodSelected,
      createSecret,
      editSecret,
      handleCreateSecret,
      handleUpdateSecret
    };
  }
});
</script>

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

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

    <div v-else class="table-scroll-container"> 
      <table>
        <thead>
          <tr>
            <th>Namespace</th>
            <th>Name</th>
            <th>Type</th>
            <th>Data</th>
            <th>Age</th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="secret in filteredSecrets" 
            :key="getUniqueKey(secret)"
            :class="{ selected: isSelected(secret) }"
            @click="selectSecret(secret)"
            @contextmenu="showContextMenu($event, secret)"
          >
            <td>{{ secret.metadata.namespace }}</td>
            <td>{{ secret.metadata.name }}</td>
            <td>{{ secret.type }}</td>
            <td>{{ secret.data ? Object.keys(secret.data).length : 0 }}</td>
            <td>{{ formatAge(secret.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="createSecret">
          Create
        </div>
        <div class="menu-item" @click="editSecret">
          Edit
        </div>
        <div class="menu-item" @click="deleteSecret">
          Delete
        </div>
      </div>
    </div>

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

    <SecretsCreateYaml
        :show="showCreateYaml"
        @close="showCreateYaml = false"
        @create-secret="handleCreateSecret"
        />

    <SecretsCreateGuided
        :show="showCreateGuided"
        :cluster="selectedCluster"
        @close="showCreateGuided = false"
        @create-secret="handleCreateSecret"
        />

    <SecretsEdit
        :show="showEditYaml"
        :secret="selectedContextSecret"
        :cluster="selectedCluster"
        @close="showEditYaml = false"
        @update-secret="handleUpdateSecret"
        />

  </div>
</template>

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