mirror of https://github.com/portainer/portainer
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
|
<table-column-header
|
||||||
col-title="'Storage'"
|
col-title="'Storage'"
|
||||||
can-sort="true"
|
can-sort="true"
|
||||||
is-sorted="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name'"
|
is-sorted="$ctrl.state.orderBy === 'PersistentVolumeClaim.storageClass.Name'"
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name' && $ctrl.state.reverseOrder"
|
is-sorted-desc="$ctrl.state.orderBy === 'PersistentVolumeClaim.storageClass.Name' && $ctrl.state.reverseOrder"
|
||||||
ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.StorageClass.Name')"
|
ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.storageClass.Name')"
|
||||||
></table-column-header>
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
|
@ -188,7 +188,7 @@
|
||||||
<span ng-if="!item.Applications.length">-</span>
|
<span ng-if="!item.Applications.length">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.PersistentVolumeClaim.StorageClass.Name }}
|
{{ item.PersistentVolumeClaim.storageClass.Name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.PersistentVolumeClaim.Storage }}
|
{{ item.PersistentVolumeClaim.Storage }}
|
||||||
|
|
|
@ -180,7 +180,7 @@ class KubernetesApplicationConverter {
|
||||||
persistedFolder.MountPath = matchingVolumeMount.mountPath;
|
persistedFolder.MountPath = matchingVolumeMount.mountPath;
|
||||||
|
|
||||||
if (volume.persistentVolumeClaim) {
|
if (volume.persistentVolumeClaim) {
|
||||||
persistedFolder.PersistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
|
persistedFolder.persistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
|
||||||
} else {
|
} else {
|
||||||
persistedFolder.HostPath = volume.hostPath.path;
|
persistedFolder.HostPath = volume.hostPath.path;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||||
res.CreationDate = data.metadata.creationTimestamp;
|
res.CreationDate = data.metadata.creationTimestamp;
|
||||||
res.Storage = `${data.spec.resources.requests.storage}B`;
|
res.Storage = `${data.spec.resources.requests.storage}B`;
|
||||||
res.AccessModes = data.spec.accessModes || [];
|
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.Yaml = yaml ? yaml.data : '';
|
||||||
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
|
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
|
||||||
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
|
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
|
||||||
|
@ -31,30 +31,32 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||||
* @param {KubernetesApplicationFormValues} formValues
|
* @param {KubernetesApplicationFormValues} formValues
|
||||||
*/
|
*/
|
||||||
static applicationFormValuesToVolumeClaims(formValues) {
|
static applicationFormValuesToVolumeClaims(formValues) {
|
||||||
_.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion);
|
_.remove(formValues.PersistedFolders, (item) => item.needsDeletion);
|
||||||
const res = _.map(formValues.PersistedFolders, (item) => {
|
const res = _.map(formValues.PersistedFolders, (item) => {
|
||||||
const pvc = new KubernetesPersistentVolumeClaim();
|
const pvc = new KubernetesPersistentVolumeClaim();
|
||||||
if (!_.isEmpty(item.ExistingVolume)) {
|
if (!_.isEmpty(item.existingVolume)) {
|
||||||
const existantPVC = item.ExistingVolume.PersistentVolumeClaim;
|
const existantPVC = item.existingVolume.PersistentVolumeClaim;
|
||||||
pvc.Name = existantPVC.Name;
|
pvc.Name = existantPVC.Name;
|
||||||
if (item.PersistentVolumeClaimName) {
|
if (item.persistentVolumeClaimName) {
|
||||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
pvc.PreviousName = item.persistentVolumeClaimName;
|
||||||
}
|
}
|
||||||
pvc.StorageClass = existantPVC.StorageClass;
|
pvc.storageClass = existantPVC.storageClass;
|
||||||
pvc.Storage = existantPVC.Storage.charAt(0);
|
pvc.Storage = existantPVC.Storage.charAt(0);
|
||||||
pvc.CreationDate = existantPVC.CreationDate;
|
pvc.CreationDate = existantPVC.CreationDate;
|
||||||
pvc.Id = existantPVC.Id;
|
pvc.Id = existantPVC.Id;
|
||||||
} else {
|
} else {
|
||||||
if (item.PersistentVolumeClaimName) {
|
if (item.persistentVolumeClaimName) {
|
||||||
pvc.Name = item.PersistentVolumeClaimName;
|
pvc.Name = item.persistentVolumeClaimName;
|
||||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
if (!item.useNewVolume) {
|
||||||
|
pvc.PreviousName = item.persistentVolumeClaimName;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
pvc.Name = formValues.Name + '-' + pvc.Name;
|
pvc.Name = formValues.Name + '-' + pvc.Name;
|
||||||
}
|
}
|
||||||
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0);
|
pvc.Storage = '' + item.size + item.sizeUnit.charAt(0);
|
||||||
pvc.StorageClass = item.StorageClass;
|
pvc.storageClass = item.storageClass;
|
||||||
}
|
}
|
||||||
pvc.MountPath = item.ContainerPath;
|
pvc.MountPath = item.containerPath;
|
||||||
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
|
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||||
pvc.ApplicationOwner = formValues.ApplicationOwner;
|
pvc.ApplicationOwner = formValues.ApplicationOwner;
|
||||||
pvc.ApplicationName = formValues.Name;
|
pvc.ApplicationName = formValues.Name;
|
||||||
|
@ -68,7 +70,7 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||||
res.metadata.name = pvc.Name;
|
res.metadata.name = pvc.Name;
|
||||||
res.metadata.namespace = pvc.Namespace;
|
res.metadata.namespace = pvc.Namespace;
|
||||||
res.spec.resources.requests.storage = pvc.Storage;
|
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]) : [];
|
const accessModes = pvc.StorageClass && pvc.StorageClass.AccessModes ? pvc.StorageClass.AccessModes.map((accessMode) => storageClassToPVCAccessModes[accessMode]) : [];
|
||||||
res.spec.accessModes = accessModes;
|
res.spec.accessModes = accessModes;
|
||||||
res.metadata.labels.app = pvc.ApplicationName;
|
res.metadata.labels.app = pvc.ApplicationName;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-c
|
||||||
|
|
||||||
class KubernetesStorageClassConverter {
|
class KubernetesStorageClassConverter {
|
||||||
/**
|
/**
|
||||||
* API StorageClass to front StorageClass
|
* API storageClass to front storageClass
|
||||||
*/
|
*/
|
||||||
static apiToStorageClass(data) {
|
static apiToStorageClass(data) {
|
||||||
const res = new KubernetesStorageClass();
|
const res = new KubernetesStorageClass();
|
||||||
|
|
|
@ -391,12 +391,12 @@ class KubernetesApplicationHelper {
|
||||||
/* #region PERSISTED FOLDERS FV <> VOLUMES */
|
/* #region PERSISTED FOLDERS FV <> VOLUMES */
|
||||||
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
|
||||||
const finalRes = _.map(persistedFolders, (folder) => {
|
const finalRes = _.map(persistedFolders, (folder) => {
|
||||||
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
|
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.persistentVolumeClaimName));
|
||||||
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
|
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.storageClass);
|
||||||
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
|
res.persistentVolumeClaimName = folder.persistentVolumeClaimName;
|
||||||
res.Size = parseInt(pvc.Storage, 10);
|
res.size = pvc.Storage.slice(0, -2); // remove trailing units
|
||||||
res.SizeUnit = pvc.Storage.slice(-2);
|
res.sizeUnit = pvc.Storage.slice(-2);
|
||||||
res.ContainerPath = folder.MountPath;
|
res.containerPath = folder.MountPath;
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
return finalRes;
|
return finalRes;
|
||||||
|
@ -420,11 +420,11 @@ class KubernetesApplicationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static hasRWOOnly(formValues) {
|
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) {
|
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 */
|
/* #endregion */
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ class KubernetesResourceQuotaHelper {
|
||||||
|
|
||||||
static formatBytes(bytes, decimals = 0, base10 = true) {
|
static formatBytes(bytes, decimals = 0, base10 = true) {
|
||||||
const res = {
|
const res = {
|
||||||
Size: 0,
|
size: 0,
|
||||||
SizeUnit: 'B',
|
sizeUnit: 'B',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
|
@ -22,8 +22,8 @@ class KubernetesResourceQuotaHelper {
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
|
size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)),
|
||||||
SizeUnit: sizes[i],
|
sizeUnit: sizes[i],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,20 +81,20 @@ export class KubernetesApplicationEnvironmentVariableFormValue {
|
||||||
* KubernetesApplicationPersistedFolderFormValue Model
|
* KubernetesApplicationPersistedFolderFormValue Model
|
||||||
*/
|
*/
|
||||||
const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({
|
const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({
|
||||||
PersistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit)
|
persistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit)
|
||||||
NeedsDeletion: false,
|
needsDeletion: false,
|
||||||
ContainerPath: '',
|
containerPath: '',
|
||||||
Size: '',
|
size: '',
|
||||||
SizeUnit: 'GB',
|
sizeUnit: 'GB',
|
||||||
StorageClass: {},
|
storageClass: {},
|
||||||
ExistingVolume: null,
|
existingVolume: null,
|
||||||
UseNewVolume: true,
|
useNewVolume: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationPersistedFolderFormValue {
|
export class KubernetesApplicationPersistedFolderFormValue {
|
||||||
constructor(storageClass) {
|
constructor(storageClass) {
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolderFormValue)));
|
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({
|
const _KubernetesApplicationPersistedFolder = Object.freeze({
|
||||||
MountPath: '',
|
MountPath: '',
|
||||||
PersistentVolumeClaimName: '',
|
persistentVolumeClaimName: '',
|
||||||
HostPath: '',
|
HostPath: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const _KubernetesPersistentVolumeClaim = Object.freeze({
|
||||||
PreviousName: '',
|
PreviousName: '',
|
||||||
Namespace: '',
|
Namespace: '',
|
||||||
Storage: 0,
|
Storage: 0,
|
||||||
StorageClass: {}, // KubernetesStorageClass
|
storageClass: {}, // KubernetesStorageClass
|
||||||
CreationDate: '',
|
CreationDate: '',
|
||||||
ApplicationOwner: '',
|
ApplicationOwner: '',
|
||||||
AccessModes: [],
|
AccessModes: [],
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessVie
|
||||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/ConfigureForm/StorageAccessModeSelector';
|
||||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||||
import { RegistriesSelector } from '@/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector';
|
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 { KubeServicesForm } from '@/react/kubernetes/applications/CreateView/application-services/KubeServicesForm';
|
||||||
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
|
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
|
||||||
import { KubeApplicationDeploymentTypeSelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationDeploymentTypeSelector';
|
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 { SecretsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/SecretsFormSection';
|
||||||
import { configurationsValidationSchema } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/configurationValidationSchema';
|
import { configurationsValidationSchema } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/configurationValidationSchema';
|
||||||
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
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';
|
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
|
@ -94,8 +96,8 @@ export const ngModule = angular
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(NodesDatatable))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(NodesDatatable))), [])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubeApplicationAccessPolicySelector',
|
'dataAccessPolicyFormSection',
|
||||||
r2a(KubeApplicationAccessPolicySelector, [
|
r2a(DataAccessPolicyFormSection, [
|
||||||
'value',
|
'value',
|
||||||
'onChange',
|
'onChange',
|
||||||
'isEdit',
|
'isEdit',
|
||||||
|
@ -205,3 +207,17 @@ withFormValidation(
|
||||||
['values', 'onChange', 'namespace'],
|
['values', 'onChange', 'namespace'],
|
||||||
configurationsValidationSchema
|
configurationsValidationSchema
|
||||||
);
|
);
|
||||||
|
|
||||||
|
withFormValidation(
|
||||||
|
ngModule,
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(PersistedFoldersFormSection))),
|
||||||
|
'persistedFoldersFormSection',
|
||||||
|
[
|
||||||
|
'isEdit',
|
||||||
|
'applicationValues',
|
||||||
|
'isAddPersistentFolderButtonShown',
|
||||||
|
'initialValues',
|
||||||
|
'availableVolumes',
|
||||||
|
],
|
||||||
|
persistedFoldersValidation
|
||||||
|
);
|
||||||
|
|
|
@ -410,250 +410,25 @@
|
||||||
></secrets-form-section>
|
></secrets-form-section>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region PERSISTED FOLDERS -->
|
<persisted-folders-form-section
|
||||||
<div class="form-group">
|
values="ctrl.formValues.PersistedFolders"
|
||||||
<div class="col-sm-12 vertical-center mb-2 pt-2.5" style="margin-top: 5px">
|
initial-values="ctrl.formValues.OriginalPersistedFolders"
|
||||||
<label class="control-label !pt-0 text-left !text-sm">Persisted folders</label>
|
on-change="(ctrl.onChangePersistedFolder)"
|
||||||
</div>
|
is-edit="ctrl.state.isEdit"
|
||||||
|
application-values="ctrl.formValues"
|
||||||
<div class="col-sm-12 small text-muted vertical-center mt-1" ng-if="!ctrl.storageClassAvailable()">
|
is-add-persistent-folder-button-shown="ctrl.isAddPersistentFolderButtonShown()"
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
available-volumes="ctrl.availableVolumes"
|
||||||
No storage option is available to persist data, contact your administrator to enable a storage option.
|
validation-data="{ namespaceQuotas: ctrl.formValues.ResourcePool.Quota, persistedFolders: ctrl.formValues.PersistedFolders, storageAvailabilities: ctrl.state.storages.availabilities }"
|
||||||
</div>
|
></persisted-folders-form-section>
|
||||||
|
|
||||||
<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 -->
|
|
||||||
|
|
||||||
<!-- #region DATA ACCESS POLICY -->
|
<!-- #region DATA ACCESS POLICY -->
|
||||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
<div ng-if="ctrl.showDataAccessPolicySection()">
|
||||||
<div class="form-group">
|
<data-access-policy-form-section
|
||||||
<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
|
|
||||||
value="ctrl.formValues.DataAccessPolicy"
|
value="ctrl.formValues.DataAccessPolicy"
|
||||||
on-change="(ctrl.onDataAccessPolicyChange)"
|
on-change="(ctrl.onDataAccessPolicyChange)"
|
||||||
is-edit="ctrl.state.isEdit"
|
is-edit="ctrl.state.isEdit"
|
||||||
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
||||||
></kube-application-access-policy-selector>
|
></data-access-policy-form-section>
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
|
|
@ -153,6 +153,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.onEnvironmentVariableChange = this.onEnvironmentVariableChange.bind(this);
|
this.onEnvironmentVariableChange = this.onEnvironmentVariableChange.bind(this);
|
||||||
this.onConfigMapsChange = this.onConfigMapsChange.bind(this);
|
this.onConfigMapsChange = this.onConfigMapsChange.bind(this);
|
||||||
this.onSecretsChange = this.onSecretsChange.bind(this);
|
this.onSecretsChange = this.onSecretsChange.bind(this);
|
||||||
|
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -312,21 +313,21 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
restorePersistedFolder(index) {
|
restorePersistedFolder(index) {
|
||||||
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
this.formValues.PersistedFolders[index].needsDeletion = false;
|
||||||
this.validatePersistedFolders();
|
this.validatePersistedFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPersistedFolders() {
|
resetPersistedFolders() {
|
||||||
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
persistedFolder.ExistingVolume = null;
|
persistedFolder.existingVolume = null;
|
||||||
persistedFolder.UseNewVolume = true;
|
persistedFolder.useNewVolume = true;
|
||||||
});
|
});
|
||||||
this.validatePersistedFolders();
|
this.validatePersistedFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
removePersistedFolder(index) {
|
removePersistedFolder(index) {
|
||||||
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
|
if (this.state.isEdit && this.formValues.PersistedFolders[index].persistentVolumeClaimName) {
|
||||||
this.formValues.PersistedFolders[index].NeedsDeletion = true;
|
this.formValues.PersistedFolders[index].needsDeletion = true;
|
||||||
} else {
|
} else {
|
||||||
this.formValues.PersistedFolders.splice(index, 1);
|
this.formValues.PersistedFolders.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
@ -334,15 +335,15 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
useNewVolume(index) {
|
useNewVolume(index) {
|
||||||
this.formValues.PersistedFolders[index].UseNewVolume = true;
|
this.formValues.PersistedFolders[index].useNewVolume = true;
|
||||||
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
this.formValues.PersistedFolders[index].existingVolume = null;
|
||||||
this.state.persistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
|
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
|
||||||
this.validatePersistedFolders();
|
this.validatePersistedFolders();
|
||||||
}
|
}
|
||||||
|
|
||||||
useExistingVolume(index) {
|
useExistingVolume(index) {
|
||||||
this.formValues.PersistedFolders[index].UseNewVolume = false;
|
this.formValues.PersistedFolders[index].useNewVolume = false;
|
||||||
this.state.persistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
|
this.state.persistedFoldersUseExistingVolumes = _.some(this.formValues.PersistedFolders, { useNewVolume: false });
|
||||||
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
|
||||||
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
|
||||||
this.resetDeploymentType();
|
this.resetDeploymentType();
|
||||||
|
@ -360,22 +361,26 @@ class KubernetesCreateApplicationController {
|
||||||
onChangePersistedFolderPath() {
|
onChangePersistedFolderPath() {
|
||||||
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
|
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
|
||||||
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
if (persistedFolder.NeedsDeletion) {
|
if (persistedFolder.needsDeletion) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return persistedFolder.ContainerPath;
|
return persistedFolder.containerPath;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangePersistedFolder(values) {
|
||||||
|
this.formValues.PersistedFolders = values;
|
||||||
|
}
|
||||||
|
|
||||||
onChangeExistingVolumeSelection() {
|
onChangeExistingVolumeSelection() {
|
||||||
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
|
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
|
||||||
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
if (persistedFolder.NeedsDeletion) {
|
if (persistedFolder.needsDeletion) {
|
||||||
return undefined;
|
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;
|
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++) {
|
for (let i = 0; i < this.formValues.PersistedFolders.length; i++) {
|
||||||
const folder = this.formValues.PersistedFolders[i];
|
const folder = this.formValues.PersistedFolders[i];
|
||||||
|
|
||||||
if (folder.StorageClass && _.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
|
if (folder.storageClass && _.isEqual(folder.storageClass.AccessModes, ['RWO'])) {
|
||||||
storageOptions.push(folder.StorageClass.Name);
|
storageOptions.push(folder.storageClass.Name);
|
||||||
} else {
|
} else {
|
||||||
storageOptions.push('<no storage option available>');
|
storageOptions.push('<no storage option available>');
|
||||||
}
|
}
|
||||||
|
@ -612,7 +617,7 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
/* #region PERSISTED FOLDERS */
|
/* #region PERSISTED FOLDERS */
|
||||||
/* #region BUTTONS STATES */
|
/* #region BUTTONS STATES */
|
||||||
isAddPersistentFolderButtonShowed() {
|
isAddPersistentFolderButtonShown() {
|
||||||
return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1;
|
return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,7 +635,7 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditAndExistingPersistedFolder(index) {
|
isEditAndExistingPersistedFolder(index) {
|
||||||
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
return this.state.isEdit && this.formValues.PersistedFolders[index].persistentVolumeClaimName;
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -781,7 +786,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.volumes = volumes;
|
this.volumes = volumes;
|
||||||
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
||||||
const isUnused = !KubernetesVolumeHelper.isUsed(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;
|
return isUnused || isRWX;
|
||||||
});
|
});
|
||||||
this.availableVolumes = filteredVolumes;
|
this.availableVolumes = filteredVolumes;
|
||||||
|
@ -873,7 +878,11 @@ class KubernetesCreateApplicationController {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues, false, this.originalServicePorts);
|
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues, false, this.originalServicePorts);
|
||||||
this.Notifications.success('Success', 'Request to update application successfully submitted');
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to update application');
|
this.Notifications.error('Failure', err, 'Unable to update application');
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1087,13 +1096,14 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
|
||||||
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
_.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) {
|
if (volume) {
|
||||||
persistedFolder.UseNewVolume = false;
|
persistedFolder.useNewVolume = false;
|
||||||
persistedFolder.ExistingVolume = volume;
|
persistedFolder.existingVolume = volume;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.formValues.OriginalPersistedFolders = this.formValues.PersistedFolders;
|
||||||
await this.refreshNamespaceData(namespace);
|
await this.refreshNamespaceData(namespace);
|
||||||
} else {
|
} else {
|
||||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Name }}</td>
|
<td>{{ item.Name }}</td>
|
||||||
<td>{{ item.Size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
dir-paginate-end
|
dir-paginate-end
|
||||||
|
|
|
@ -81,9 +81,9 @@
|
||||||
<table-column-header
|
<table-column-header
|
||||||
col-title="'Usage'"
|
col-title="'Usage'"
|
||||||
can-sort="true"
|
can-sort="true"
|
||||||
is-sorted="$ctrl.state.orderBy === 'Size'"
|
is-sorted="$ctrl.state.orderBy === 'size'"
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"
|
is-sorted-desc="$ctrl.state.orderBy === 'size' && $ctrl.state.reverseOrder"
|
||||||
ng-click="$ctrl.changeOrderBy('Size')"
|
ng-click="$ctrl.changeOrderBy('size')"
|
||||||
></table-column-header>
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Name }}</td>
|
<td>{{ item.Name }}</td>
|
||||||
<td>{{ item.Size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
dir-paginate-end
|
dir-paginate-end
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Storage Class</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Access Modes</td>
|
<td>Access Modes</td>
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Provisioner</td>
|
<td>Provisioner</td>
|
||||||
<td data-cy="k8sVolDetail-volProvisioner">{{
|
<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>
|
}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -77,14 +77,14 @@
|
||||||
<td data-cy="k8sVolDetail-volCreatedAt">{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
<td data-cy="k8sVolDetail-volCreatedAt">{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Size</td>
|
<td>size</td>
|
||||||
<td ng-if="!ctrl.state.increaseSize">
|
<td ng-if="!ctrl.state.increaseSize">
|
||||||
{{ ctrl.volume.PersistentVolumeClaim.Storage }}
|
{{ ctrl.volume.PersistentVolumeClaim.Storage }}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
ng-click="ctrl.state.increaseSize = true"
|
ng-click="ctrl.state.increaseSize = true"
|
||||||
ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass.AllowVolumeExpansion"
|
ng-if="ctrl.volume.PersistentVolumeClaim.storageClass.AllowVolumeExpansion"
|
||||||
data-cy="k8sVolDetail-increaseSizeButton"
|
data-cy="k8sVolDetail-increaseSizeButton"
|
||||||
>Increase size</button
|
>Increase size</button
|
||||||
>
|
>
|
||||||
|
|
|
@ -186,12 +186,14 @@ class KubernetesVolumeController {
|
||||||
try {
|
try {
|
||||||
await this.getVolume();
|
await this.getVolume();
|
||||||
await this.getEvents();
|
await this.getEvents();
|
||||||
|
if (this.volume.PersistentVolumeClaim.storageClass !== undefined) {
|
||||||
this.state.volumeSharedAccessPolicies = this.volume.PersistentVolumeClaim.AccessModes;
|
this.state.volumeSharedAccessPolicies = this.volume.PersistentVolumeClaim.AccessModes;
|
||||||
let policies = KubernetesStorageClassAccessPolicies();
|
let policies = KubernetesStorageClassAccessPolicies();
|
||||||
this.state.volumeSharedAccessPolicyTooltips = this.state.volumeSharedAccessPolicies.map((policy) => {
|
this.state.volumeSharedAccessPolicyTooltips = this.state.volumeSharedAccessPolicies.map((policy) => {
|
||||||
const matchingPolicy = policies.find((p) => p.Name === policy);
|
const matchingPolicy = policies.find((p) => p.Name === policy);
|
||||||
return matchingPolicy ? matchingPolicy.Description : undefined;
|
return matchingPolicy ? matchingPolicy.Description : undefined;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -7,9 +7,9 @@ import { confirmDelete } from '@@/modals/confirm';
|
||||||
|
|
||||||
function buildStorages(storages, volumes) {
|
function buildStorages(storages, volumes) {
|
||||||
_.forEach(storages, (s) => {
|
_.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.Volumes = filteredVolumes;
|
||||||
s.Size = computeSize(filteredVolumes);
|
s.size = computeSize(filteredVolumes);
|
||||||
});
|
});
|
||||||
return storages;
|
return storages;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ function buildStorages(storages, volumes) {
|
||||||
function computeSize(volumes) {
|
function computeSize(volumes) {
|
||||||
const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 }));
|
const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 }));
|
||||||
const format = KubernetesResourceQuotaHelper.formatBytes(size);
|
const format = KubernetesResourceQuotaHelper.formatBytes(size);
|
||||||
return `${format.Size}${format.SizeUnit}`;
|
return `${format.size}${format.sizeUnit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
class KubernetesVolumesController {
|
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) {
|
if (changes.values) {
|
||||||
await this.runValidation(changes.values.currentValue);
|
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> {
|
export interface Option<T> {
|
||||||
value: T;
|
value: T;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
|
@ -43,7 +44,7 @@ export function ButtonSelector<T extends string | number | boolean>({
|
||||||
key={option.value.toString()}
|
key={option.value.toString()}
|
||||||
selected={value === option.value}
|
selected={value === option.value}
|
||||||
onChange={() => onChange(option.value)}
|
onChange={() => onChange(option.value)}
|
||||||
disabled={disabled}
|
disabled={disabled || option.disabled}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
>
|
>
|
||||||
{option.label || option.value.toString()}
|
{option.label || option.value.toString()}
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface Props {
|
||||||
titleSize?: 'sm' | 'md' | 'lg';
|
titleSize?: 'sm' | 'md' | 'lg';
|
||||||
isFoldable?: boolean;
|
isFoldable?: boolean;
|
||||||
defaultFolded?: boolean;
|
defaultFolded?: boolean;
|
||||||
|
titleClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormSection({
|
export function FormSection({
|
||||||
|
@ -18,6 +19,7 @@ export function FormSection({
|
||||||
children,
|
children,
|
||||||
isFoldable = false,
|
isFoldable = false,
|
||||||
defaultFolded = isFoldable,
|
defaultFolded = isFoldable,
|
||||||
|
titleClassName,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
const [isExpanded, setIsExpanded] = useState(!defaultFolded);
|
||||||
|
|
||||||
|
@ -26,6 +28,7 @@ export function FormSection({
|
||||||
<FormSectionTitle
|
<FormSectionTitle
|
||||||
htmlFor={isFoldable ? `foldingButton${title}` : ''}
|
htmlFor={isFoldable ? `foldingButton${title}` : ''}
|
||||||
titleSize={titleSize}
|
titleSize={titleSize}
|
||||||
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
{isFoldable && (
|
{isFoldable && (
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { PropsWithChildren } from 'react';
|
||||||
interface Props {
|
interface Props {
|
||||||
htmlFor?: string;
|
htmlFor?: string;
|
||||||
titleSize?: 'sm' | 'md' | 'lg';
|
titleSize?: 'sm' | 'md' | 'lg';
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tailwindTitleSize = {
|
const tailwindTitleSize = {
|
||||||
|
@ -16,6 +17,7 @@ export function FormSectionTitle({
|
||||||
children,
|
children,
|
||||||
htmlFor,
|
htmlFor,
|
||||||
titleSize = 'md',
|
titleSize = 'md',
|
||||||
|
className,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
if (htmlFor) {
|
if (htmlFor) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +25,8 @@ export function FormSectionTitle({
|
||||||
htmlFor={htmlFor}
|
htmlFor={htmlFor}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'col-sm-12 mb-2 mt-1 flex cursor-pointer items-center pl-0 font-medium',
|
'col-sm-12 mb-2 mt-1 flex cursor-pointer items-center pl-0 font-medium',
|
||||||
tailwindTitleSize[titleSize]
|
tailwindTitleSize[titleSize],
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -34,7 +37,8 @@ export function FormSectionTitle({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'col-sm-12 mb-2 mt-4 pl-0 font-medium',
|
'col-sm-12 mb-2 mt-4 pl-0 font-medium',
|
||||||
tailwindTitleSize[titleSize]
|
tailwindTitleSize[titleSize],
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -6,11 +6,13 @@ import { useInputGroupContext } from './InputGroup';
|
||||||
type BaseProps<TProps> = {
|
type BaseProps<TProps> = {
|
||||||
as?: ComponentType<TProps> | string;
|
as?: ComponentType<TProps> | string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InputGroupAddon<TProps>({
|
export function InputGroupAddon<TProps>({
|
||||||
children,
|
children,
|
||||||
as = 'span',
|
as = 'span',
|
||||||
|
className,
|
||||||
required,
|
required,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<BaseProps<TProps> & TProps>) {
|
}: PropsWithChildren<BaseProps<TProps> & TProps>) {
|
||||||
|
@ -19,7 +21,7 @@ export function InputGroupAddon<TProps>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<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
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,7 +11,7 @@ interface Props {
|
||||||
onChange(value: number): void;
|
onChange(value: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KubeApplicationAccessPolicySelector({
|
export function DataAccessPolicyFormSection({
|
||||||
isEdit,
|
isEdit,
|
||||||
persistedFoldersUseExistingVolumes,
|
persistedFoldersUseExistingVolumes,
|
||||||
value,
|
value,
|
|
@ -108,7 +108,7 @@ const queryKeys = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// useQuery to get a list of all applications from an array of namespaces
|
// useQuery to get a list of all applications from an array of namespaces
|
||||||
export function useApplicationsForCluster(
|
export function useApplicationsQuery(
|
||||||
environemtId: EnvironmentId,
|
environemtId: EnvironmentId,
|
||||||
namespaces?: string[]
|
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 { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||||
import { RawExtension } from 'kubernetes-types/runtime';
|
import { RawExtension } from 'kubernetes-types/runtime';
|
||||||
|
|
||||||
|
export type ApplicationFormValues = {
|
||||||
|
Containers: Array<unknown>;
|
||||||
|
ApplicationType: number; // KubernetesApplicationTypes
|
||||||
|
};
|
||||||
|
|
||||||
export type Application = Deployment | DaemonSet | StatefulSet | Pod;
|
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.
|
// 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 { useIsRBACEnabledQuery } from '../../getIsRBACEnabled';
|
||||||
import { getIngressClassesFormValues } from '../../ingressClass/IngressClassDatatable/utils';
|
import { getIngressClassesFormValues } from '../../ingressClass/IngressClassDatatable/utils';
|
||||||
|
|
||||||
import { useStorageClassesFormValues } from './useStorageClassesFormValues';
|
import { useStorageClassesFormValues } from './useStorageClasses';
|
||||||
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
||||||
import { configureValidationSchema } from './validation';
|
import { configureValidationSchema } from './validation';
|
||||||
import { RBACAlert } from './RBACAlert';
|
import { RBACAlert } from './RBACAlert';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||||
|
|
||||||
import { StorageAccessModeSelector } from './StorageAccessModeSelector';
|
import { StorageAccessModeSelector } from './StorageAccessModeSelector';
|
||||||
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
import { ConfigureFormValues, StorageClassFormValues } from './types';
|
||||||
import { availableStorageClassPolicies } from './useStorageClassesFormValues';
|
import { availableStorageClassPolicies } from './useStorageClasses';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
storageClassValues: StorageClassFormValues[];
|
storageClassValues: StorageClassFormValues[];
|
||||||
|
|
|
@ -26,9 +26,31 @@ export const availableStorageClassPolicies = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function useStorageClassesFormValues(
|
export function useStorageClasses(environment?: Environment | null) {
|
||||||
environment: Environment | null | undefined
|
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(
|
return useQuery(
|
||||||
[
|
[
|
||||||
'environments',
|
'environments',
|
|
@ -8,7 +8,7 @@ import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultD
|
||||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
||||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
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 { Application } from '@/react/kubernetes/applications/types';
|
||||||
import { pluralize } from '@/portainer/helpers/strings';
|
import { pluralize } from '@/portainer/helpers/strings';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
|
@ -54,8 +54,10 @@ export function ConfigMapsDatatable() {
|
||||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { data: applications, ...applicationsQuery } =
|
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||||
useApplicationsForCluster(environmentId, namespaceNames);
|
environmentId,
|
||||||
|
namespaceNames
|
||||||
|
);
|
||||||
|
|
||||||
const filteredConfigMaps = useMemo(
|
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 { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
import { isSystemNamespace } from '@/react/kubernetes/namespaces/utils';
|
||||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
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 { Application } from '@/react/kubernetes/applications/types';
|
||||||
import { pluralize } from '@/portainer/helpers/strings';
|
import { pluralize } from '@/portainer/helpers/strings';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
|
@ -54,8 +54,10 @@ export function SecretsDatatable() {
|
||||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { data: applications, ...applicationsQuery } =
|
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||||
useApplicationsForCluster(environmentId, namespaceNames);
|
environmentId,
|
||||||
|
namespaceNames
|
||||||
|
);
|
||||||
|
|
||||||
const filteredSecrets = useMemo(
|
const filteredSecrets = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { DashboardGrid } from '@@/DashboardItem/DashboardGrid';
|
||||||
import { DashboardItem } from '@@/DashboardItem/DashboardItem';
|
import { DashboardItem } from '@@/DashboardItem/DashboardItem';
|
||||||
import { PageHeader } from '@@/PageHeader';
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
import { useApplicationsForCluster } from '../applications/application.queries';
|
import { useApplicationsQuery } from '../applications/application.queries';
|
||||||
import { usePVCsForCluster } from '../volumes/queries';
|
import { usePVCsQuery } from '../volumes/usePVCsQuery';
|
||||||
import { useServicesForCluster } from '../services/service';
|
import { useServicesForCluster } from '../services/service';
|
||||||
import { useIngresses } from '../ingresses/queries';
|
import { useIngresses } from '../ingresses/queries';
|
||||||
import { useConfigMapsForCluster } from '../configs/configmap.service';
|
import { useConfigMapsForCluster } from '../configs/configmap.service';
|
||||||
|
@ -24,9 +24,11 @@ export function DashboardView() {
|
||||||
const { data: namespaces, ...namespacesQuery } =
|
const { data: namespaces, ...namespacesQuery } =
|
||||||
useNamespacesQuery(environmentId);
|
useNamespacesQuery(environmentId);
|
||||||
const namespaceNames = namespaces && Object.keys(namespaces);
|
const namespaceNames = namespaces && Object.keys(namespaces);
|
||||||
const { data: applications, ...applicationsQuery } =
|
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||||
useApplicationsForCluster(environmentId, namespaceNames);
|
environmentId,
|
||||||
const { data: pvcs, ...pvcsQuery } = usePVCsForCluster(
|
namespaceNames
|
||||||
|
);
|
||||||
|
const { data: pvcs, ...pvcsQuery } = usePVCsQuery(
|
||||||
environmentId,
|
environmentId,
|
||||||
namespaceNames
|
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 { PersistentVolumeClaimList } from 'kubernetes-types/core/v1';
|
||||||
|
|
||||||
import axios from '@/portainer/services/axios';
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
import axios from '@/portainer/services/axios';
|
||||||
|
|
||||||
import { parseKubernetesAxiosError } from '../axiosError';
|
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,
|
environmentId: EnvironmentId,
|
||||||
namespaces: string[]
|
namespaces?: string[]
|
||||||
) {
|
) {
|
||||||
|
return useQuery(
|
||||||
|
['environments', environmentId, 'kubernetes', 'pvcs'],
|
||||||
|
async () => {
|
||||||
|
if (!namespaces) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const pvcs = await Promise.all(
|
const pvcs = await Promise.all(
|
||||||
namespaces.map((namespace) => getPVCs(environmentId, namespace))
|
namespaces?.map((namespace) => getPVCs(environmentId, namespace))
|
||||||
);
|
);
|
||||||
return pvcs.flat();
|
return pvcs.flat();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...withError('Unable to retrieve perrsistent volume claims'),
|
||||||
|
enabled: !!namespaces,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all persistent volume claims for a namespace
|
// get all persistent volume claims for a namespace
|
Loading…
Reference in New Issue