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>