summary history files

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

export default defineComponent({
    name: 'StatefulSetsList',
    components: {
        SearchBar
    },
    props: {
        selectedCluster: {
            type: Object as PropType<KubernetesCluster>,
            required: false,
            default: null
        },
        statefulSets: {
            type: Array as PropType<KubernetesStatefulSet[]>,
            required: false,
            default: () => []
        }
    },
    emits: ['statefulset-selected', 'restart-statefulset'],
    data() {
        return {
            selectedStatefulSet: null as KubernetesStatefulSet | null
        };
    },
    setup(props, { emit }) {
        const searchQuery = ref('');
        const filteredStatefulSets = computed(() => {
            if (!searchQuery.value) {
                return props.statefulSets;
            }

            const query = searchQuery.value.toLowerCase();

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

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

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

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

            return props.statefulSets.filter(statefulSet => {
                const name = statefulSet.metadata.name.toLowerCase();
                return name.includes(query);
            });
        });

        const showMenu = ref(false);
        const menuPosition = ref({ x: 0, y: 0});
        const selectedContextStatefulSet = ref<KubernetesStatefulSet | null>(null);

        // Function to handle document click for hiding context menu
        const handleDocumentClick = () => {
            showMenu.value = false;
        };

        const showContextMenu = (event: MouseEvent, statefulSet: KubernetesStatefulSet) => {
            event.preventDefault();
            event.stopPropagation();

            // Hide any existing menu first
            showMenu.value = false;

            // Set new menu position and selected item
            menuPosition.value = {
                x: event.clientX,
                y: event.clientY
            };

            selectedContextStatefulSet.value = statefulSet;

            // Show the menu after a small delay to avoid immediate closing
            setTimeout(() => {
                showMenu.value = true;
                document.addEventListener('click', handleDocumentClick);
            }, 10);
        };

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

        // Proper lifecycle cleanup
        onBeforeUnmount(() => {
            document.removeEventListener('click', handleDocumentClick);
        });

        return {
            searchQuery,
            filteredStatefulSets,
            showMenu,
            menuPosition,
            selectedContextStatefulSet,
            showContextMenu,
            hideContextMenu,
            formatAge
        };
    },
    methods: {
      isSelected(statefulSet: KubernetesStatefulSet): boolean {
        return this.selectedStatefulSet?.metadata.name === statefulSet.metadata.name;
      },
      selectStatefulSet(statefulSet: KubernetesStatefulSet): void {
        const statefulSetToEmit = { ...statefulSet };
        if (!statefulSetToEmit.kind) {
          statefulSetToEmit.kind = 'StatefulSet'
        }
        this.selectedStatefulSet = statefulSetToEmit;
        this.$emit('statefulset-selected', statefulSetToEmit);
      },
      getUniqueKey(statefulSet: KubernetesStatefulSet): string {
        let uniqueKey;
        const namespace = statefulSet.metadata.namespace || 'default';
        uniqueKey = namespace + '-' + statefulSet.metadata.name + '-' + (statefulSet.metadata.uid || '');
        return uniqueKey;
      },
      restartStatefulSet() {
        if (this.selectedContextStatefulSet && this.selectedCluster) {
            this.$emit('restart-statefulset', {
                cluster: this.selectedCluster,
                statefulSet: this.selectedContextStatefulSet
            });
        }
        this.hideContextMenu();
      },
      getReadyReplicas(statefulSet: KubernetesStatefulSet): string {
        return (statefulSet.status.readyReplicas || 0) + '/' + statefulSet.spec.replicas;
      }
    },
});
</script>

<template>
  <div class="statefulsets-container">
    <div class="search-bar-container">
      <SearchBar
        :value="searchQuery"
        @update:value="searchQuery = $event"
        placeholder="Search statefulsets..."
      />
    </div>
    <div class="table-scroll-container">
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Namespace</th>
            <th>Ready</th>
            <th>Up-to-date</th>
            <th>Available</th>
            <th>Age</th>
          </tr>
        </thead>
        <tbody>
          <tr 
            v-for="statefulSet in filteredStatefulSets" 
            :key="getUniqueKey(statefulSet)"
            :class="{ selected: isSelected(statefulSet) }"
            @click="selectStatefulSet(statefulSet)"
            @contextmenu.prevent="showContextMenu($event, statefulSet)"
          >
            <td>{{ statefulSet.metadata.name }}</td>
            <td>{{ statefulSet.metadata.namespace || 'default' }}</td>
            <td>{{ getReadyReplicas(statefulSet) }}</td>
            <td>{{ statefulSet.status.updatedReplicas || 0 }}</td>
            <td>{{ statefulSet.status.availableReplicas || 0 }}</td>
            <td>{{ formatAge(statefulSet.metadata.creationTimestamp) }}</td>
          </tr>
        </tbody>
      </table>
    </div>
    <div 
      v-if="showMenu" 
      class="context-menu" 
      :style="{ top: menuPosition.y + 'px', left: menuPosition.x + 'px' }"
    >
      <div class="menu-item" @click="restartStatefulSet">
        <span class="menu-text">Restart</span>
      </div>
    </div>
  </div>
</template>

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