mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
refactor(app): persisted folders form section [EE-6235] (#10693)
* refactor(app): persisted folder section [EE-6235]
This commit is contained in:
parent
7a2412b1be
commit
e07ee05ee7
39 changed files with 732 additions and 374 deletions
|
@ -137,9 +137,9 @@
|
|||
<table-column-header
|
||||
col-title="'Storage'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.StorageClass.Name')"
|
||||
is-sorted="$ctrl.state.orderBy === 'PersistentVolumeClaim.storageClass.Name'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'PersistentVolumeClaim.storageClass.Name' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.storageClass.Name')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
|
@ -188,7 +188,7 @@
|
|||
<span ng-if="!item.Applications.length">-</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.PersistentVolumeClaim.StorageClass.Name }}
|
||||
{{ item.PersistentVolumeClaim.storageClass.Name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.PersistentVolumeClaim.Storage }}
|
||||
|
|
|
@ -180,7 +180,7 @@ class KubernetesApplicationConverter {
|
|||
persistedFolder.MountPath = matchingVolumeMount.mountPath;
|
||||
|
||||
if (volume.persistentVolumeClaim) {
|
||||
persistedFolder.PersistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
|
||||
persistedFolder.persistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
|
||||
} else {
|
||||
persistedFolder.HostPath = volume.hostPath.path;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
|||
res.CreationDate = data.metadata.creationTimestamp;
|
||||
res.Storage = `${data.spec.resources.requests.storage}B`;
|
||||
res.AccessModes = data.spec.accessModes || [];
|
||||
res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
|
||||
res.storageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
|
||||
res.Yaml = yaml ? yaml.data : '';
|
||||
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
|
||||
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
|
||||
|
@ -31,30 +31,32 @@ class KubernetesPersistentVolumeClaimConverter {
|
|||
* @param {KubernetesApplicationFormValues} formValues
|
||||
*/
|
||||
static applicationFormValuesToVolumeClaims(formValues) {
|
||||
_.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion);
|
||||
_.remove(formValues.PersistedFolders, (item) => item.needsDeletion);
|
||||
const res = _.map(formValues.PersistedFolders, (item) => {
|
||||
const pvc = new KubernetesPersistentVolumeClaim();
|
||||
if (!_.isEmpty(item.ExistingVolume)) {
|
||||
const existantPVC = item.ExistingVolume.PersistentVolumeClaim;
|
||||
if (!_.isEmpty(item.existingVolume)) {
|
||||
const existantPVC = item.existingVolume.PersistentVolumeClaim;
|
||||
pvc.Name = existantPVC.Name;
|
||||
if (item.PersistentVolumeClaimName) {
|
||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
||||
if (item.persistentVolumeClaimName) {
|
||||
pvc.PreviousName = item.persistentVolumeClaimName;
|
||||
}
|
||||
pvc.StorageClass = existantPVC.StorageClass;
|
||||
pvc.storageClass = existantPVC.storageClass;
|
||||
pvc.Storage = existantPVC.Storage.charAt(0);
|
||||
pvc.CreationDate = existantPVC.CreationDate;
|
||||
pvc.Id = existantPVC.Id;
|
||||
} else {
|
||||
if (item.PersistentVolumeClaimName) {
|
||||
pvc.Name = item.PersistentVolumeClaimName;
|
||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
||||
if (item.persistentVolumeClaimName) {
|
||||
pvc.Name = item.persistentVolumeClaimName;
|
||||
if (!item.useNewVolume) {
|
||||
pvc.PreviousName = item.persistentVolumeClaimName;
|
||||
}
|
||||
} else {
|
||||
pvc.Name = formValues.Name + '-' + pvc.Name;
|
||||
}
|
||||
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0);
|
||||
pvc.StorageClass = item.StorageClass;
|
||||
pvc.Storage = '' + item.size + item.sizeUnit.charAt(0);
|
||||
pvc.storageClass = item.storageClass;
|
||||
}
|
||||
pvc.MountPath = item.ContainerPath;
|
||||
pvc.MountPath = item.containerPath;
|
||||
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||
pvc.ApplicationOwner = formValues.ApplicationOwner;
|
||||
pvc.ApplicationName = formValues.Name;
|
||||
|
@ -68,7 +70,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
|||
res.metadata.name = pvc.Name;
|
||||
res.metadata.namespace = pvc.Namespace;
|
||||
res.spec.resources.requests.storage = pvc.Storage;
|
||||
res.spec.storageClassName = pvc.StorageClass ? pvc.StorageClass.Name : '';
|
||||
res.spec.storageClassName = pvc.storageClass ? pvc.storageClass.Name : '';
|
||||
const accessModes = pvc.StorageClass && pvc.StorageClass.AccessModes ? pvc.StorageClass.AccessModes.map((accessMode) => storageClassToPVCAccessModes[accessMode]) : [];
|
||||
res.spec.accessModes = accessModes;
|
||||
res.metadata.labels.app = pvc.ApplicationName;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-c
|
|||
|
||||
class KubernetesStorageClassConverter {
|
||||
/**
|
||||
* API StorageClass to front StorageClass
|
||||
* API storageClass to front storageClass
|
||||
*/
|
||||
static apiToStorageClass(data) {
|
||||
const res = new KubernetesStorageClass();
|
||||
|
|
|
@ -391,12 +391,12 @@ class KubernetesApplicationHelper {
|
|||
/* #region PERSISTED FOLDERS FV <> VOLUMES */
|
||||
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
||||
const finalRes = _.map(persistedFolders, (folder) => {
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
||||
res.Size = parseInt(pvc.Storage, 10);
|
||||
res.SizeUnit = pvc.Storage.slice(-2);
|
||||
res.ContainerPath = folder.MountPath;
|
||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.persistentVolumeClaimName));
|
||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.storageClass);
|
||||
res.persistentVolumeClaimName = folder.persistentVolumeClaimName;
|
||||
res.size = pvc.Storage.slice(0, -2); // remove trailing units
|
||||
res.sizeUnit = pvc.Storage.slice(-2);
|
||||
res.containerPath = folder.MountPath;
|
||||
return res;
|
||||
});
|
||||
return finalRes;
|
||||
|
@ -420,11 +420,11 @@ class KubernetesApplicationHelper {
|
|||
}
|
||||
|
||||
static hasRWOOnly(formValues) {
|
||||
return _.find(formValues.PersistedFolders, (item) => item.StorageClass && _.isEqual(item.StorageClass.AccessModes, ['RWO']));
|
||||
return _.find(formValues.PersistedFolders, (item) => item.storageClass && _.isEqual(item.storageClass.AccessModes, ['RWO']));
|
||||
}
|
||||
|
||||
static hasRWX(claims) {
|
||||
return _.find(claims, (item) => item.StorageClass && _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined;
|
||||
return _.find(claims, (item) => item.storageClass && _.includes(item.storageClass.AccessModes, 'RWX')) !== undefined;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ class KubernetesResourceQuotaHelper {
|
|||
|
||||
static formatBytes(bytes, decimals = 0, base10 = true) {
|
||||
const res = {
|
||||
Size: 0,
|
||||
SizeUnit: 'B',
|
||||
size: 0,
|
||||
sizeUnit: 'B',
|
||||
};
|
||||
|
||||
if (bytes === 0) {
|
||||
|
@ -22,8 +22,8 @@ class KubernetesResourceQuotaHelper {
|
|||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return {
|
||||
Size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
|
||||
SizeUnit: sizes[i],
|
||||
size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
|
||||
sizeUnit: sizes[i],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,20 +81,20 @@ export class KubernetesApplicationEnvironmentVariableFormValue {
|
|||
* KubernetesApplicationPersistedFolderFormValue Model
|
||||
*/
|
||||
const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({
|
||||
PersistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit)
|
||||
NeedsDeletion: false,
|
||||
ContainerPath: '',
|
||||
Size: '',
|
||||
SizeUnit: 'GB',
|
||||
StorageClass: {},
|
||||
ExistingVolume: null,
|
||||
UseNewVolume: true,
|
||||
persistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit)
|
||||
needsDeletion: false,
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: {},
|
||||
existingVolume: null,
|
||||
useNewVolume: true,
|
||||
});
|
||||
|
||||
export class KubernetesApplicationPersistedFolderFormValue {
|
||||
constructor(storageClass) {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolderFormValue)));
|
||||
this.StorageClass = storageClass;
|
||||
this.storageClass = storageClass;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ export class HelmApplication {
|
|||
*/
|
||||
const _KubernetesApplicationPersistedFolder = Object.freeze({
|
||||
MountPath: '',
|
||||
PersistentVolumeClaimName: '',
|
||||
persistentVolumeClaimName: '',
|
||||
HostPath: '',
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const _KubernetesPersistentVolumeClaim = Object.freeze({
|
|||
PreviousName: '',
|
||||
Namespace: '',
|
||||
Storage: 0,
|
||||
StorageClass: {}, // KubernetesStorageClass
|
||||
storageClass: {}, // KubernetesStorageClass
|
||||
CreationDate: '',
|
||||
ApplicationOwner: '',
|
||||
AccessModes: [],
|
||||
|
|
|
@ -6,7 +6,7 @@ import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessVie
|
|||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||
import { RegistriesSelector } from '@/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector';
|
||||
import { KubeApplicationAccessPolicySelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationAccessPolicySelector';
|
||||
import { DataAccessPolicyFormSection } from '@/react/kubernetes/applications/CreateView/DataAccessPolicyFormSection';
|
||||
import { KubeServicesForm } from '@/react/kubernetes/applications/CreateView/application-services/KubeServicesForm';
|
||||
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
|
||||
import { KubeApplicationDeploymentTypeSelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationDeploymentTypeSelector';
|
||||
|
@ -28,6 +28,8 @@ import { kubeEnvVarValidationSchema } from '@/react/kubernetes/applications/Appl
|
|||
import { SecretsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/SecretsFormSection';
|
||||
import { configurationsValidationSchema } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/configurationValidationSchema';
|
||||
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
||||
import { PersistedFoldersFormSection } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection';
|
||||
import { persistedFoldersValidation } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection/persistedFoldersValidation';
|
||||
|
||||
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||
|
||||
|
@ -94,8 +96,8 @@ export const ngModule = angular
|
|||
r2a(withUIRouter(withReactQuery(withCurrentUser(NodesDatatable))), [])
|
||||
)
|
||||
.component(
|
||||
'kubeApplicationAccessPolicySelector',
|
||||
r2a(KubeApplicationAccessPolicySelector, [
|
||||
'dataAccessPolicyFormSection',
|
||||
r2a(DataAccessPolicyFormSection, [
|
||||
'value',
|
||||
'onChange',
|
||||
'isEdit',
|
||||
|
@ -205,3 +207,17 @@ withFormValidation(
|
|||
['values', 'onChange', 'namespace'],
|
||||
configurationsValidationSchema
|
||||
);
|
||||
|
||||
withFormValidation(
|
||||
ngModule,
|
||||
withUIRouter(withCurrentUser(withReactQuery(PersistedFoldersFormSection))),
|
||||
'persistedFoldersFormSection',
|
||||
[
|
||||
'isEdit',
|
||||
'applicationValues',
|
||||
'isAddPersistentFolderButtonShown',
|
||||
'initialValues',
|
||||
'availableVolumes',
|
||||
],
|
||||
persistedFoldersValidation
|
||||
);
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
</div>
|
||||
</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.Size }}</td>
|
||||
<td>{{ item.size }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
dir-paginate-end
|
||||
|
|
|
@ -81,9 +81,9 @@
|
|||
<table-column-header
|
||||
col-title="'Usage'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Size'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Size')"
|
||||
is-sorted="$ctrl.state.orderBy === 'size'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'size' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('size')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
</tr>
|
||||
|
@ -102,7 +102,7 @@
|
|||
</div>
|
||||
</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.Size }}</td>
|
||||
<td>{{ item.size }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
dir-paginate-end
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td>Storage Class</td>
|
||||
<td data-cy="k8sVolDetail-volStorageClassname">{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
|
||||
<td data-cy="k8sVolDetail-volStorageClassname">{{ ctrl.volume.PersistentVolumeClaim.storageClass.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access Modes</td>
|
||||
|
@ -69,7 +69,7 @@
|
|||
<tr>
|
||||
<td>Provisioner</td>
|
||||
<td data-cy="k8sVolDetail-volProvisioner">{{
|
||||
ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-'
|
||||
ctrl.volume.PersistentVolumeClaim.storageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.storageClass.Provisioner : '-'
|
||||
}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -77,14 +77,14 @@
|
|||
<td data-cy="k8sVolDetail-volCreatedAt">{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>size</td>
|
||||
<td ng-if="!ctrl.state.increaseSize">
|
||||
{{ ctrl.volume.PersistentVolumeClaim.Storage }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-click="ctrl.state.increaseSize = true"
|
||||
ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass.AllowVolumeExpansion"
|
||||
ng-if="ctrl.volume.PersistentVolumeClaim.storageClass.AllowVolumeExpansion"
|
||||
data-cy="k8sVolDetail-increaseSizeButton"
|
||||
>Increase size</button
|
||||
>
|
||||
|
|
|
@ -186,12 +186,14 @@ class KubernetesVolumeController {
|
|||
try {
|
||||
await this.getVolume();
|
||||
await this.getEvents();
|
||||
this.state.volumeSharedAccessPolicies = this.volume.PersistentVolumeClaim.AccessModes;
|
||||
let policies = KubernetesStorageClassAccessPolicies();
|
||||
this.state.volumeSharedAccessPolicyTooltips = this.state.volumeSharedAccessPolicies.map((policy) => {
|
||||
const matchingPolicy = policies.find((p) => p.Name === policy);
|
||||
return matchingPolicy ? matchingPolicy.Description : undefined;
|
||||
});
|
||||
if (this.volume.PersistentVolumeClaim.storageClass !== undefined) {
|
||||
this.state.volumeSharedAccessPolicies = this.volume.PersistentVolumeClaim.AccessModes;
|
||||
let policies = KubernetesStorageClassAccessPolicies();
|
||||
this.state.volumeSharedAccessPolicyTooltips = this.state.volumeSharedAccessPolicies.map((policy) => {
|
||||
const matchingPolicy = policies.find((p) => p.Name === policy);
|
||||
return matchingPolicy ? matchingPolicy.Description : undefined;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
|
|
@ -7,9 +7,9 @@ import { confirmDelete } from '@@/modals/confirm';
|
|||
|
||||
function buildStorages(storages, volumes) {
|
||||
_.forEach(storages, (s) => {
|
||||
const filteredVolumes = _.filter(volumes, ['PersistentVolumeClaim.StorageClass.Name', s.Name, 'PersistentVolumeClaim.StorageClass.Provisioner', s.Provisioner]);
|
||||
const filteredVolumes = _.filter(volumes, ['PersistentVolumeClaim.storageClass.Name', s.Name, 'PersistentVolumeClaim.storageClass.Provisioner', s.Provisioner]);
|
||||
s.Volumes = filteredVolumes;
|
||||
s.Size = computeSize(filteredVolumes);
|
||||
s.size = computeSize(filteredVolumes);
|
||||
});
|
||||
return storages;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ function buildStorages(storages, volumes) {
|
|||
function computeSize(volumes) {
|
||||
const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 }));
|
||||
const format = KubernetesResourceQuotaHelper.formatBytes(size);
|
||||
return `${format.Size}${format.SizeUnit}`;
|
||||
return `${format.size}${format.sizeUnit}`;
|
||||
}
|
||||
|
||||
class KubernetesVolumesController {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue