1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

refactor(app): persisted folders form section [EE-6235] (#10693)

* refactor(app): persisted folder section [EE-6235]
This commit is contained in:
Ali 2024-01-03 09:46:26 +13:00 committed by GitHub
parent 7a2412b1be
commit e07ee05ee7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 732 additions and 374 deletions

View file

@ -410,250 +410,25 @@
></secrets-form-section>
<!-- #endregion -->
<!-- #region PERSISTED FOLDERS -->
<div class="form-group">
<div class="col-sm-12 vertical-center mb-2 pt-2.5" style="margin-top: 5px">
<label class="control-label !pt-0 text-left !text-sm">Persisted folders</label>
</div>
<div class="col-sm-12 small text-muted vertical-center mt-1" ng-if="!ctrl.storageClassAvailable()">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
No storage option is available to persist data, contact your administrator to enable a storage option.
</div>
<div class="row" ng-if="ctrl.storageClassAvailable()">
<div class="col-sm-12" style="margin-top: 5px" ng-if="ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
<span class="small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
This namespace has exhausted its storage capacity. Contact your administrator to expand the capacity of the namespace.
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px" ng-repeat="persistedFolder in ctrl.formValues.PersistedFolders">
<div style="margin-top: 2px">
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
<span class="input-group-addon required">path in container</span>
<input
type="text"
class="form-control"
name="persisted_folder_path_{{ $index }}"
ng-model="persistedFolder.ContainerPath"
ng-change="ctrl.onChangePersistedFolderPath()"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
placeholder="/data"
required
data-cy="k8sAppCreate-containerPathInput_{{ $index }}"
/>
</div>
<div
class="input-group col-sm-2 input-group-sm"
ng-if="
!ctrl.isEditAndExistingPersistedFolder($index) &&
ctrl.application.ApplicationType !== ctrl.ApplicationTypes.STATEFULSET &&
ctrl.formValues.Containers.length <= 1
"
>
<span class="btn-group btn-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
<label
class="btn btn-light"
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="true"
ng-change="ctrl.useNewVolume($index)"
ng-disabled="ctrl.isNewVolumeButtonDisabled($index)"
>New volume</label
>
<label
class="btn btn-light"
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="false"
ng-change="ctrl.useExistingVolume($index)"
ng-disabled="ctrl.isExistingVolumeButtonDisabled()"
>Existing volume</label
>
</span>
</div>
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
<span class="input-group-addon required">requested size</span>
<input
type="number"
class="form-control !rounded-none"
name="persisted_folder_size_{{ $index }}"
ng-model="persistedFolder.Size"
placeholder="20"
min="0"
required
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
ng-change="ctrl.onChangeVolumeRequestedSize()"
/>
<span class="input-group-addon !rounded-r-[5px] !p-0">
<select
class="form-control !h-[28px] w-12 !rounded-r-[5px] !border-none text-xs"
ng-model="persistedFolder.SizeUnit"
ng-style="{ height: '100%', cursor: ctrl.isEditAndExistingPersistedFolder($index) ? 'not-allowed' : 'auto' }"
ng-options="unit for unit in ctrl.state.availableSizeUnits"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
ng-change="ctrl.onChangeVolumeRequestedSize()"
></select>
</span>
</div>
<div class="input-group col-sm-2 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
<span class="input-group-addon">storage</span>
<select
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
class="form-control"
ng-model="persistedFolder.StorageClass"
ng-options="storageClass as storageClass.Name for storageClass in ctrl.storageClasses"
ng-disabled="ctrl.state.isEdit || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-storageSelect_{{ $index }}"
></select>
<input
ng-if="!ctrl.hasMultipleStorageClassesAvailable()"
type="text"
class="form-control"
disabled
ng-model="persistedFolder.StorageClass.Name"
data-cy="k8sAppCreate-storageClassNameInput_{{ $index }}"
/>
</div>
<div class="input-group col-sm-5 input-group-sm" ng-if="!persistedFolder.UseNewVolume" ng-class="{ striked: persistedFolder.NeedsDeletion }">
<span class="input-group-addon">volume</span>
<select
class="form-control"
name="existing_volumes_{{ $index }}"
ng-model="ctrl.formValues.PersistedFolders[$index].ExistingVolume"
ng-options="vol as vol.PersistentVolumeClaim.Name for vol in ctrl.availableVolumes"
ng-change="ctrl.onChangeExistingVolumeSelection()"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
required
>
<option selected disabled hidden value="">Select a volume</option>
</select>
</div>
<div class="input-group col-sm-1 input-group-sm">
<div ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
<button
ng-if="!persistedFolder.NeedsDeletion"
class="btn btn-sm btn-dangerlight !ml-0 h-[30px]"
type="button"
ng-click="ctrl.removePersistedFolder($index)"
data-cy="k8sAppCreate-rmPersistentFolderButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
<button
ng-if="persistedFolder.NeedsDeletion"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePersistedFolder($index)"
data-cy="k8sAppCreate-restorePersistentButton"
>
<pr-icon icon="'rotate-cw'"></pr-icon>
</button>
</div>
</div>
</div>
<div
class="flex flex-row gap-x-1"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined ||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined ||
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined
"
>
<div class="input-group col-sm-3 input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This path is already defined.</p
>
</div>
</div>
<div class="input-group col-sm-offset-3 col-sm-3 input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid || ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Size is required.</p>
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This value must be greater than zero.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
You can only request up to
{{ ctrl.state.storages.availabilities[persistedFolder.StorageClass.Name] | kubernetesAppStorageRequestSizeHumanReadable }} for
{{ persistedFolder.StorageClass.Name }}
</p>
</div>
<div
class="small text-warning"
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
>
<ng-messages for="kubernetesApplicationCreationForm['existing_volumes_' + $index].$error">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Volume is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This volume is already used.</p
>
</div>
</div>
<div class="input-group col-sm-1 input-group-sm"> </div>
</div>
</div>
<div class="col-sm-12 mt-2">
<span
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
ng-click="ctrl.addPersistedFolder()"
ng-if="ctrl.isAddPersistentFolderButtonShowed()"
data-cy="k8sAppCreate-addPersistentFolderButton"
>
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add persisted folder
</span>
</div>
</div>
</div>
<!-- #endregion -->
<persisted-folders-form-section
values="ctrl.formValues.PersistedFolders"
initial-values="ctrl.formValues.OriginalPersistedFolders"
on-change="(ctrl.onChangePersistedFolder)"
is-edit="ctrl.state.isEdit"
application-values="ctrl.formValues"
is-add-persistent-folder-button-shown="ctrl.isAddPersistentFolderButtonShown()"
available-volumes="ctrl.availableVolumes"
validation-data="{ namespaceQuotas: ctrl.formValues.ResourcePool.Quota, persistedFolders: ctrl.formValues.PersistedFolders, storageAvailabilities: ctrl.state.storages.availabilities }"
></persisted-folders-form-section>
<!-- #region DATA ACCESS POLICY -->
<div ng-if="ctrl.showDataAccessPolicySection()">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Data access policy</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 small text-muted"> Specify how the data will be used across instances. </div>
</div>
<kube-application-access-policy-selector
<data-access-policy-form-section
value="ctrl.formValues.DataAccessPolicy"
on-change="(ctrl.onDataAccessPolicyChange)"
is-edit="ctrl.state.isEdit"
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
></kube-application-access-policy-selector>
></data-access-policy-form-section>
</div>
<!-- #endregion -->

View file

@ -153,6 +153,7 @@ class KubernetesCreateApplicationController {
this.onEnvironmentVariableChange = this.onEnvironmentVariableChange.bind(this);
this.onConfigMapsChange = this.onConfigMapsChange.bind(this);
this.onSecretsChange = this.onSecretsChange.bind(this);
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
}
/* #endregion */
@ -312,21 +313,21 @@ class KubernetesCreateApplicationController {
}
restorePersistedFolder(index) {
this.formValues.PersistedFolders[index].NeedsDeletion = false;
this.formValues.PersistedFolders[index].needsDeletion = false;
this.validatePersistedFolders();
}
resetPersistedFolders() {
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
persistedFolder.ExistingVolume = null;
persistedFolder.UseNewVolume = true;
persistedFolder.existingVolume = null;
persistedFolder.useNewVolume = true;
});
this.validatePersistedFolders();
}
removePersistedFolder(index) {
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
this.formValues.PersistedFolders[index].NeedsDeletion = true;
if (this.state.isEdit && this.formValues.PersistedFolders[index].persistentVolumeClaimName) {
this.formValues.PersistedFolders[index].needsDeletion = true;
} else {
this.formValues.PersistedFolders.splice(index, 1);
}
@ -334,15 +335,15 @@ class KubernetesCreateApplicationController {
}
useNewVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = true;
this.formValues.PersistedFolders[index].ExistingVolume = null;
this.state.persistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
this.formValues.PersistedFolders[index].useNewVolume = true;
this.formValues.PersistedFolders[index].existingVolume = null;
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
this.validatePersistedFolders();
}
useExistingVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = false;
this.state.persistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
this.formValues.PersistedFolders[index].useNewVolume = false;
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
this.resetDeploymentType();
@ -360,22 +361,26 @@ class KubernetesCreateApplicationController {
onChangePersistedFolderPath() {
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
if (persistedFolder.NeedsDeletion) {
if (persistedFolder.needsDeletion) {
return undefined;
}
return persistedFolder.ContainerPath;
return persistedFolder.containerPath;
})
);
this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
}
onChangePersistedFolder(values) {
this.formValues.PersistedFolders = values;
}
onChangeExistingVolumeSelection() {
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
if (persistedFolder.NeedsDeletion) {
if (persistedFolder.needsDeletion) {
return undefined;
}
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
return persistedFolder.existingVolume ? persistedFolder.existingVolume.PersistentVolumeClaim.Name : '';
})
);
this.state.duplicates.existingVolumes.hasRefs = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
@ -518,8 +523,8 @@ class KubernetesCreateApplicationController {
for (let i = 0; i < this.formValues.PersistedFolders.length; i++) {
const folder = this.formValues.PersistedFolders[i];
if (folder.StorageClass && _.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
storageOptions.push(folder.StorageClass.Name);
if (folder.storageClass && _.isEqual(folder.storageClass.AccessModes, ['RWO'])) {
storageOptions.push(folder.storageClass.Name);
} else {
storageOptions.push('<no storage option available>');
}
@ -612,7 +617,7 @@ class KubernetesCreateApplicationController {
/* #region PERSISTED FOLDERS */
/* #region BUTTONS STATES */
isAddPersistentFolderButtonShowed() {
isAddPersistentFolderButtonShown() {
return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1;
}
@ -630,7 +635,7 @@ class KubernetesCreateApplicationController {
}
isEditAndExistingPersistedFolder(index) {
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
return this.state.isEdit && this.formValues.PersistedFolders[index].persistentVolumeClaimName;
}
/* #endregion */
@ -781,7 +786,7 @@ class KubernetesCreateApplicationController {
this.volumes = volumes;
const filteredVolumes = _.filter(this.volumes, (volume) => {
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.includes(volume.PersistentVolumeClaim.StorageClass.AccessModes, 'RWX');
const isRWX = volume.PersistentVolumeClaim.storageClass && _.includes(volume.PersistentVolumeClaim.storageClass.AccessModes, 'RWX');
return isUnused || isRWX;
});
this.availableVolumes = filteredVolumes;
@ -873,7 +878,11 @@ class KubernetesCreateApplicationController {
this.state.actionInProgress = true;
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues, false, this.originalServicePorts);
this.Notifications.success('Success', 'Request to update application successfully submitted');
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
this.$state.go(
'kubernetes.applications.application',
{ name: this.application.Name, namespace: this.application.ResourcePool, endpointId: this.endpoint.Id },
{ inherit: false }
);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update application');
} finally {
@ -1087,13 +1096,14 @@ class KubernetesCreateApplicationController {
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.PersistentVolumeClaimName]);
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.persistentVolumeClaimName]);
if (volume) {
persistedFolder.UseNewVolume = false;
persistedFolder.ExistingVolume = volume;
persistedFolder.useNewVolume = false;
persistedFolder.existingVolume = volume;
}
});
}
this.formValues.OriginalPersistedFolders = this.formValues.PersistedFolders;
await this.refreshNamespaceData(namespace);
} else {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);