frontend/src/components/DeploymentsList.vue
<script lang="ts">
import { defineComponent, PropType, ref, computed } from 'vue';
import { KubernetesCluster, KubernetesDeployment } from '../types/kubernetes';
import SearchBar from './SearchBar.vue';
import { formatAge } from '../lib/format';
export default defineComponent({
name: 'DeploymentsList',
components: {
SearchBar
},
props: {
selectedCluster: {
type: Object as PropType<KubernetesCluster | null>,
required: false,
default: null
},
deployments: {
type: Array as PropType<KubernetesDeployment[]>,
required: false,
default: () => []
}
},
emits: ['deployment-selected', 'restart-deployment'],
setup(props) {
// Search functionality
const searchQuery = ref('');
const filteredDeployments = computed(() => {
if (!searchQuery.value) {
return props.deployments;
}
const query = searchQuery.value.toLowerCase();
// Check if the query is in the format "name:deployment-name"
const nameSpecificMatch = query.match(/^name:(.+)$/);
if (nameSpecificMatch) {
const nameQuery = nameSpecificMatch[1].trim();
return props.deployments.filter(deployment => {
const name = deployment.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.deployments.filter(deployment => {
const namespace = deployment.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.deployments.filter(deployment => {
const labels = deployment.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 deployment names
return props.deployments.filter(deployment => {
const name = deployment.metadata.name.toLowerCase();
return name.includes(query);
});
});
// Context menu state
const showMenu = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
const selectedContextDeployment = ref<KubernetesDeployment | null>(null);
// Show context menu
const showContextMenu = (event: MouseEvent, deployment: KubernetesDeployment) => {
event.preventDefault();
// Position the menu
menuPosition.value = {
x: event.clientX,
y: event.clientY
};
// Store the deployment that was right-clicked
selectedContextDeployment.value = deployment;
// Show the menu
showMenu.value = true;
// Add a click event listener to hide the menu when clicking outside
setTimeout(() => {
document.addEventListener('click', hideContextMenu);
}, 0);
};
// Hide context menu
const hideContextMenu = () => {
showMenu.value = false;
document.removeEventListener('click', hideContextMenu);
};
return {
searchQuery,
filteredDeployments,
showMenu,
menuPosition,
selectedContextDeployment,
showContextMenu,
hideContextMenu,
formatAge
};
},
data() {
return {
selectedDeployment: null as KubernetesDeployment | null
};
},
methods: {
selectDeployment(deployment: KubernetesDeployment): void {
const deploymentToEmit = { ...deployment };
if (!deploymentToEmit.kind) {
deploymentToEmit.kind = 'Deployment';
}
this.selectedDeployment = deploymentToEmit;
this.$emit('deployment-selected', deploymentToEmit);
},
isSelected(deployment: KubernetesDeployment): boolean {
return this.selectedDeployment?.metadata.name === deployment.metadata.name;
},
// Menu actions
restartDeployment() {
if (this.selectedContextDeployment && this.selectedCluster) {
this.$emit('restart-deployment', {
cluster: this.selectedCluster,
deployment: this.selectedContextDeployment
});
}
this.hideContextMenu();
},
getUniqueKey(deployment: KubernetesDeployment): string {
const namespace = deployment.metadata.namespace || 'default';
return `${namespace}-${deployment.metadata.name}-${deployment.metadata.uid}`;
},
}
});
</script>
<template>
<div class="deployments-container">
<div class="search-bar-container">
<SearchBar
:value="searchQuery"
@update:value="searchQuery = $event"
placeholder="Search deployments..."
/>
</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="deployment in filteredDeployments"
:key="getUniqueKey(deployment)"
:class="{ selected: isSelected(deployment) }"
@click="selectDeployment(deployment)"
@contextmenu.prevent="showContextMenu($event, deployment)"
>
<td>{{ deployment.metadata.name }}</td>
<td>{{ deployment.metadata.namespace || 'default' }}</td> <!-- Display namespace -->
<td>{{ `${deployment.status.readyReplicas || 0}/${deployment.spec.replicas}` }}</td>
<td>{{ deployment.status.updatedReplicas || 0 }}</td>
<td>{{ deployment.status.availableReplicas || 0 }}</td>
<td>{{ formatAge(deployment.metadata.creationTimestamp) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Context Menu -->
<div
v-if="showMenu"
class="context-menu"
:style="{ top: menuPosition.y + 'px', left: menuPosition.x + 'px' }"
>
<div class="menu-item" @click="restartDeployment">
<span class="menu-text">Restart</span>
</div>
</div>
</div>
</template>
<style src="@/assets/css/DeploymentsList.css" scoped></style>