refactor(app): persisted folders form section [EE-6235] (#10693)
* refactor(app): persisted folder section [EE-6235]pull/10695/head
parent
7a2412b1be
commit
e07ee05ee7
|
@ -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 {
|
||||
|
|
|
@ -129,10 +129,17 @@ function createFormValidatorController<TFormModel, TData = never>(
|
|||
});
|
||||
}
|
||||
|
||||
async $onChanges(changes: { values?: { currentValue: TFormModel } }) {
|
||||
async $onChanges(changes: {
|
||||
values?: { currentValue: TFormModel };
|
||||
validationData?: { currentValue: TData };
|
||||
}) {
|
||||
if (changes.values) {
|
||||
await this.runValidation(changes.values.currentValue);
|
||||
}
|
||||
// also run validation if validationData changes
|
||||
if (changes.validationData) {
|
||||
await this.runValidation(this.values!);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import styles from './ButtonSelector.module.css';
|
|||
export interface Option<T> {
|
||||
value: T;
|
||||
label?: ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
|
@ -43,7 +44,7 @@ export function ButtonSelector<T extends string | number | boolean>({
|
|||
key={option.value.toString()}
|
||||
selected={value === option.value}
|
||||
onChange={() => onChange(option.value)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || option.disabled}
|
||||
readOnly={readOnly}
|
||||
>
|
||||
{option.label || option.value.toString()}
|
||||
|
|
|
@ -10,6 +10,7 @@ interface Props {
|
|||
titleSize?: 'sm' | 'md' | 'lg';
|
||||
isFoldable?: boolean;
|
||||
defaultFolded?: boolean;
|
||||
titleClassName?: string;
|
||||
}
|
||||
|
||||
export function FormSection({
|
||||
|
@ -18,6 +19,7 @@ export function FormSection({
|
|||
children,
|
||||
isFoldable = false,
|
||||
defaultFolded = isFoldable,
|
||||
titleClassName,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
||||
|
||||
|
@ -26,6 +28,7 @@ export function FormSection({
|
|||
<FormSectionTitle
|
||||
htmlFor={isFoldable ? `foldingButton${title}` : ''}
|
||||
titleSize={titleSize}
|
||||
className={titleClassName}
|
||||
>
|
||||
{isFoldable && (
|
||||
<button
|
||||
|
|
|
@ -4,6 +4,7 @@ import { PropsWithChildren } from 'react';
|
|||
interface Props {
|
||||
htmlFor?: string;
|
||||
titleSize?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const tailwindTitleSize = {
|
||||
|
@ -16,6 +17,7 @@ export function FormSectionTitle({
|
|||
children,
|
||||
htmlFor,
|
||||
titleSize = 'md',
|
||||
className,
|
||||
}: PropsWithChildren<Props>) {
|
||||
if (htmlFor) {
|
||||
return (
|
||||
|
@ -23,7 +25,8 @@ export function FormSectionTitle({
|
|||
htmlFor={htmlFor}
|
||||
className={clsx(
|
||||
'col-sm-12 mb-2 mt-1 flex cursor-pointer items-center pl-0 font-medium',
|
||||
tailwindTitleSize[titleSize]
|
||||
tailwindTitleSize[titleSize],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
@ -34,7 +37,8 @@ export function FormSectionTitle({
|
|||
<div
|
||||
className={clsx(
|
||||
'col-sm-12 mb-2 mt-4 pl-0 font-medium',
|
||||
tailwindTitleSize[titleSize]
|
||||
tailwindTitleSize[titleSize],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -6,11 +6,13 @@ import { useInputGroupContext } from './InputGroup';
|
|||
type BaseProps<TProps> = {
|
||||
as?: ComponentType<TProps> | string;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function InputGroupAddon<TProps>({
|
||||
children,
|
||||
as = 'span',
|
||||
className,
|
||||
required,
|
||||
...props
|
||||
}: PropsWithChildren<BaseProps<TProps> & TProps>) {
|
||||
|
@ -19,7 +21,7 @@ export function InputGroupAddon<TProps>({
|
|||
|
||||
return (
|
||||
<Component
|
||||
className={clsx('input-group-addon', required && 'required')}
|
||||
className={clsx('input-group-addon', required && 'required', className)}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
|
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onChange(value: number): void;
|
||||
}
|
||||
|
||||
export function KubeApplicationAccessPolicySelector({
|
||||
export function DataAccessPolicyFormSection({
|
||||
isEdit,
|
||||
persistedFoldersUseExistingVolumes,
|
||||
value,
|
|
@ -108,7 +108,7 @@ const queryKeys = {
|
|||
};
|
||||
|
||||
// useQuery to get a list of all applications from an array of namespaces
|
||||
export function useApplicationsForCluster(
|
||||
export function useApplicationsQuery(
|
||||
environemtId: EnvironmentId,
|
||||
namespaces?: string[]
|
||||
) {
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StorageClass } from '@/react/portainer/environments/types';
|
||||
|
||||
import { ItemError } from '@@/form-components/InputList/InputList';
|
||||
import { Option } from '@@/form-components/PortainerSelect';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { isErrorType } from '@@/form-components/formikUtils';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
|
||||
import { ApplicationFormValues } from '../../types';
|
||||
|
||||
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
||||
|
||||
type Props = {
|
||||
initialValues: PersistedFolderFormValue[];
|
||||
item: PersistedFolderFormValue;
|
||||
onChange: (value: PersistedFolderFormValue) => void;
|
||||
error: ItemError<PersistedFolderFormValue>;
|
||||
storageClasses: StorageClass[];
|
||||
index: number;
|
||||
PVCOptions: Option<string>[];
|
||||
availableVolumes: ExistingVolume[];
|
||||
isEdit: boolean;
|
||||
applicationValues: ApplicationFormValues;
|
||||
};
|
||||
|
||||
export function PersistedFolderItem({
|
||||
initialValues,
|
||||
item,
|
||||
onChange,
|
||||
error,
|
||||
storageClasses,
|
||||
index,
|
||||
PVCOptions,
|
||||
availableVolumes,
|
||||
isEdit,
|
||||
applicationValues,
|
||||
}: Props) {
|
||||
// rule out the error being of type string
|
||||
const formikError = isErrorType(error) ? error : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-start flex-wrap gap-x-2 gap-y-2">
|
||||
<div>
|
||||
<InputGroup
|
||||
size="small"
|
||||
className={clsx('min-w-[250px]', item.needsDeletion && 'striked')}
|
||||
>
|
||||
<InputGroup.Addon required>Path in container</InputGroup.Addon>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="e.g. /data"
|
||||
disabled={
|
||||
(isEdit && isExistingPersistedFolder()) ||
|
||||
applicationValues.Containers.length > 1
|
||||
}
|
||||
value={item.containerPath}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...item,
|
||||
containerPath: e.target.value,
|
||||
})
|
||||
}
|
||||
data-cy={`k8sAppCreate-containerPathInput_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{formikError?.containerPath && (
|
||||
<FormError>{formikError?.containerPath}</FormError>
|
||||
)}
|
||||
</div>
|
||||
{isToggleVolumeTypeVisible() && (
|
||||
<ButtonSelector<boolean>
|
||||
onChange={(isNewVolume) =>
|
||||
onChange({
|
||||
...item,
|
||||
useNewVolume: isNewVolume,
|
||||
size: isNewVolume ? item.size : '',
|
||||
existingVolume: isNewVolume ? undefined : availableVolumes[0],
|
||||
})
|
||||
}
|
||||
value={item.useNewVolume}
|
||||
options={[
|
||||
{ value: true, label: 'New volume' },
|
||||
{
|
||||
value: false,
|
||||
label: 'Existing volume',
|
||||
disabled: PVCOptions.length === 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{item.useNewVolume && (
|
||||
<>
|
||||
<div>
|
||||
<InputGroup
|
||||
size="small"
|
||||
className={clsx(
|
||||
'min-w-fit flex',
|
||||
item.needsDeletion && 'striked'
|
||||
)}
|
||||
>
|
||||
<InputGroup.Addon className="min-w-fit" required>
|
||||
Requested size
|
||||
</InputGroup.Addon>
|
||||
<Input
|
||||
className="!rounded-none -mr-[1px] !w-20"
|
||||
type="number"
|
||||
placeholder="e.g. 20"
|
||||
min="0"
|
||||
disabled={
|
||||
(isEdit && isExistingPersistedFolder()) ||
|
||||
applicationValues.Containers.length > 1
|
||||
}
|
||||
value={item.size}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...item,
|
||||
size: e.target.value,
|
||||
})
|
||||
}
|
||||
data-cy={`k8sAppCreate-persistentFolderSizeInput_${index}`}
|
||||
/>
|
||||
<Select<Option<string>>
|
||||
size="sm"
|
||||
className="min-w-fit"
|
||||
options={[
|
||||
{ label: 'MB', value: 'MB' },
|
||||
{ label: 'GB', value: 'GB' },
|
||||
{ label: 'TB', value: 'TB' },
|
||||
]}
|
||||
value={{
|
||||
label: item.sizeUnit ?? '',
|
||||
value: item.sizeUnit ?? '',
|
||||
}}
|
||||
onChange={(option) =>
|
||||
onChange({ ...item, sizeUnit: option?.value ?? 'GB' })
|
||||
}
|
||||
isDisabled={
|
||||
(isEdit && isExistingPersistedFolder()) ||
|
||||
applicationValues.Containers.length > 1
|
||||
}
|
||||
data-cy={`k8sAppCreate-persistentFolderSizeUnitSelect_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{formikError?.size && <FormError>{formikError?.size}</FormError>}
|
||||
</div>
|
||||
<InputGroup
|
||||
size="small"
|
||||
className={clsx(item.needsDeletion && 'striked')}
|
||||
>
|
||||
<InputGroup.Addon>Storage</InputGroup.Addon>
|
||||
<Select<Option<string>>
|
||||
className="w-40"
|
||||
size="sm"
|
||||
options={storageClasses.map((sc) => ({
|
||||
label: sc.Name,
|
||||
value: sc.Name,
|
||||
}))}
|
||||
value={getStorageClassValue(storageClasses, item)}
|
||||
onChange={(option) =>
|
||||
onChange({
|
||||
...item,
|
||||
storageClass:
|
||||
storageClasses.find((sc) => sc.Name === option?.value) ??
|
||||
storageClasses[0],
|
||||
})
|
||||
}
|
||||
isDisabled={
|
||||
(isEdit && isExistingPersistedFolder()) ||
|
||||
applicationValues.Containers.length > 1 ||
|
||||
storageClasses.length <= 1
|
||||
}
|
||||
data-cy={`k8sAppCreate-storageSelect_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
</>
|
||||
)}
|
||||
{!item.useNewVolume && (
|
||||
<InputGroup
|
||||
size="small"
|
||||
className={clsx(item.needsDeletion && 'striked')}
|
||||
>
|
||||
<InputGroup.Addon>Volume</InputGroup.Addon>
|
||||
<Select<Option<string>>
|
||||
className="w-[440px]"
|
||||
size="sm"
|
||||
options={PVCOptions}
|
||||
value={PVCOptions.find(
|
||||
(pvc) => pvc.value === item.persistentVolumeClaimName
|
||||
)}
|
||||
onChange={(option) =>
|
||||
onChange({
|
||||
...item,
|
||||
persistentVolumeClaimName: option?.value,
|
||||
existingVolume: availableVolumes.find(
|
||||
(pvc) => pvc.PersistentVolumeClaim.Name === option?.value
|
||||
),
|
||||
})
|
||||
}
|
||||
isDisabled={
|
||||
(isEdit && isExistingPersistedFolder()) ||
|
||||
applicationValues.Containers.length > 1 ||
|
||||
availableVolumes.length <= 1
|
||||
}
|
||||
data-cy={`k8sAppCreate-pvcSelect_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
function isExistingPersistedFolder() {
|
||||
return !!initialValues?.[index]?.persistentVolumeClaimName;
|
||||
}
|
||||
|
||||
function isToggleVolumeTypeVisible() {
|
||||
return (
|
||||
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
||||
applicationValues.ApplicationType !==
|
||||
KubernetesApplicationTypes.STATEFULSET && // and if it's not a statefulset
|
||||
applicationValues.Containers.length <= 1 // and if there is only one container);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageClassValue(
|
||||
storageClasses: StorageClass[],
|
||||
persistedFolder: PersistedFolderFormValue
|
||||
) {
|
||||
const matchingClass =
|
||||
storageClasses.find(
|
||||
(sc) => sc.Name === persistedFolder.storageClass?.Name
|
||||
) ?? storageClasses[0];
|
||||
return { label: matchingClass?.Name, value: matchingClass?.Name };
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||
import { StorageClass } from '@/react/portainer/environments/types';
|
||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Option } from '@@/form-components/PortainerSelect';
|
||||
import { InlineLoader } from '@@/InlineLoader';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { InputList } from '@@/form-components/InputList';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { ApplicationFormValues } from '../../types';
|
||||
|
||||
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
||||
import { PersistedFolderItem } from './PersistedFolderItem';
|
||||
|
||||
type Props = {
|
||||
values: PersistedFolderFormValue[];
|
||||
initialValues: PersistedFolderFormValue[];
|
||||
onChange: (values: PersistedFolderFormValue[]) => void;
|
||||
errors: FormikErrors<PersistedFolderFormValue[]>;
|
||||
isAddPersistentFolderButtonShown: unknown;
|
||||
isEdit: boolean;
|
||||
applicationValues: ApplicationFormValues;
|
||||
availableVolumes: ExistingVolume[];
|
||||
};
|
||||
|
||||
export function PersistedFoldersFormSection({
|
||||
values,
|
||||
initialValues,
|
||||
onChange,
|
||||
errors,
|
||||
isAddPersistentFolderButtonShown,
|
||||
isEdit,
|
||||
applicationValues,
|
||||
availableVolumes,
|
||||
}: Props) {
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
const storageClasses =
|
||||
environmentQuery.data?.Kubernetes.Configuration.StorageClasses ?? [];
|
||||
const PVCOptions = usePVCOptions(availableVolumes);
|
||||
|
||||
return (
|
||||
<FormSection
|
||||
title="Persisted folders"
|
||||
titleSize="sm"
|
||||
titleClassName="control-label !text-[0.9em]"
|
||||
>
|
||||
{storageClasses.length === 0 && (
|
||||
<TextTip color="blue">
|
||||
No storage option is available to persist data, contact your
|
||||
administrator to enable a storage option.
|
||||
</TextTip>
|
||||
)}
|
||||
{environmentQuery.isLoading && (
|
||||
<InlineLoader>Loading volumes...</InlineLoader>
|
||||
)}
|
||||
<InputList<PersistedFolderFormValue>
|
||||
value={values}
|
||||
onChange={onChange}
|
||||
errors={errors}
|
||||
isDeleteButtonHidden={isDeleteButtonHidden()}
|
||||
deleteButtonDataCy="k8sAppCreate-persistentFolderRemoveButton"
|
||||
addButtonDataCy="k8sAppCreate-persistentFolderAddButton"
|
||||
disabled={storageClasses.length === 0}
|
||||
addButtonError={getAddButtonError(storageClasses)}
|
||||
isAddButtonHidden={!isAddPersistentFolderButtonShown}
|
||||
renderItem={(item, onChange, index, error) => (
|
||||
<PersistedFolderItem
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
PVCOptions={PVCOptions}
|
||||
availableVolumes={availableVolumes}
|
||||
storageClasses={storageClasses ?? []}
|
||||
index={index}
|
||||
isEdit={isEdit}
|
||||
applicationValues={applicationValues}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
)}
|
||||
itemBuilder={() => ({
|
||||
persistentVolumeClaimName:
|
||||
availableVolumes[0]?.PersistentVolumeClaim.Name || '',
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: storageClasses[0],
|
||||
useNewVolume: true,
|
||||
existingVolume: undefined,
|
||||
needsDeletion: false,
|
||||
})}
|
||||
addLabel="Add persisted folder"
|
||||
canUndoDelete={isEdit}
|
||||
/>
|
||||
</FormSection>
|
||||
);
|
||||
|
||||
function isDeleteButtonHidden() {
|
||||
return (
|
||||
(isEdit &&
|
||||
applicationValues.ApplicationType ===
|
||||
KubernetesApplicationTypes.STATEFULSET) ||
|
||||
applicationValues.Containers.length >= 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function usePVCOptions(existingPVCs: ExistingVolume[]): Option<string>[] {
|
||||
return useMemo(
|
||||
() =>
|
||||
existingPVCs.map((pvc) => ({
|
||||
label: pvc.PersistentVolumeClaim.Name ?? '',
|
||||
value: pvc.PersistentVolumeClaim.Name ?? '',
|
||||
})),
|
||||
[existingPVCs]
|
||||
);
|
||||
}
|
||||
|
||||
function getAddButtonError(storageClasses: StorageClass[]) {
|
||||
if (storageClasses.length === 0) {
|
||||
return 'No storage option available';
|
||||
}
|
||||
return '';
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { PersistedFoldersFormSection } from './PersistedFoldersFormSection';
|
|
@ -0,0 +1,113 @@
|
|||
import { SchemaOf, array, boolean, object, string } from 'yup';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { StorageClass } from '@/react/portainer/environments/types';
|
||||
|
||||
import { buildUniquenessTest } from '@@/form-components/validate-unique';
|
||||
|
||||
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
||||
|
||||
type FormData = {
|
||||
namespaceQuotas: unknown;
|
||||
persistedFolders: PersistedFolderFormValue[];
|
||||
storageAvailabilities: Record<string, number>;
|
||||
};
|
||||
|
||||
export function persistedFoldersValidation(
|
||||
formData?: FormData
|
||||
): SchemaOf<PersistedFolderFormValue[]> {
|
||||
return array(
|
||||
object({
|
||||
persistentVolumeClaimName: string(),
|
||||
containerPath: string().required('Path is required.'),
|
||||
size: string().when('useNewVolume', {
|
||||
is: true,
|
||||
then: string()
|
||||
.test(
|
||||
'quotaExceeded',
|
||||
'Requested size exceeds available quota for this storage class.',
|
||||
// eslint-disable-next-line prefer-arrow-callback, func-names
|
||||
function (this) {
|
||||
const persistedFolderFormValue = this
|
||||
.parent as PersistedFolderFormValue;
|
||||
const quota = formData?.namespaceQuotas;
|
||||
let quotaExceeded = false;
|
||||
if (quota) {
|
||||
const pfs = formData?.persistedFolders;
|
||||
const groups = _.groupBy(pfs, 'storageClass.Name');
|
||||
_.forOwn(groups, (storagePfs, storageClassName) => {
|
||||
if (
|
||||
storageClassName ===
|
||||
persistedFolderFormValue.storageClass.Name
|
||||
) {
|
||||
const newPfs = _.filter(storagePfs, {
|
||||
persistentVolumeClaimName: '',
|
||||
});
|
||||
const requestedSize = _.reduce(
|
||||
newPfs,
|
||||
(sum, pf) =>
|
||||
pf.useNewVolume && pf.size
|
||||
? sum +
|
||||
filesizeParser(`${pf.size}${pf.sizeUnit}`, {
|
||||
base: 10,
|
||||
})
|
||||
: sum,
|
||||
0
|
||||
);
|
||||
if (
|
||||
formData?.storageAvailabilities[storageClassName] <
|
||||
requestedSize
|
||||
) {
|
||||
quotaExceeded = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return !quotaExceeded;
|
||||
}
|
||||
)
|
||||
.required('Size is required.'),
|
||||
}),
|
||||
sizeUnit: string().when('useNewVolume', {
|
||||
is: true,
|
||||
then: string().required('Size unit is required.'),
|
||||
}),
|
||||
storageClass: storageClassValidation(),
|
||||
useNewVolume: boolean().required(),
|
||||
existingVolume: existingVolumeValidation().nullable(),
|
||||
needsDeletion: boolean(),
|
||||
})
|
||||
).test(
|
||||
'containerPath',
|
||||
'This path is already defined.',
|
||||
buildUniquenessTest(() => 'This path is already defined.', 'containerPath')
|
||||
);
|
||||
}
|
||||
|
||||
function storageClassValidation(): SchemaOf<StorageClass> {
|
||||
return object({
|
||||
Name: string().required(),
|
||||
AccessModes: array(string().required()).required(),
|
||||
AllowVolumeExpansion: boolean().required(),
|
||||
Provisioner: string().required(),
|
||||
});
|
||||
}
|
||||
|
||||
function existingVolumeValidation(): SchemaOf<ExistingVolume> {
|
||||
return object({
|
||||
PersistentVolumeClaim: object({
|
||||
Id: string().required(),
|
||||
Name: string().required(),
|
||||
Namespace: string().required(),
|
||||
Storage: string().required(),
|
||||
storageClass: storageClassValidation(),
|
||||
CreationDate: string().required(),
|
||||
ApplicationOwner: string().required(),
|
||||
ApplicationName: string().required(),
|
||||
PreviousName: string(),
|
||||
MountPath: string(),
|
||||
Yaml: string(),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { StorageClass } from '@/react/portainer/environments/types';
|
||||
|
||||
export type PersistedFolderFormValue = {
|
||||
containerPath: string;
|
||||
storageClass: StorageClass;
|
||||
useNewVolume: boolean;
|
||||
persistentVolumeClaimName?: string; // empty for new volumes, set for existing volumes
|
||||
sizeUnit?: string;
|
||||
size?: string;
|
||||
existingVolume?: ExistingVolume;
|
||||
needsDeletion?: boolean;
|
||||
};
|
||||
|
||||
export type ExistingVolume = {
|
||||
PersistentVolumeClaim: {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Namespace: string;
|
||||
Storage: string;
|
||||
storageClass: StorageClass;
|
||||
CreationDate: string;
|
||||
ApplicationOwner: string;
|
||||
ApplicationName: string;
|
||||
PreviousName?: string;
|
||||
MountPath?: string;
|
||||
Yaml?: string;
|
||||
};
|
||||
};
|
|
@ -11,6 +11,11 @@ import {
|
|||
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||
import { RawExtension } from 'kubernetes-types/runtime';
|
||||
|
||||
export type ApplicationFormValues = {
|
||||
Containers: Array<unknown>;
|
||||
ApplicationType: number; // KubernetesApplicationTypes
|
||||
};
|
||||
|
||||
export type Application = Deployment | DaemonSet | StatefulSet | Pod;
|
||||
|
||||
// Revisions are have the previous application state and are used for rolling back applications to their previous state.
|
||||
|
|
|
@ -25,7 +25,7 @@ import { IngressControllerClassMap } from '../../ingressClass/types';
|
|||
import { useIsRBACEnabledQuery } from '../../getIsRBACEnabled';
|
||||
import { getIngressClassesFormValues } from '../../ingressClass/IngressClassDatatable/utils';
|
||||
|
||||
import { useStorageClassesFormValues } from './useStorageClassesFormValues';
|
||||
import { useStorageClassesFormValues } from './useStorageClasses';
|
||||
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
||||
import { configureValidationSchema } from './validation';
|
||||
import { RBACAlert } from './RBACAlert';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Switch } from '@@/form-components/SwitchField/Switch';
|
|||
|
||||
import { StorageAccessModeSelector } from './StorageAccessModeSelector';
|
||||
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
||||
import { availableStorageClassPolicies } from './useStorageClassesFormValues';
|
||||
import { availableStorageClassPolicies } from './useStorageClasses';
|
||||
|
||||
type Props = {
|
||||
storageClassValues: StorageClassFormValues[];
|
||||
|
|
|
@ -26,9 +26,31 @@ export const availableStorageClassPolicies = [
|
|||
},
|
||||
];
|
||||
|
||||
export function useStorageClassesFormValues(
|
||||
environment: Environment | null | undefined
|
||||
) {
|
||||
export function useStorageClasses(environment?: Environment | null) {
|
||||
return useQuery(
|
||||
[
|
||||
'environments',
|
||||
environment?.Id,
|
||||
'kubernetes',
|
||||
'storageclasses',
|
||||
// include the storage classes in the cache key to force a refresh when the storage classes change in the environment object
|
||||
JSON.stringify(environment?.Kubernetes.Configuration.StorageClasses),
|
||||
],
|
||||
async () => {
|
||||
if (!environment) {
|
||||
return [];
|
||||
}
|
||||
const storageClasses = await getStorageClasses(environment.Id);
|
||||
return storageClasses;
|
||||
},
|
||||
{
|
||||
...withError('Failure', `Unable to get Storage Classes`),
|
||||
enabled: !!environment,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useStorageClassesFormValues(environment?: Environment | null) {
|
||||
return useQuery(
|
||||
[
|
||||
'environments',
|
|
@ -8,7 +8,7 @@ import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultD
|
|||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { useApplicationsForCluster } from '@/react/kubernetes/applications/application.queries';
|
||||
import { useApplicationsQuery } from '@/react/kubernetes/applications/application.queries';
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { pluralize } from '@/portainer/helpers/strings';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
|
@ -54,8 +54,10 @@ export function ConfigMapsDatatable() {
|
|||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
}
|
||||
);
|
||||
const { data: applications, ...applicationsQuery } =
|
||||
useApplicationsForCluster(environmentId, namespaceNames);
|
||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
|
||||
const filteredConfigMaps = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultD
|
|||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { useApplicationsForCluster } from '@/react/kubernetes/applications/application.queries';
|
||||
import { useApplicationsQuery } from '@/react/kubernetes/applications/application.queries';
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { pluralize } from '@/portainer/helpers/strings';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
|
@ -54,8 +54,10 @@ export function SecretsDatatable() {
|
|||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
}
|
||||
);
|
||||
const { data: applications, ...applicationsQuery } =
|
||||
useApplicationsForCluster(environmentId, namespaceNames);
|
||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
|
||||
const filteredSecrets = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -8,8 +8,8 @@ import { DashboardGrid } from '@@/DashboardItem/DashboardGrid';
|
|||
import { DashboardItem } from '@@/DashboardItem/DashboardItem';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useApplicationsForCluster } from '../applications/application.queries';
|
||||
import { usePVCsForCluster } from '../volumes/queries';
|
||||
import { useApplicationsQuery } from '../applications/application.queries';
|
||||
import { usePVCsQuery } from '../volumes/usePVCsQuery';
|
||||
import { useServicesForCluster } from '../services/service';
|
||||
import { useIngresses } from '../ingresses/queries';
|
||||
import { useConfigMapsForCluster } from '../configs/configmap.service';
|
||||
|
@ -24,9 +24,11 @@ export function DashboardView() {
|
|||
const { data: namespaces, ...namespacesQuery } =
|
||||
useNamespacesQuery(environmentId);
|
||||
const namespaceNames = namespaces && Object.keys(namespaces);
|
||||
const { data: applications, ...applicationsQuery } =
|
||||
useApplicationsForCluster(environmentId, namespaceNames);
|
||||
const { data: pvcs, ...pvcsQuery } = usePVCsForCluster(
|
||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
const { data: pvcs, ...pvcsQuery } = usePVCsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
import { getPVCsForCluster } from './service';
|
||||
|
||||
// useQuery to get a list of all persistent volume claims from an array of namespaces
|
||||
export function usePVCsForCluster(
|
||||
environemtId: EnvironmentId,
|
||||
namespaces?: string[]
|
||||
) {
|
||||
return useQuery(
|
||||
['environments', environemtId, 'kubernetes', 'pvcs'],
|
||||
() => namespaces && getPVCsForCluster(environemtId, namespaces),
|
||||
{
|
||||
...withError('Unable to retrieve perrsistent volume claims'),
|
||||
enabled: !!namespaces,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,18 +1,33 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { PersistentVolumeClaimList } from 'kubernetes-types/core/v1';
|
||||
|
||||
import axios from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
export async function getPVCsForCluster(
|
||||
// useQuery to get a list of all persistent volume claims from an array of namespaces
|
||||
export function usePVCsQuery(
|
||||
environmentId: EnvironmentId,
|
||||
namespaces: string[]
|
||||
namespaces?: string[]
|
||||
) {
|
||||
const pvcs = await Promise.all(
|
||||
namespaces.map((namespace) => getPVCs(environmentId, namespace))
|
||||
return useQuery(
|
||||
['environments', environmentId, 'kubernetes', 'pvcs'],
|
||||
async () => {
|
||||
if (!namespaces) {
|
||||
return [];
|
||||
}
|
||||
const pvcs = await Promise.all(
|
||||
namespaces?.map((namespace) => getPVCs(environmentId, namespace))
|
||||
);
|
||||
return pvcs.flat();
|
||||
},
|
||||
{
|
||||
...withError('Unable to retrieve perrsistent volume claims'),
|
||||
enabled: !!namespaces,
|
||||
}
|
||||
);
|
||||
return pvcs.flat();
|
||||
}
|
||||
|
||||
// get all persistent volume claims for a namespace
|
Loading…
Reference in New Issue