frontend/src/components/PersistentVolumeClaimsList.vue
<!-- PersistentVolumeClaimsList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesPersistentVolumeClaim } from '../types/kubernetes';
import { formatAge } from '../lib/format';
import SearchBar from './SearchBar.vue';
import { PersistentVolumeClaimUpdateOptions } from '../types/custom';
export default defineComponent({
name: 'PersistentVolumeClaimsList',
components: {
SearchBar
},
props: {
selectedCluster: {
type: Object as PropType<KubernetesCluster>,
required: false,
default: null
},
persistentvolumeclaims: {
type: Array as PropType<KubernetesPersistentVolumeClaim[]>,
required: false,
default: () => []
}
},
emits: ['persistentVolumeClaim-selected', 'delete-persistentvolumeclaim', 'create-persistentvolumeclaim'],
setup(props, { emit }) {
// State
const showCreate = ref(false);
const isSubmitting = ref(false);
const searchQuery = ref('');
const selectedPersistentVolumeClaim = ref<KubernetesPersistentVolumeClaim | null>(null);
const selectedContextPersistentVolumeClaim = ref<KubernetesPersistentVolumeClaim | null>(null);
const showMenu = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
const formName = ref('');
const formNamespace = ref('');
const formStorageClass = ref('local-path');
const formAccessModes = ref(['ReadWriteOnce']);
const formCapacity = ref('10');
const formCapacityUnit = ref('Gi');
const formError = ref('');
const resetForm = () => {
formName.value = '';
formNamespace.value = '';
formStorageClass.value = '';
formError.value = '';
formAccessModes.value = ['ReadWriteOnce'];
formCapacity.value = '';
formCapacityUnit.value = '';
isSubmitting.value = false;
};
const cancelForm = (): void => {
resetForm();
closeModal();
}
const createPersistentVolumeClaim = (): void => {
if (!formName.value.trim()) {
formError.value = 'Name is required';
return;
}
if (!formNamespace.value.trim()) {
formError.value = 'Namespace is required';
return;
}
if (!formStorageClass.value.trim()) {
formError.value = 'Storage Class is required';
return;
}
if (!formCapacity.value) {
formError.value = 'Capacity is required';
return;
}
if (formCapacity.value <= 0) {
formError.value = 'Capacity must be 1 or greater';
return;
}
if (formAccessModes.value.length === 0) {
formError.value = 'Access Modes is required';
return;
}
const persistentVolumeClaim: KubernetesPersistentVolumeClaim = {
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
metadata: {
name: formName.value.trim(),
namespace: formNamespace.value.trim(),
},
spec: {
storageClassName: formStorageClass.value.trim(),
accessModes: formAccessModes.value,
resources: {
requests: {
storage: `${formCapacity.value}${formCapacityUnit.value}`,
}
}
}
};
isSubmitting.value = true;
resetForm();
closeModal();
hideContextMenu();
if (props.selectedCluster) {
try {
emit('create-persistentvolumeclaim', {
cluster: props.selectedCluster,
persistentVolumeClaim: persistentVolumeClaim
});
} catch (error) {
alert(error);
}
} else {
alert("Failed to get selected cluster");
}
resetForm();
closeModal();
hideContextMenu();
};
const filteredPersistentVolumeClaims = computed(() => {
if (!searchQuery.value) {
return props.persistentvolumeclaims;
}
const query = searchQuery.value.toLowerCase();
const nameSpecificMatch = query.match(/^name:(.+)$/);
if (nameSpecificMatch) {
const nameQuery = nameSpecificMatch[1].trim();
return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
const name = persistentvolumeclaim.metadata.name.toLowerCase();
return name.includes(nameQuery);
});
}
const namespaceSpecificMatch = query.match(/^namespace:(.+)$/);
if (namespaceSpecificMatch) {
const namespaceQuery = namespaceSpecificMatch[1].trim();
return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
const namespace = persistentvolumeclaim.metadata.namespace.toLowerCase();
return namespace.includes(namespaceQuery);
});
}
const statusSpecificMatch = query.match(/^status:(.+)$/);
if (statusSpecificMatch) {
const statusQuery = statusSpecificMatch[1].trim();
return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
const status = persistentvolumeclaim.status?.phase?.toLowerCase() || '';
return status.includes(statusQuery);
});
}
const labelSpecificMatch = query.match(/^label:(.+)$/);
if (labelSpecificMatch) {
const labelQuery = labelSpecificMatch[1].trim();
return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
const labels = persistentvolumeclaim.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 persistentvolumeclaim names
return props.persistentvolumeclaims.filter(persistentvolumeclaim => {
const name = persistentvolumeclaim.metadata.name.toLowerCase();
return name.includes(query);
});
});
// Methods
const hideContextMenu = () => {
showMenu.value = false;
document.removeEventListener('click', hideContextMenu);
};
const showContextMenu = (event: MouseEvent, persistentvolumeclaim: KubernetesPersistentVolumeClaim) => {
event.preventDefault();
menuPosition.value = {
x: event.clientX,
y: event.clientY
};
selectedContextPersistentVolumeClaim.value = persistentvolumeclaim;
selectPersistentVolumeClaim(persistentvolumeclaim);
showMenu.value = true;
setTimeout(() => {
document.addEventListener('click', hideContextMenu);
}, 0);
};
const closeModal = () => {
showCreate.value = false;
};
const isSelected = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): boolean => {
if (!selectedPersistentVolumeClaim.value) return false;
return selectedPersistentVolumeClaim.value.metadata.name === persistentvolumeclaim.metadata.name &&
selectedPersistentVolumeClaim.value.metadata.uid === persistentvolumeclaim.metadata.uid;
};
const selectPersistentVolumeClaim = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): void => {
try {
const payload = { ...persistentVolumeClaim };
if (!payload.kind) {
payload.kind = 'PersistentVolumeClaim';
}
selectedPersistentVolumeClaim.value = payload;
emit('persistentVolumeClaim-selected', payload);
} catch (error) {
alert("Failed to select Persistent Volume Claim:", error);
}
};
const deletePersistentVolumeClaim = (): void => {
if (selectedContextPersistentVolumeClaim.value && props.selectedCluster) {
try {
emit('delete-persistentvolumeclaim', {
cluster: props.selectedCluster,
persistentVolumeClaim: selectedContextPersistentVolumeClaim.value
});
} catch (error) {
alert(error);
}
}
hideContextMenu();
};
const getUniqueKey = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): string => {
return `${persistentvolumeclaim.metadata.name}-${persistentvolumeclaim.metadata.uid || ''}`;
};
const getPersistentVolumeClaimStorageClass = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
return persistentVolumeClaim.spec?.storageClassName || 'default';
};
const getPersistentVolumeClaimAccessModes = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
const accessModes = persistentVolumeClaim.spec?.accessModes?.join(', ') || 'Unknown';
switch (accessModes) {
case "ReadWriteOnce":
return "RWO";
default:
return accessModes;
}
};
const getPersistentVolumeClaimStatus = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): string => {
return persistentvolumeclaim.status?.phase || 'Unknown';
};
const getPersistentVolumeClaimStatusClass = (persistentvolumeclaim: KubernetesPersistentVolumeClaim): string => {
const status = getPersistentVolumeClaimStatus(persistentvolumeclaim);
switch (status) {
case 'Bound':
return 'status-active';
case 'Pending':
return 'status-pending';
case 'Lost':
return 'status-error';
default:
return 'status-unknown';
}
};
const getPersistentVolumeClaimCapacity = (persistentVolumeClaim: KubernetesPersistentVolumeClaim): string => {
return persistentVolumeClaim.spec?.resources?.requests?.storage || 'Unknown';
};
onBeforeUnmount(() => {
document.removeEventListener('click', hideContextMenu);
});
return {
// State
showCreate,
searchQuery,
showMenu,
menuPosition,
cancelForm,
isSubmitting,
// Form
formError,
formName,
formNamespace,
formStorageClass,
formCapacity,
formCapacityUnit,
formAccessModes,
// Computed
filteredPersistentVolumeClaims,
// Methods
hideContextMenu,
showContextMenu,
isSelected,
selectPersistentVolumeClaim,
deletePersistentVolumeClaim,
createPersistentVolumeClaim,
getUniqueKey,
getPersistentVolumeClaimStorageClass,
getPersistentVolumeClaimAccessModes,
getPersistentVolumeClaimStatus,
getPersistentVolumeClaimStatusClass,
getPersistentVolumeClaimCapacity,
formatAge,
};
}
});
</script>
<template>
<div class="persistentvolumeclaims-container">
<div class="search-bar-container">
<SearchBar
:value="searchQuery"
@update:value="searchQuery = $event"
placeholder="Search persistent volume claims..."
/>
</div>
<div v-if="filteredPersistentVolumeClaims.length === 0" class="no-persistentvolumeclaims">
<p v-if="searchQuery">No persistentvolumeclaims found matching "{{ searchQuery }}"</p>
<p v-else>No persistentvolumeclaims found.</p>
</div>
<div v-else class="table-scroll-container">
<table>
<thead>
<tr>
<th>Namespace</th>
<th>Name</th>
<th>Status</th>
<th>Volume</th>
<th>Capacity</th>
<th>Access Modes</th>
<th>Storage Class</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr
v-for="persistentVolumeClaim in filteredPersistentVolumeClaims"
:key="getUniqueKey(persistentVolumeClaim)"
:class="{ selected: isSelected(persistentVolumeClaim) }"
@click="selectPersistentVolumeClaim(persistentVolumeClaim)"
@contextmenu="showContextMenu($event, persistentVolumeClaim)"
>
<td>{{ persistentVolumeClaim.metadata.namespace }}</td>
<td>{{ persistentVolumeClaim.metadata.name }}</td>
<td :class="getPersistentVolumeClaimStatusClass(persistentVolumeClaim)">{{ getPersistentVolumeClaimStatus(persistentVolumeClaim) }}</td>
<td>{{ persistentVolumeClaim.spec?.volumeName || '-' }}</td>
<td>{{ getPersistentVolumeClaimCapacity(persistentVolumeClaim) }}</td>
<td>{{ getPersistentVolumeClaimAccessModes(persistentVolumeClaim) }}</td>
<td>{{ getPersistentVolumeClaimStorageClass(persistentVolumeClaim) }}</td>
<td>{{ formatAge(persistentVolumeClaim.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="showCreate = true; hideContextMenu()">
Create
</div>
<div class="menu-item" @click="deletePersistentVolumeClaim">
Delete
</div>
</div>
<div v-if="showCreate" class="modal-overlay">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Create Persistent Volume Claim</h3>
<button class="close-button" @click="cancelForm">x</button>
</div>
<div class="modal-body">
<div v-if="formError" class="error-message">{{ formError }}</div>
<div class="form-group">
<label for="name">Name:</label>
<input id="name" v-model="formName" type="text" placeholder="test-claim-1" :disabled="isSubmitting">
</div>
<div class="form-group">
<label for="namespace">Namespace:</label>
<input id="namespace" v-model="formNamespace" type="text" placeholder="default" :disabled="isSubmitting">
</div>
<div class="form-group">
<label for="storageClass">Storage Class:</label>
<select id="storageClass" v-model="formStorageClass" :disabled="isSubmitting">
<option value="local-path">local-path</option>
</select>
</div>
<div class="form-group">
<label for="capacity">Capacity (Gi):</label>
<input id="capacity" v-model="formCapacity" type="number" min="1" placeholder="10" :disabled="isSubmitting"/>
</div>
<div class="form-group">
<label for="accessModes">Access Modes:</label>
<div>
<label class="checkbox-label">
<input type="checkbox" v-model="formAccessModes" value="ReadWriteOnce" :disabled="isSubmitting" />
ReadWriteOnce
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-button" @click="cancelForm" :disabled="isSubmitting">Cancel</button>
<button class="create-button" @click="createPersistentVolumeClaim" :disabled="isSubmitting">{{ isSubmitting ? 'Creating...' : 'Create' }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style src="@/assets/css/CreateResource.css" scoped></style>
<style src="@/assets/css/ListResource.css" scoped></style>
<style src="@/assets/css/CreatePodMethodSelector.css" scoped></style>