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>