mirror of https://github.com/portainer/portainer
refactor(app): migrate replicas form section [EE-6238] (#10705)
Co-authored-by: testa113 <testa113>pull/10709/head
parent
58da51f767
commit
6da71661d5
|
@ -34,6 +34,10 @@ import {
|
||||||
ResourceReservationFormSection,
|
ResourceReservationFormSection,
|
||||||
resourceReservationValidation,
|
resourceReservationValidation,
|
||||||
} from '@/react/kubernetes/applications/components/ResourceReservationFormSection';
|
} from '@/react/kubernetes/applications/components/ResourceReservationFormSection';
|
||||||
|
import {
|
||||||
|
ReplicationFormSection,
|
||||||
|
replicationValidation,
|
||||||
|
} from '@/react/kubernetes/applications/components/ReplicationFormSection';
|
||||||
|
|
||||||
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
|
@ -238,3 +242,16 @@ withFormValidation(
|
||||||
],
|
],
|
||||||
resourceReservationValidation
|
resourceReservationValidation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
withFormValidation(
|
||||||
|
ngModule,
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(ReplicationFormSection))),
|
||||||
|
'replicationFormSection',
|
||||||
|
[
|
||||||
|
'supportScalableReplicaDeployment',
|
||||||
|
'cpuLimit',
|
||||||
|
'memoryLimit',
|
||||||
|
'resourceReservationsOverflow',
|
||||||
|
],
|
||||||
|
replicationValidation
|
||||||
|
);
|
||||||
|
|
|
@ -451,71 +451,17 @@
|
||||||
></app-deployment-type-form-section>
|
></app-deployment-type-form-section>
|
||||||
|
|
||||||
<!-- replica count -->
|
<!-- replica count -->
|
||||||
<div class="form-group" ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
||||||
<label for="replica_count" class="col-sm-3 col-lg-2 control-label required text-left">Instance count </label>
|
<replication-form-section
|
||||||
<div class="col-sm-2">
|
values="{replicaCount: ctrl.formValues.ReplicaCount}"
|
||||||
<input
|
on-change="(ctrl.onChangeReplicaCount)"
|
||||||
type="number"
|
support-scalable-replica-deployment="ctrl.supportScalableReplicaDeployment()"
|
||||||
name="replica_count"
|
memory-limit="ctrl.formValues.MemoryLimit"
|
||||||
class="form-control"
|
cpu-limit="ctrl.formValues.CpuLimit"
|
||||||
min="1"
|
resource-reservations-overflow="ctrl.resourceReservationsOverflow()"
|
||||||
max="9999"
|
non-scalable-storage="ctrl.getNonScalableStorage()"
|
||||||
placeholder="1"
|
validation-data="{resourceReservationsOverflow: ctrl.resourceReservationsOverflow(), quotaExceeded: ctrl.state.storages.quotaExceeded, nonScalableStorage: ctrl.getNonScalableStorage(), supportScalableReplicaDeployment: ctrl.supportScalableReplicaDeployment()}"
|
||||||
ng-model="ctrl.formValues.ReplicaCount"
|
></replication-form-section>
|
||||||
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
|
|
||||||
ng-change="ctrl.enforceReplicaCountMinimum()"
|
|
||||||
required
|
|
||||||
data-cy="k8sAppCreate-replicaCountInput"
|
|
||||||
/>
|
|
||||||
<div class="help-block" ng-if="kubernetesApplicationCreationForm['replica_count'].$invalid">
|
|
||||||
<div class="small text-warning whitespace-nowrap">
|
|
||||||
<ng-messages for="kubernetesApplicationCreationForm['replica_count'].$error">
|
|
||||||
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Instance count is required.</p>
|
|
||||||
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Instance count must be greater than 0.</p>
|
|
||||||
</ng-messages>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !replica count -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="form-group"
|
|
||||||
ng-if="!ctrl.resourceReservationsOverflow() && ctrl.formValues.ReplicaCount > 1 && (ctrl.formValues.CpuLimit !== 0 || ctrl.formValues.MemoryLimit !== 0)"
|
|
||||||
>
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
|
||||||
<div>
|
|
||||||
This application will reserve the following resources:
|
|
||||||
<b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
|
|
||||||
<b>{{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB</b> of memory.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
|
|
||||||
This application would exceed available resources. Please review resource reservations or the instance count.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.state.storages.quotaExceeded">
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
|
||||||
This application would exceed available storage. Please review the persisted folders or the instance count.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="!ctrl.supportScalableReplicaDeployment()">
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
|
||||||
<div>
|
|
||||||
The following storage option(s) do not support concurrent access from multiples instances:
|
|
||||||
<code>{{ ctrl.getNonScalableStorage() }}</code
|
|
||||||
>. You will not be able to scale that application.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.onSecretsChange = this.onSecretsChange.bind(this);
|
this.onSecretsChange = this.onSecretsChange.bind(this);
|
||||||
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
|
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
|
||||||
this.onChangeResourceReservation = this.onChangeResourceReservation.bind(this);
|
this.onChangeResourceReservation = this.onChangeResourceReservation.bind(this);
|
||||||
|
this.onChangeReplicaCount = this.onChangeReplicaCount.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -517,6 +518,12 @@ class KubernetesCreateApplicationController {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeReplicaCount(values) {
|
||||||
|
return this.$async(async () => {
|
||||||
|
this.formValues.ReplicaCount = values.replicaCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO)
|
// For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO)
|
||||||
getNonScalableStorage() {
|
getNonScalableStorage() {
|
||||||
let storageOptions = [];
|
let storageOptions = [];
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function toViewModelMemory(value = 0): number {
|
||||||
return value / 1024 / 1024;
|
return value / 1024 / 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
function round(value: number, decimals: number) {
|
export function round(value: number, decimals: number) {
|
||||||
const tenth = 10 ** decimals;
|
const tenth = 10 ** decimals;
|
||||||
return Math.round((value + Number.EPSILON) * tenth) / tenth;
|
return Math.round((value + Number.EPSILON) * tenth) / tenth;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { FormikErrors } from 'formik';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { Input } from '@@/form-components/Input';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
|
import { ReplicaCountFormValues } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
values: ReplicaCountFormValues;
|
||||||
|
onChange: (values: ReplicaCountFormValues) => void;
|
||||||
|
errors: FormikErrors<ReplicaCountFormValues>;
|
||||||
|
cpuLimit: number;
|
||||||
|
memoryLimit: number;
|
||||||
|
resourceReservationsOverflow: boolean;
|
||||||
|
supportScalableReplicaDeployment: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ReplicationFormSection({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
errors,
|
||||||
|
supportScalableReplicaDeployment,
|
||||||
|
cpuLimit,
|
||||||
|
memoryLimit,
|
||||||
|
resourceReservationsOverflow,
|
||||||
|
}: Props) {
|
||||||
|
const hasResourceLimit = cpuLimit !== 0 || memoryLimit !== 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormControl label="Instance count" required>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="9999"
|
||||||
|
value={values.replicaCount}
|
||||||
|
disabled={!supportScalableReplicaDeployment}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ replicaCount: e.target.valueAsNumber || 1 })
|
||||||
|
}
|
||||||
|
className="w-1/4"
|
||||||
|
data-cy="k8sAppCreate-replicaCountInput"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{!resourceReservationsOverflow &&
|
||||||
|
values.replicaCount > 1 &&
|
||||||
|
hasResourceLimit && (
|
||||||
|
<TextTip color="blue">
|
||||||
|
This application will reserve the following resources:{' '}
|
||||||
|
<b>{round(cpuLimit * values.replicaCount, 2)} CPU</b> and{' '}
|
||||||
|
<b>{memoryLimit * values.replicaCount} MB</b> of memory.
|
||||||
|
</TextTip>
|
||||||
|
)}
|
||||||
|
{errors?.replicaCount && <FormError>{errors.replicaCount}</FormError>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { ReplicationFormSection } from './ReplicationFormSection';
|
||||||
|
export { replicationValidation } from './replicationValidation';
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { SchemaOf, number, object } from 'yup';
|
||||||
|
|
||||||
|
import { ReplicaCountFormValues } from './types';
|
||||||
|
|
||||||
|
type ValidationData = {
|
||||||
|
resourceReservationsOverflow: boolean;
|
||||||
|
quotaExceeded: boolean;
|
||||||
|
nonScalableStorage: string;
|
||||||
|
supportScalableReplicaDeployment: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function replicationValidation(
|
||||||
|
validationData?: ValidationData
|
||||||
|
): SchemaOf<ReplicaCountFormValues> {
|
||||||
|
const {
|
||||||
|
resourceReservationsOverflow,
|
||||||
|
quotaExceeded,
|
||||||
|
nonScalableStorage,
|
||||||
|
supportScalableReplicaDeployment,
|
||||||
|
} = validationData || {};
|
||||||
|
return object({
|
||||||
|
replicaCount: number()
|
||||||
|
.min(1, 'Instance count must be greater than 0.')
|
||||||
|
.test(
|
||||||
|
'overflow',
|
||||||
|
'This application would exceed available resources. Please review resource reservations or the instance count.',
|
||||||
|
() => !resourceReservationsOverflow // must not have resource reservations overflow
|
||||||
|
)
|
||||||
|
.test(
|
||||||
|
'quota',
|
||||||
|
'This application would exceed available storage. Please review the persisted folders or the instance count.',
|
||||||
|
() => !quotaExceeded // must not have quota exceeded
|
||||||
|
)
|
||||||
|
.test(
|
||||||
|
'scalable',
|
||||||
|
`The following storage option(s) do not support concurrent access from multiples instances: ${nonScalableStorage}. You will not be able to scale that application.`,
|
||||||
|
() => !!supportScalableReplicaDeployment // must have support scalable replica deployment
|
||||||
|
)
|
||||||
|
.required('Instance count is required.'),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type ReplicaCountFormValues = {
|
||||||
|
replicaCount: number;
|
||||||
|
};
|
Loading…
Reference in New Issue