frontend/src/components/PersistentVolumesList.vue
<!-- PersistentVolumeList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesPersistentVolume, KubernetesPersistentVolumeClaim } from '../types/kubernetes';
import { formatAge } from '../lib/format';
import SearchBar from './SearchBar.vue';
export default defineComponent({
name: 'PersistentVolumeList',
components: {
SearchBar
},
props: {
selectedCluster: {
type: Object as PropType<KubernetesCluster>,
required: false,
default: null
},
persistentVolumes: {
type: Array as PropType<KubernetesPersistentVolume[]>,
required: false,
default: () => []
},
persistentVolumeClaims: {
type: Array as PropType<KubernetesPersistentVolumeClaim[]>,
required: false,
default: () => []
}
},
emits: ['persistentVolume-selected', 'delete-persistentvolume'],
setup(props, { emit }) {
// State
const searchQuery = ref('');
const selectedPersistentVolume = ref<KubernetesPersistentVolume | null>(null);
const selectedContextPersistentVolume = ref<KubernetesPersistentVolume | null>(null);
const showMenu = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
// Computed properties
const filteredPersistentVolumes = computed(() => {
if (!searchQuery.value) {
return props.persistentVolumes;
}
const query = searchQuery.value.toLowerCase();
const nameSpecificMatch = query.match(/^name:(.+)$/);
if (nameSpecificMatch) {
const nameQuery = nameSpecificMatch[1].trim();
return props.persistentVolumes.filter(persistentVolume => {
const name = persistentVolume.metadata.name.toLowerCase();
return name.includes(nameQuery);
});
}
const statusSpecificMatch = query.match(/^status:(.+)$/);
if (statusSpecificMatch) {
const statusQuery = statusSpecificMatch[1].trim();
return props.persistentVolumes.filter(persistentVolume => {
const status = persistentVolume.status?.phase?.toLowerCase() || '';
return status.includes(statusQuery);
});
}
const claimSpecificMatch = query.match(/^claim:(.+)$/);
if (claimSpecificMatch) {
const claimQuery = claimSpecificMatch[1].trim();
return props.persistentVolumes.filter(persistentVolume => {
const claimName = persistentVolume.spec?.claimRef?.name?.toLowerCase() || '';
return claimName.includes(claimQuery);
});
}
const storageClassSpecificMatch = query.match(/^storageclass:(.+)$/);
if (storageClassSpecificMatch) {
const storageClassQuery = storageClassSpecificMatch[1].trim();
return props.persistentVolumes.filter(persistentVolume => {
const storageClass = persistentVolume.spec?.storageClassName?.toLowerCase() || '';
return storageClass.includes(storageClassQuery);
});
}
const labelSpecificMatch = query.match(/^label:(.+)$/);
if (labelSpecificMatch) {
const labelQuery = labelSpecificMatch[1].trim();
return props.persistentVolumes.filter(persistentVolume => {
const labels = persistentVolume.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 persistent volume names
return props.persistentVolumes.filter(persistentVolume => {
const name = persistentVolume.metadata.name.toLowerCase();
return name.includes(query);
});
});
// Methods
const hideContextMenu = () => {
showMenu.value = false;
document.removeEventListener('click', hideContextMenu);
};
const showContextMenu = (event: MouseEvent, persistentVolume: KubernetesPersistentVolume) => {
event.preventDefault();
menuPosition.value = {
x: event.clientX,
y: event.clientY
};
selectedContextPersistentVolume.value = persistentVolume;
selectPersistentVolume(persistentVolume);
showMenu.value = true;
setTimeout(() => {
document.addEventListener('click', hideContextMenu);
}, 0);
};
const isSelected = (persistentVolume: KubernetesPersistentVolume): boolean => {
if (!selectedPersistentVolume.value) return false;
return selectedPersistentVolume.value.metadata.name === persistentVolume.metadata.name &&
selectedPersistentVolume.value.metadata.uid === persistentVolume.metadata.uid;
};
const selectPersistentVolume = (persistentVolume: KubernetesPersistentVolume): void => {
try {
const payload = { ...persistentVolume };
if (!payload.kind) {
payload.kind = 'PersistentVolume';
}
selectedPersistentVolume.value = payload;
emit('persistentVolume-selected', payload);
} catch (error) {
alert("Failed to select Persistent Volume:", error);
}
};
const deletePersistentVolume = (): void => {
if (selectedContextPersistentVolume.value && props.selectedCluster) {
try {
emit('delete-persistentvolume', {
cluster: props.selectedCluster,
definition: selectedContextPersistentVolume.value
});
} catch (error) {
alert(error);
}
}
hideContextMenu();
};
const getUniqueKey = (persistentVolume: KubernetesPersistentVolume): string => {
return `${persistentVolume.metadata.name}-${persistentVolume.metadata.uid || ''}`;
};
const getPersistentVolumeStorageClass = (persistentVolume: KubernetesPersistentVolume): string => {
return persistentVolume.spec?.storageClassName || '-';
};
const getPersistentVolumeAccessModes = (persistentVolume: KubernetesPersistentVolume): string => {
const accessModes = persistentVolume.spec?.accessModes?.join(', ') || 'Unknown';
switch (accessModes) {
case "ReadWriteOnce":
return "RWO";
case "ReadOnlyMany":
return "ROX";
case "ReadWriteMany":
return "RWX";
case "ReadWriteOncePod":
return "RWOP";
default:
return accessModes;
}
};
const getPersistentVolumeStatus = (persistentVolume: KubernetesPersistentVolume): string => {
return persistentVolume.status?.phase || 'Unknown';
};
const getPersistentVolumeStatusClass = (persistentVolume: KubernetesPersistentVolume): string => {
const status = getPersistentVolumeStatus(persistentVolume);
switch (status) {
case 'Available':
return 'status-available';
case 'Bound':
return 'status-active';
case 'Released':
return 'status-warning';
case 'Failed':
return 'status-error';
default:
return 'status-unknown';
}
};
const getPersistentVolumeCapacity = (persistentVolume: KubernetesPersistentVolume): string => {
return persistentVolume.spec?.capacity?.storage || 'Unknown';
};
const getPersistentVolumeClaim = (persistentVolume: KubernetesPersistentVolume): string => {
const claimRef = persistentVolume.spec?.claimRef;
if (claimRef) {
return `${claimRef.namespace}/${claimRef.name}`;
}
return '-';
};
const getPersistentVolumeReclaimPolicy = (persistentVolume: KubernetesPersistentVolume): string => {
return persistentVolume.spec?.persistentVolumeReclaimPolicy || 'Retain';
};
onBeforeUnmount(() => {
document.removeEventListener('click', hideContextMenu);
});
// Return all refs, computed properties, and methods
return {
// State
searchQuery,
selectedPersistentVolume,
selectedContextPersistentVolume,
showMenu,
menuPosition,
// Props
persistentVolumeClaims: props.persistentVolumeClaims,
// Computed
filteredPersistentVolumes,
// Methods
hideContextMenu,
showContextMenu,
isSelected,
selectPersistentVolume,
deletePersistentVolume,
getUniqueKey,
getPersistentVolumeStorageClass,
getPersistentVolumeAccessModes,
getPersistentVolumeStatus,
getPersistentVolumeStatusClass,
getPersistentVolumeCapacity,
getPersistentVolumeClaim,
getPersistentVolumeReclaimPolicy,
formatAge
};
}
});
</script>
<template>
<div class="persistentvolumes-container">
<div class="search-bar-container">
<SearchBar
:value="searchQuery"
@update:value="searchQuery = $event"
placeholder="Search persistent volumes..."
/>
</div>
<div v-if="filteredPersistentVolumes.length === 0" class="no-persistentvolumes">
<p v-if="searchQuery">No persistent volumes found matching "{{ searchQuery }}"</p>
<p v-else>No persistent volumes found.</p>
</div>
<div v-else class="table-scroll-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Claim</th>
<th>Capacity</th>
<th>Access Modes</th>
<th>Reclaim Policy</th>
<th>Storage Class</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr
v-for="persistentVolume in filteredPersistentVolumes"
:key="getUniqueKey(persistentVolume)"
:class="{ selected: isSelected(persistentVolume) }"
@click="selectPersistentVolume(persistentVolume)"
@contextmenu="showContextMenu($event, persistentVolume)"
>
<td>{{ persistentVolume.metadata.name }}</td>
<td :class="getPersistentVolumeStatusClass(persistentVolume)">{{ getPersistentVolumeStatus(persistentVolume) }}</td>
<td>{{ getPersistentVolumeClaim(persistentVolume) }}</td>
<td>{{ getPersistentVolumeCapacity(persistentVolume) }}</td>
<td>{{ getPersistentVolumeAccessModes(persistentVolume) }}</td>
<td>{{ getPersistentVolumeReclaimPolicy(persistentVolume) }}</td>
<td>{{ getPersistentVolumeStorageClass(persistentVolume) }}</td>
<td>{{ formatAge(persistentVolume.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="deletePersistentVolume">
Delete
</div>
</div>
</div>
</div>
</template>
<style src="@/assets/css/ListResource.css" scoped></style>