frontend/src/components/PersistentVolumeClaimsCreateGuided.vue
<!-- PersistentVolumeClaimsCreateGuided.vue -->
<template>
<div v-if="show" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>Create Persistent Volume Claim</h3>
<button class="close-button" @click="closeModal">×</button>
</div>
<div class="modal-body">
<div v-if="error" class="error-message">{{ error }}</div>
<div class="form-group">
<label for="name">Name:</label>
<input
id="name"
v-model="name"
type="text"
placeholder="my-pvc"
:disabled="isSubmitting"
/>
</div>
<div class="form-group">
<label for="namespace">Namespace:</label>
<input
id="namespace"
v-model="namespace"
type="text"
placeholder="default"
:disabled="isSubmitting"
/>
</div>
<div class="form-group">
<label for="storageClass">Storage Class:</label>
<input
id="storageClass"
v-model="storageClass"
type="text"
placeholder="standard"
:disabled="isSubmitting"
/>
<small class="field-hint">Leave empty to use default storage class</small>
</div>
<div class="form-group">
<label for="storageSize">Storage Size:</label>
<div class="storage-input-group">
<input
id="storageSize"
v-model="storageSize"
type="number"
min="1"
placeholder="1"
:disabled="isSubmitting"
/>
<select v-model="storageUnit" :disabled="isSubmitting">
<option value="Gi">Gi</option>
<option value="Mi">Mi</option>
<option value="Ti">Ti</option>
</select>
</div>
</div>
<div class="form-section">
<h4>Access Modes</h4>
<div class="checkbox-group">
<label class="checkbox-label">
<input
type="checkbox"
v-model="accessModes"
value="ReadWriteOnce"
:disabled="isSubmitting"
/>
ReadWriteOnce (RWO)
<small>Volume can be mounted as read-write by a single node</small>
</label>
<label class="checkbox-label">
<input
type="checkbox"
v-model="accessModes"
value="ReadOnlyMany"
:disabled="isSubmitting"
/>
ReadOnlyMany (ROX)
<small>Volume can be mounted read-only by many nodes</small>
</label>
<label class="checkbox-label">
<input
type="checkbox"
v-model="accessModes"
value="ReadWriteMany"
:disabled="isSubmitting"
/>
ReadWriteMany (RWX)
<small>Volume can be mounted as read-write by many nodes</small>
</label>
<label class="checkbox-label">
<input
type="checkbox"
v-model="accessModes"
value="ReadWriteOncePod"
:disabled="isSubmitting"
/>
ReadWriteOncePod (RWOP)
<small>Volume can be mounted as read-write by a single pod</small>
</label>
</div>
</div>
<div class="form-section">
<h4>Volume Mode</h4>
<div class="radio-group">
<label class="radio-label">
<input
type="radio"
v-model="volumeMode"
value="Filesystem"
:disabled="isSubmitting"
/>
Filesystem
<small>Volume is mounted into pods as a directory</small>
</label>
<label class="radio-label">
<input
type="radio"
v-model="volumeMode"
value="Block"
:disabled="isSubmitting"
/>
Block
<small>Volume is used as a raw block device</small>
</label>
</div>
</div>
<div class="form-section">
<h4>Labels (Optional)</h4>
<div
v-for="(label, index) in labels"
:key="index"
class="label-row"
>
<div class="form-group key-field">
<input
v-model="label.key"
type="text"
placeholder="key"
:disabled="isSubmitting"
/>
</div>
<div class="form-group value-field">
<input
v-model="label.value"
type="text"
placeholder="value"
:disabled="isSubmitting"
/>
</div>
<button
class="remove-button"
@click="removeLabel(index)"
:disabled="isSubmitting || labels.length <= 1"
>
×
</button>
</div>
<button
class="add-button"
@click="addLabel"
:disabled="isSubmitting"
>
+ Add Label
</button>
</div>
</div>
<div class="modal-footer">
<button
class="cancel-button"
@click="closeModal"
:disabled="isSubmitting"
>
Cancel
</button>
<button
class="create-button"
@click="createPvc"
:disabled="!isFormValid || isSubmitting"
>
{{ isSubmitting ? 'Creating...' : 'Create PVC' }}
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, PropType } from 'vue';
import { PersistentVolumeClaimCreateOptions } from '../types/custom';
import { KubernetesCluster } from '../types/kubernetes';
export default defineComponent({
name: 'PersistentVolumeClaimsCreateGuided',
props: {
show: {
type: Boolean,
required: true
},
cluster: {
type: Object as PropType<KubernetesCluster | null>,
required: false,
default: null
}
},
emits: ['close', 'create-pvc'],
setup(props, { emit }) {
const name = ref('');
const namespace = ref('default');
const storageClass = ref('');
const storageSize = ref(1);
const storageUnit = ref('Gi');
const accessModes = ref(['ReadWriteOnce']);
const volumeMode = ref('Filesystem');
const isSubmitting = ref(false);
const error = ref('');
const labels = ref([{ key: '', value: '' }]);
const isFormValid = computed(() => {
if (!name.value.trim()) return false;
if (!namespace.value.trim()) return false;
if (!storageSize.value || storageSize.value <= 0) return false;
if (accessModes.value.length === 0) return false;
return true;
});
const addLabel = () => {
labels.value.push({ key: '', value: '' });
};
const removeLabel = (index: number) => {
if (labels.value.length > 1) {
labels.value.splice(index, 1);
}
};
const resetForm = () => {
name.value = '';
namespace.value = 'default';
storageClass.value = '';
storageSize.value = 1;
storageUnit.value = 'Gi';
accessModes.value = ['ReadWriteOnce'];
volumeMode.value = 'Filesystem';
labels.value = [{ key: '', value: '' }];
error.value = '';
isSubmitting.value = false;
};
const closeModal = () => {
resetForm();
emit('close');
};
const createPvc = () => {
if (!name.value.trim()) {
error.value = 'Name is required';
return;
}
if (!namespace.value.trim()) {
error.value = 'Namespace is required';
return;
}
if (!props.cluster) {
error.value = 'No cluster selected';
return;
}
if (!storageSize.value || storageSize.value <= 0) {
error.value = 'Storage size must be greater than 0';
return;
}
if (accessModes.value.length === 0) {
error.value = 'At least one access mode must be selected';
return;
}
isSubmitting.value = true;
// Build labels object
const labelsObj = {};
labels.value.forEach(label => {
if (label.key.trim() && label.value.trim()) {
labelsObj[label.key.trim()] = label.value.trim();
}
});
// Build PVC spec
const spec: any = {
accessModes: accessModes.value,
resources: {
requests: {
storage: `${storageSize.value}${storageUnit.value}`
}
},
volumeMode: volumeMode.value
};
// Add storage class if specified
if (storageClass.value.trim()) {
spec.storageClassName = storageClass.value.trim();
}
// Build metadata
const metadata: any = {
name: name.value.trim(),
namespace: namespace.value.trim()
};
// Add labels if any
if (Object.keys(labelsObj).length > 0) {
metadata.labels = labelsObj;
}
const opts: PersistentVolumeClaimCreateOptions = {
context: props.cluster.contextName,
opts: {
definition: {
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
metadata: metadata,
spec: spec
},
isYaml: false
}
};
emit('create-pvc', opts);
resetForm();
closeModal();
};
return {
name,
namespace,
storageClass,
storageSize,
storageUnit,
accessModes,
volumeMode,
labels,
isSubmitting,
error,
isFormValid,
addLabel,
removeLabel,
closeModal,
createPvc
};
}
});
</script>
<style src="@/assets/css/CreateResource.css" scoped></style>
<style scoped>
.storage-input-group {
display: flex;
gap: 0.5rem;
}
.storage-input-group input {
flex: 1;
}
.storage-input-group select {
width: 80px;
padding: 0.5rem;
border: 1px solid #444;
border-radius: 4px;
background-color: #2d2d2d;
color: #ffffff;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.checkbox-label {
display: flex;
flex-direction: column;
gap: 0.25rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.radio-label {
display: flex;
flex-direction: column;
gap: 0.25rem;
cursor: pointer;
}
.radio-label input[type="radio"] {
width: auto;
margin-right: 0.5rem;
}
.field-hint {
color: #999;
font-size: 0.8rem;
margin-top: 0.25rem;
display: block;
}
.label-row {
display: flex;
gap: 0.5rem;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.key-field,
.value-field {
flex: 1;
margin-bottom: 0;
}
.remove-button {
background-color: #dc3545;
border: 1px solid #dc3545;
color: white;
border-radius: 4px;
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1.5rem;
}
.remove-button:hover:not(:disabled) {
background-color: #c82333;
}
.remove-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.add-button {
background-color: #28a745;
border: 1px solid #28a745;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 0.5rem;
}
.add-button:hover:not(:disabled) {
background-color: #218838;
}
.add-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>