@spinnaker/google 2026.3.2 → 2026.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
- package/dist/domain/loadBalancer.d.ts +6 -3
- package/dist/image/ImageSelect.d.ts +1 -1
- package/dist/index.js +159 -1
- package/dist/index.js.map +1 -1
- package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
- package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
- package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
- package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
- package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
- package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
- package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
- package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
- package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
- package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
- package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
- package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
- package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
- package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
- package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
- package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
- package/package.json +5 -5
- package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
- package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
- package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
- package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
- package/src/domain/loadBalancer.ts +6 -3
- package/src/gce.module.spec.ts +132 -153
- package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
- package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
- package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
- package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
- package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
- package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
- package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
- package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
- package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
- package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
- package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
- package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
- package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
- package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
- package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
- package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
- package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
- package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
- package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
- package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
- package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
- package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
- package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
- package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
- package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
- package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
- package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
- package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
- package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
- package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
- package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
- package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
- package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
- package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
- package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
- package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
- package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
- package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
- package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
- package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
- package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
- package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
- package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
- package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
- package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
- package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
- package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
- package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
- package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
- package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
- package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
- package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
- package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
- package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
- package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
- package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
- package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
- package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
- package/src/serverGroup/serverGroup.transformer.js +1 -0
- package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
- package/src/validation/ApplicationNameValidator.spec.ts +1 -1
- /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
|
@@ -12,6 +12,27 @@ import { GCEProviderSettings } from '../../../../gce.settings';
|
|
|
12
12
|
const MAX_INT = 2147483647;
|
|
13
13
|
const MAX_SCHEDULE_DURATION_SECONDS = 14 * 24 * 60 * 60;
|
|
14
14
|
|
|
15
|
+
interface IAutoscalingPolicyValidationErrors {
|
|
16
|
+
coolDownPeriodSec?: string;
|
|
17
|
+
maxNumReplicas?: string;
|
|
18
|
+
metric?: string;
|
|
19
|
+
minNumReplicas?: string;
|
|
20
|
+
predictiveAutoscaling?: string;
|
|
21
|
+
scaleInControl?: string;
|
|
22
|
+
scalingSchedules?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IAutoHealingPolicyValidationErrors {
|
|
26
|
+
healthCheck?: string;
|
|
27
|
+
healthCheckKind?: string;
|
|
28
|
+
initialDelaySec?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IPoliciesValidationErrors {
|
|
32
|
+
autoHealingPolicy?: IAutoHealingPolicyValidationErrors;
|
|
33
|
+
autoscalingPolicy?: IAutoscalingPolicyValidationErrors;
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
function isIntegerInRange(value: unknown, minimum: number, maximum = MAX_INT): value is number {
|
|
16
37
|
return (
|
|
17
38
|
typeof value === 'number' &&
|
|
@@ -92,61 +113,8 @@ function hasValidScaleInControl(policy: IGceAutoscalingPolicy): boolean {
|
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
export class Policies extends GceServerGroupWizardPage {
|
|
95
|
-
public validate(values: IGceServerGroupCommand):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (values.autoscalingPolicy) {
|
|
99
|
-
const policy = values.autoscalingPolicy as IGceAutoscalingPolicy;
|
|
100
|
-
const policyErrors: { [key: string]: string } = {};
|
|
101
|
-
const validMinimum = isIntegerInRange(policy.minNumReplicas, 0);
|
|
102
|
-
const validMaximum = isIntegerInRange(policy.maxNumReplicas, 0);
|
|
103
|
-
if (!validMinimum) {
|
|
104
|
-
policyErrors.minNumReplicas = 'Minimum capacity must be a nonnegative integer.';
|
|
105
|
-
}
|
|
106
|
-
if (!validMaximum) {
|
|
107
|
-
policyErrors.maxNumReplicas = 'Maximum capacity must be a nonnegative integer.';
|
|
108
|
-
} else if (validMinimum && policy.maxNumReplicas < policy.minNumReplicas) {
|
|
109
|
-
policyErrors.maxNumReplicas = 'Maximum capacity must be at least the minimum capacity.';
|
|
110
|
-
}
|
|
111
|
-
if (!isIntegerInRange(policy.coolDownPeriodSec, 15)) {
|
|
112
|
-
policyErrors.coolDownPeriodSec = 'Cool-down period must be an integer of at least 15 seconds.';
|
|
113
|
-
}
|
|
114
|
-
if (!hasValidMetrics(policy)) {
|
|
115
|
-
policyErrors.metric = 'At least one complete autoscaling metric required.';
|
|
116
|
-
}
|
|
117
|
-
if (!hasValidSchedules(policy)) {
|
|
118
|
-
policyErrors.scalingSchedules = 'Every scaling schedule must be complete and within supported bounds.';
|
|
119
|
-
}
|
|
120
|
-
if (!hasValidScaleInControl(policy)) {
|
|
121
|
-
policyErrors.scaleInControl = 'Scale-in control values are outside supported bounds.';
|
|
122
|
-
}
|
|
123
|
-
const predictiveEnabled = policy.cpuUtilization?.predictiveMethod === GcePredictiveMethod.STANDARD;
|
|
124
|
-
if (predictiveEnabled && !GCEProviderSettings.feature.predictiveAutoscaling) {
|
|
125
|
-
policyErrors.predictiveAutoscaling = 'Predictive autoscaling is not enabled.';
|
|
126
|
-
}
|
|
127
|
-
if (Object.keys(policyErrors).length) {
|
|
128
|
-
errors.autoscalingPolicy = policyErrors;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (values.enableAutoHealing) {
|
|
133
|
-
const policy = (values.autoHealingPolicy || {}) as IGceAutoHealingPolicy;
|
|
134
|
-
const policyErrors: { [key: string]: string } = {};
|
|
135
|
-
if (!policy.healthCheck?.trim()) {
|
|
136
|
-
policyErrors.healthCheck = 'Health check required.';
|
|
137
|
-
}
|
|
138
|
-
if (!policy.healthCheckKind) {
|
|
139
|
-
policyErrors.healthCheckKind = 'Health check kind required.';
|
|
140
|
-
}
|
|
141
|
-
if (!isIntegerInRange(policy.initialDelaySec, 0)) {
|
|
142
|
-
policyErrors.initialDelaySec = 'Initial delay must be an integer between 0 and 2147483647 seconds.';
|
|
143
|
-
}
|
|
144
|
-
if (Object.keys(policyErrors).length) {
|
|
145
|
-
errors.autoHealingPolicy = policyErrors;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return errors;
|
|
116
|
+
public validate(values: IGceServerGroupCommand): IPoliciesValidationErrors {
|
|
117
|
+
return validateGceServerGroupPolicies(values);
|
|
150
118
|
}
|
|
151
119
|
|
|
152
120
|
private setField = (field: string, value: any): void => {
|
|
@@ -285,6 +253,63 @@ export class Policies extends GceServerGroupWizardPage {
|
|
|
285
253
|
}
|
|
286
254
|
}
|
|
287
255
|
|
|
256
|
+
export function validateGceServerGroupPolicies(values: IGceServerGroupCommand): IPoliciesValidationErrors {
|
|
257
|
+
const errors: IPoliciesValidationErrors = {};
|
|
258
|
+
|
|
259
|
+
if (values.autoscalingPolicy) {
|
|
260
|
+
const policy = values.autoscalingPolicy as IGceAutoscalingPolicy;
|
|
261
|
+
const policyErrors: IAutoscalingPolicyValidationErrors = {};
|
|
262
|
+
const validMinimum = isIntegerInRange(policy.minNumReplicas, 0);
|
|
263
|
+
const validMaximum = isIntegerInRange(policy.maxNumReplicas, 0);
|
|
264
|
+
if (!validMinimum) {
|
|
265
|
+
policyErrors.minNumReplicas = 'Minimum capacity must be a nonnegative integer.';
|
|
266
|
+
}
|
|
267
|
+
if (!validMaximum) {
|
|
268
|
+
policyErrors.maxNumReplicas = 'Maximum capacity must be a nonnegative integer.';
|
|
269
|
+
} else if (validMinimum && policy.maxNumReplicas < policy.minNumReplicas) {
|
|
270
|
+
policyErrors.maxNumReplicas = 'Maximum capacity must be at least the minimum capacity.';
|
|
271
|
+
}
|
|
272
|
+
if (!isIntegerInRange(policy.coolDownPeriodSec, 15)) {
|
|
273
|
+
policyErrors.coolDownPeriodSec = 'Cool-down period must be an integer of at least 15 seconds.';
|
|
274
|
+
}
|
|
275
|
+
if (!hasValidMetrics(policy)) {
|
|
276
|
+
policyErrors.metric = 'At least one complete autoscaling metric required.';
|
|
277
|
+
}
|
|
278
|
+
if (!hasValidSchedules(policy)) {
|
|
279
|
+
policyErrors.scalingSchedules = 'Every scaling schedule must be complete and within supported bounds.';
|
|
280
|
+
}
|
|
281
|
+
if (!hasValidScaleInControl(policy)) {
|
|
282
|
+
policyErrors.scaleInControl = 'Scale-in control values are outside supported bounds.';
|
|
283
|
+
}
|
|
284
|
+
const predictiveEnabled = policy.cpuUtilization?.predictiveMethod === GcePredictiveMethod.STANDARD;
|
|
285
|
+
if (predictiveEnabled && !GCEProviderSettings.feature.predictiveAutoscaling) {
|
|
286
|
+
policyErrors.predictiveAutoscaling = 'Predictive autoscaling is not enabled.';
|
|
287
|
+
}
|
|
288
|
+
if (Object.keys(policyErrors).length) {
|
|
289
|
+
errors.autoscalingPolicy = policyErrors;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (values.enableAutoHealing) {
|
|
294
|
+
const policy = (values.autoHealingPolicy || {}) as IGceAutoHealingPolicy;
|
|
295
|
+
const policyErrors: IAutoHealingPolicyValidationErrors = {};
|
|
296
|
+
if (!policy.healthCheck?.trim()) {
|
|
297
|
+
policyErrors.healthCheck = 'Health check required.';
|
|
298
|
+
}
|
|
299
|
+
if (!policy.healthCheckKind) {
|
|
300
|
+
policyErrors.healthCheckKind = 'Health check kind required.';
|
|
301
|
+
}
|
|
302
|
+
if (!isIntegerInRange(policy.initialDelaySec, 0)) {
|
|
303
|
+
policyErrors.initialDelaySec = 'Initial delay must be an integer between 0 and 2147483647 seconds.';
|
|
304
|
+
}
|
|
305
|
+
if (Object.keys(policyErrors).length) {
|
|
306
|
+
errors.autoHealingPolicy = policyErrors;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return errors;
|
|
311
|
+
}
|
|
312
|
+
|
|
288
313
|
function coherentDesiredCapacity(desired: any, min: any, max: any): any {
|
|
289
314
|
if (typeof desired === 'number' && typeof min === 'number' && typeof max === 'number') {
|
|
290
315
|
return Math.min(max, Math.max(min, desired));
|
|
@@ -1,30 +1,67 @@
|
|
|
1
|
+
import type { Mock, Mocked } from 'vitest';
|
|
1
2
|
import type { FormikProps } from 'formik';
|
|
2
3
|
import React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { DeploymentStrategySelector, TaskReason } from '@spinnaker/core';
|
|
4
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
5
|
+
import type { RenderResult } from '@testing-library/react';
|
|
6
6
|
|
|
7
7
|
import { ServerGroupBasicSettings } from './ServerGroupBasicSettings';
|
|
8
8
|
import { GceImageReader } from '../../../../image';
|
|
9
9
|
import type { IGceServerGroupCommand, IGceServerGroupWizardAdapter } from '../GceServerGroupWizard.types';
|
|
10
|
+
import { validateGceServerGroupBasicSettings } from './ServerGroupBasicSettings';
|
|
11
|
+
|
|
12
|
+
vi.mock('@spinnaker/core', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import('@spinnaker/core')>();
|
|
14
|
+
const MockTaskReason = ({ onChange, reason }: any) => (
|
|
15
|
+
<input
|
|
16
|
+
aria-label="Task reason"
|
|
17
|
+
data-component="MockTaskReason"
|
|
18
|
+
onChange={(event) => onChange(event.target.value)}
|
|
19
|
+
value={reason || ''}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
const MockDeploymentStrategySelector = ({ command, onFieldChange, onStrategyChange }: any) => (
|
|
23
|
+
<div data-component="MockDeploymentStrategySelector">
|
|
24
|
+
<output aria-label="Selected strategy">{command.strategy}</output>
|
|
25
|
+
<button
|
|
26
|
+
onClick={() =>
|
|
27
|
+
onStrategyChange(
|
|
28
|
+
{ ...command, strategy: 'redblack', maxRemainingAsgs: 2, scaleDown: false },
|
|
29
|
+
{ key: 'redblack', label: 'Red/Black' },
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
type="button"
|
|
33
|
+
>
|
|
34
|
+
Select red black
|
|
35
|
+
</button>
|
|
36
|
+
<button onClick={() => onFieldChange('maxRemainingAsgs', 3)} type="button">
|
|
37
|
+
Update strategy field
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
DeploymentStrategySelector: MockDeploymentStrategySelector,
|
|
44
|
+
TaskReason: MockTaskReason,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
10
47
|
|
|
11
48
|
describe('ServerGroupBasicSettings', () => {
|
|
12
49
|
it('renders accessible location fields and preserves persisted unavailable references', () => {
|
|
13
50
|
const { formik } = testProps();
|
|
14
|
-
const wrapper =
|
|
51
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
15
52
|
|
|
16
53
|
expect(selectOptions(wrapper, 'Account')).toEqual([
|
|
17
54
|
['', 'Select...'],
|
|
18
55
|
['known-account', 'known-account'],
|
|
19
56
|
['persisted-account', 'persisted-account (unavailable)'],
|
|
20
57
|
]);
|
|
21
|
-
expect(selectOptions(wrapper, 'Region')).
|
|
22
|
-
expect(selectOptions(wrapper, 'Zone')).
|
|
23
|
-
expect(selectOptions(wrapper, 'Network')).
|
|
24
|
-
expect(selectOptions(wrapper, 'Subnet')).
|
|
25
|
-
expect(wrapper.
|
|
26
|
-
expect(wrapper.
|
|
27
|
-
expect(wrapper.
|
|
58
|
+
expect(selectOptions(wrapper, 'Region')).toContainEqual(['persisted-region', 'persisted-region (unavailable)']);
|
|
59
|
+
expect(selectOptions(wrapper, 'Zone')).toContainEqual(['persisted-zone', 'persisted-zone (unavailable)']);
|
|
60
|
+
expect(selectOptions(wrapper, 'Network')).toContainEqual(['persisted-network', 'persisted-network (unavailable)']);
|
|
61
|
+
expect(selectOptions(wrapper, 'Subnet')).toContainEqual(['persisted-subnet', 'persisted-subnet (unavailable)']);
|
|
62
|
+
expect(wrapper.getByLabelText('Location mode')).toHaveValue('zonal');
|
|
63
|
+
expect(wrapper.getByLabelText('Stack')).toHaveValue('main');
|
|
64
|
+
expect(wrapper.getByLabelText('Detail')).toHaveValue('detail');
|
|
28
65
|
expect(formik.setValues).not.toHaveBeenCalled();
|
|
29
66
|
expect(formik.setFieldValue).not.toHaveBeenCalled();
|
|
30
67
|
});
|
|
@@ -44,14 +81,14 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
44
81
|
subnet: '',
|
|
45
82
|
});
|
|
46
83
|
const { adapter, formik } = testProps(command(), reconciled);
|
|
47
|
-
const wrapper =
|
|
84
|
+
const wrapper = renderPage(
|
|
48
85
|
<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} adapter={adapter} />,
|
|
49
86
|
);
|
|
50
87
|
|
|
51
|
-
wrapper.
|
|
88
|
+
fireEvent.change(wrapper.getByLabelText(label), { target: { value: selected } });
|
|
52
89
|
await flush();
|
|
53
90
|
|
|
54
|
-
const changedCommand = adapter.applyCommandHandler.
|
|
91
|
+
const changedCommand = adapter.applyCommandHandler.mock.lastCall[0];
|
|
55
92
|
expect(changedCommand[field]).toBe(field === 'regional' ? true : selected);
|
|
56
93
|
expect(adapter.applyCommandHandler).toHaveBeenCalledWith(changedCommand, handler);
|
|
57
94
|
expect(formik.setValues).toHaveBeenCalledWith(reconciled);
|
|
@@ -60,7 +97,7 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
60
97
|
|
|
61
98
|
it('reloads account-scoped images in addition to reconciling account-dependent fields', async () => {
|
|
62
99
|
const images = [{ imageName: 'new-account-image' }];
|
|
63
|
-
spyOn(GceImageReader, 'findImages').
|
|
100
|
+
vi.spyOn(GceImageReader, 'findImages').mockResolvedValue(images);
|
|
64
101
|
const values = command({
|
|
65
102
|
backingData: {
|
|
66
103
|
...command().backingData,
|
|
@@ -68,14 +105,14 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
68
105
|
},
|
|
69
106
|
});
|
|
70
107
|
const { adapter, formik } = testProps(values);
|
|
71
|
-
adapter.applyCommandHandler.
|
|
108
|
+
adapter.applyCommandHandler.mockImplementation((working: IGceServerGroupCommand) =>
|
|
72
109
|
Promise.resolve({ command: { ...working, region: null }, result: { dirty: {} } }),
|
|
73
110
|
);
|
|
74
|
-
const wrapper =
|
|
111
|
+
const wrapper = renderPage(
|
|
75
112
|
<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} adapter={adapter} />,
|
|
76
113
|
);
|
|
77
114
|
|
|
78
|
-
wrapper.
|
|
115
|
+
fireEvent.change(wrapper.getByLabelText('Account'), { target: { value: 'known-account' } });
|
|
79
116
|
await flush();
|
|
80
117
|
|
|
81
118
|
expect(GceImageReader.findImages).toHaveBeenCalledWith({
|
|
@@ -84,10 +121,10 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
84
121
|
q: '*',
|
|
85
122
|
});
|
|
86
123
|
expect(adapter.applyCommandHandler).toHaveBeenCalledWith(
|
|
87
|
-
|
|
124
|
+
expect.objectContaining({ credentials: 'known-account' }),
|
|
88
125
|
'credentialsChanged',
|
|
89
126
|
);
|
|
90
|
-
expect(formik.setValues.
|
|
127
|
+
expect(formik.setValues.mock.lastCall[0].backingData.allImages).toEqual(images);
|
|
91
128
|
});
|
|
92
129
|
|
|
93
130
|
([
|
|
@@ -105,7 +142,7 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
105
142
|
},
|
|
106
143
|
] as const).forEach(({ description, expectedImage, image, images }) => {
|
|
107
144
|
it(description, async () => {
|
|
108
|
-
spyOn(GceImageReader, 'findImages').
|
|
145
|
+
vi.spyOn(GceImageReader, 'findImages').mockResolvedValue(images as any);
|
|
109
146
|
const values = command({
|
|
110
147
|
image,
|
|
111
148
|
backingData: {
|
|
@@ -115,16 +152,14 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
115
152
|
});
|
|
116
153
|
const formik = publishingFormik(values);
|
|
117
154
|
const adapter = imageValidatingAdapter();
|
|
118
|
-
const wrapper =
|
|
155
|
+
const wrapper = renderPage(
|
|
119
156
|
<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} adapter={adapter} />,
|
|
120
157
|
);
|
|
121
158
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
await request;
|
|
159
|
+
fireEvent.change(wrapper.getByLabelText('Account'), { target: { value: 'known-account' } });
|
|
160
|
+
await flush();
|
|
126
161
|
|
|
127
|
-
expect(adapter.applyCommandHandler.
|
|
162
|
+
expect(adapter.applyCommandHandler.mock.lastCall[0].backingData.allImages).toEqual(images);
|
|
128
163
|
expect(formik.values.image).toBe(expectedImage);
|
|
129
164
|
});
|
|
130
165
|
});
|
|
@@ -132,7 +167,7 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
132
167
|
it('keeps the latest account images and user edits when account reloads finish out of order', async () => {
|
|
133
168
|
const firstImages = deferred<Array<{ imageName: string }>>();
|
|
134
169
|
const secondImages = deferred<Array<{ imageName: string }>>();
|
|
135
|
-
spyOn(GceImageReader, 'findImages').
|
|
170
|
+
vi.spyOn(GceImageReader, 'findImages').mockImplementation(({ account }) =>
|
|
136
171
|
account === 'first-account' ? firstImages.promise : secondImages.promise,
|
|
137
172
|
);
|
|
138
173
|
const values = command({
|
|
@@ -144,24 +179,22 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
144
179
|
});
|
|
145
180
|
const formik = publishingFormik(values);
|
|
146
181
|
const adapter = ({
|
|
147
|
-
applyCommandHandler:
|
|
148
|
-
.
|
|
149
|
-
.
|
|
182
|
+
applyCommandHandler: vi
|
|
183
|
+
.fn()
|
|
184
|
+
.mockImplementation((working: IGceServerGroupCommand) =>
|
|
150
185
|
Promise.resolve({ command: working, result: { dirty: {} } }),
|
|
151
186
|
),
|
|
152
|
-
} as unknown) as
|
|
153
|
-
const wrapper =
|
|
187
|
+
} as unknown) as Mocked<IGceServerGroupWizardAdapter>;
|
|
188
|
+
const wrapper = renderPage(
|
|
154
189
|
<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} adapter={adapter} />,
|
|
155
190
|
);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const firstRequest = changeAccount({ target: { value: 'first-account' } });
|
|
159
|
-
const secondRequest = changeAccount({ target: { value: 'second-account' } });
|
|
191
|
+
fireEvent.change(wrapper.getByLabelText('Account'), { target: { value: 'first-account' } });
|
|
192
|
+
fireEvent.change(wrapper.getByLabelText('Account'), { target: { value: 'second-account' } });
|
|
160
193
|
secondImages.resolve([{ imageName: 'second-account-image' }]);
|
|
161
|
-
await
|
|
194
|
+
await flush();
|
|
162
195
|
formik.values = { ...formik.values, freeFormDetails: 'edited-while-loading' };
|
|
163
196
|
firstImages.resolve([{ imageName: 'first-account-image' }]);
|
|
164
|
-
await
|
|
197
|
+
await flush();
|
|
165
198
|
|
|
166
199
|
expect(formik.values.credentials).toBe('second-account');
|
|
167
200
|
expect(formik.values.backingData.allImages).toEqual([{ imageName: 'second-account-image' }]);
|
|
@@ -170,15 +203,15 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
170
203
|
|
|
171
204
|
it('updates subnet, stack, and detail without invoking a parent handler', () => {
|
|
172
205
|
const { adapter, formik } = testProps();
|
|
173
|
-
const wrapper =
|
|
206
|
+
const wrapper = renderPage(
|
|
174
207
|
<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} adapter={adapter} />,
|
|
175
208
|
);
|
|
176
209
|
|
|
177
|
-
wrapper.
|
|
178
|
-
wrapper.
|
|
179
|
-
wrapper.
|
|
210
|
+
fireEvent.change(wrapper.getByLabelText('Subnet'), { target: { value: 'known-subnet' } });
|
|
211
|
+
fireEvent.change(wrapper.getByLabelText('Stack'), { target: { value: 'new-stack' } });
|
|
212
|
+
fireEvent.change(wrapper.getByLabelText('Detail'), { target: { value: 'new-detail' } });
|
|
180
213
|
|
|
181
|
-
expect(formik.setFieldValue.calls
|
|
214
|
+
expect(formik.setFieldValue.mock.calls).toEqual([
|
|
182
215
|
['subnet', 'known-subnet'],
|
|
183
216
|
['stack', 'new-stack'],
|
|
184
217
|
['freeFormDetails', 'new-detail'],
|
|
@@ -189,20 +222,18 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
189
222
|
(['create', 'clone'] as const).forEach((mode) => {
|
|
190
223
|
it(`binds the optional task reason to the command in ${mode} mode`, () => {
|
|
191
224
|
const { formik } = testProps(command({ reason: 'existing reason', viewState: { mode } }));
|
|
192
|
-
const wrapper =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(taskReason.prop('reason')).toBe('existing reason');
|
|
196
|
-
taskReason.prop('onChange')('updated reason');
|
|
225
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
226
|
+
expect(wrapper.getByLabelText('Task reason')).toHaveValue('existing reason');
|
|
227
|
+
fireEvent.change(wrapper.getByLabelText('Task reason'), { target: { value: 'updated reason' } });
|
|
197
228
|
expect(formik.setFieldValue).toHaveBeenCalledWith('reason', 'updated reason');
|
|
198
229
|
});
|
|
199
230
|
});
|
|
200
231
|
|
|
201
232
|
it('hides the zonal field in regional mode', () => {
|
|
202
233
|
const { formik } = testProps(command({ regional: true, zone: null }));
|
|
203
|
-
const wrapper =
|
|
234
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
204
235
|
|
|
205
|
-
expect(wrapper.
|
|
236
|
+
expect(wrapper.container.querySelector('[aria-label="Zone"]')).not.toBeInTheDocument();
|
|
206
237
|
});
|
|
207
238
|
|
|
208
239
|
([
|
|
@@ -216,33 +247,32 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
216
247
|
viewState: { mode, disableStrategySelection: false },
|
|
217
248
|
});
|
|
218
249
|
const { formik } = testProps(values);
|
|
219
|
-
const wrapper =
|
|
220
|
-
const traffic = wrapper.
|
|
250
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
251
|
+
const traffic = wrapper.getByLabelText('Send client requests to new instances');
|
|
221
252
|
|
|
222
|
-
expect(traffic.
|
|
223
|
-
expect(wrapper.
|
|
224
|
-
expect(wrapper.
|
|
253
|
+
expect(traffic).toHaveProperty('checked', enableTraffic);
|
|
254
|
+
expect(wrapper.getByRole('group', { name: 'Deployment strategy' })).toBeInTheDocument();
|
|
255
|
+
expect(wrapper.getByLabelText('Selected strategy')).toHaveTextContent('custom');
|
|
225
256
|
|
|
226
|
-
|
|
257
|
+
fireEvent.click(traffic);
|
|
227
258
|
expect(formik.setFieldValue).toHaveBeenCalledWith('enableTraffic', !enableTraffic);
|
|
228
259
|
});
|
|
229
260
|
});
|
|
230
261
|
|
|
231
262
|
it('forwards strategy commands and strategy-specific fields through Formik and the command callback', () => {
|
|
232
|
-
const onStrategyChange =
|
|
263
|
+
const onStrategyChange = vi.fn();
|
|
233
264
|
const values = command({
|
|
234
265
|
onStrategyChange,
|
|
235
266
|
strategy: '',
|
|
236
267
|
viewState: { mode: 'editPipeline', disableStrategySelection: false },
|
|
237
268
|
});
|
|
238
269
|
const { formik } = testProps(values);
|
|
239
|
-
const wrapper =
|
|
240
|
-
const selector = wrapper.find(DeploymentStrategySelector);
|
|
270
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
241
271
|
const strategy = { key: 'redblack', label: 'Red/Black' } as any;
|
|
242
272
|
const updatedCommand = { ...values, strategy: 'redblack', maxRemainingAsgs: 2, scaleDown: false };
|
|
243
273
|
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
fireEvent.click(wrapper.getByRole('button', { name: 'Select red black' }));
|
|
275
|
+
fireEvent.click(wrapper.getByRole('button', { name: 'Update strategy field' }));
|
|
246
276
|
|
|
247
277
|
expect(onStrategyChange).toHaveBeenCalledWith(updatedCommand, strategy);
|
|
248
278
|
expect(formik.setValues).toHaveBeenCalledWith(updatedCommand);
|
|
@@ -257,10 +287,9 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
257
287
|
viewState: { mode: 'create', disableStrategySelection: true },
|
|
258
288
|
});
|
|
259
289
|
const { formik } = testProps(values);
|
|
260
|
-
const wrapper =
|
|
290
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
261
291
|
|
|
262
|
-
expect(wrapper.
|
|
263
|
-
expect(wrapper.find('[aria-label="Deployment strategy"]').exists()).toBe(false);
|
|
292
|
+
expect(wrapper.container.querySelector('[aria-label="Deployment strategy"]')).not.toBeInTheDocument();
|
|
264
293
|
expect(values.strategy).toBe('custom');
|
|
265
294
|
expect(values.strategyApplication).toBe('strategy-app');
|
|
266
295
|
expect(values.strategyPipeline).toBe('strategy-pipeline');
|
|
@@ -270,10 +299,8 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
270
299
|
|
|
271
300
|
it('validates required fields and naming rules owned by the page', () => {
|
|
272
301
|
const { formik } = testProps();
|
|
273
|
-
const page = new ServerGroupBasicSettings({ app: { name: 'app' } as any, formik } as any);
|
|
274
|
-
|
|
275
302
|
expect(
|
|
276
|
-
|
|
303
|
+
validateGceServerGroupBasicSettings(
|
|
277
304
|
command({ credentials: '', region: '', zone: '', stack: 'invalid stack', freeFormDetails: 'bad_detail' }),
|
|
278
305
|
),
|
|
279
306
|
).toEqual({
|
|
@@ -294,7 +321,7 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
294
321
|
freeFormDetails: 'bad_detail',
|
|
295
322
|
});
|
|
296
323
|
const { formik } = testProps(values);
|
|
297
|
-
const wrapper =
|
|
324
|
+
const wrapper = renderPage(<ServerGroupBasicSettings app={{ name: 'app' } as any} formik={formik} />);
|
|
298
325
|
|
|
299
326
|
[
|
|
300
327
|
['Account', 'credentials', 'Account required.'],
|
|
@@ -303,14 +330,14 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
303
330
|
['Stack', 'stack', 'Stack can only contain letters and numbers.'],
|
|
304
331
|
['Detail', 'freeFormDetails', 'Detail can only contain letters, numbers, and dashes.'],
|
|
305
332
|
].forEach(([label, field, message]) => {
|
|
306
|
-
const control = wrapper.
|
|
333
|
+
const control = wrapper.getByLabelText(label);
|
|
307
334
|
const errorId = `gce-server-group-${field}-error`;
|
|
308
|
-
const alert = wrapper.
|
|
335
|
+
const alert = wrapper.container.querySelector(`[id="${errorId}"][role="alert"]`);
|
|
309
336
|
|
|
310
|
-
expect(control.
|
|
311
|
-
expect(control.
|
|
312
|
-
expect(alert
|
|
313
|
-
expect(alert
|
|
337
|
+
expect(control, label).toHaveAttribute('aria-invalid', 'true');
|
|
338
|
+
expect(control, label).toHaveAttribute('aria-describedby', errorId);
|
|
339
|
+
expect(alert, label).toBeInTheDocument();
|
|
340
|
+
expect(alert, label).toHaveTextContent(message);
|
|
314
341
|
});
|
|
315
342
|
});
|
|
316
343
|
|
|
@@ -321,47 +348,44 @@ describe('ServerGroupBasicSettings', () => {
|
|
|
321
348
|
viewState: { mode: 'editPipeline', templatingEnabled: true },
|
|
322
349
|
});
|
|
323
350
|
const { formik } = testProps(values);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
expect(page.validate(values)).toEqual({});
|
|
351
|
+
expect(validateGceServerGroupBasicSettings(values)).toEqual({});
|
|
327
352
|
});
|
|
328
353
|
});
|
|
329
354
|
|
|
330
|
-
function selectOptions(wrapper:
|
|
331
|
-
return wrapper
|
|
332
|
-
.
|
|
333
|
-
.
|
|
355
|
+
function selectOptions(wrapper: RenderResult, label: string): string[][] {
|
|
356
|
+
return Array.from(wrapper.getByLabelText(label).querySelectorAll('option')).map((option) => [
|
|
357
|
+
option.value,
|
|
358
|
+
option.textContent || '',
|
|
359
|
+
]);
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
function testProps(values = command(), adapterResult = values) {
|
|
337
363
|
const formik = ({
|
|
338
364
|
values,
|
|
339
|
-
setFieldValue:
|
|
340
|
-
setValues:
|
|
365
|
+
setFieldValue: vi.fn(),
|
|
366
|
+
setValues: vi.fn(),
|
|
341
367
|
} as unknown) as FormikProps<IGceServerGroupCommand>;
|
|
342
368
|
const adapter = ({
|
|
343
|
-
applyCommandHandler:
|
|
344
|
-
|
|
345
|
-
.and.resolveTo({ command: adapterResult, result: { dirty: {} } }),
|
|
346
|
-
} as unknown) as jasmine.SpyObj<IGceServerGroupWizardAdapter>;
|
|
369
|
+
applyCommandHandler: vi.fn().mockResolvedValue({ command: adapterResult, result: { dirty: {} } }),
|
|
370
|
+
} as unknown) as Mocked<IGceServerGroupWizardAdapter>;
|
|
347
371
|
return { adapter, formik };
|
|
348
372
|
}
|
|
349
373
|
|
|
350
374
|
function publishingFormik(values = command()): FormikProps<IGceServerGroupCommand> {
|
|
351
375
|
const formik = ({
|
|
352
376
|
values,
|
|
353
|
-
setFieldValue:
|
|
354
|
-
setValues:
|
|
377
|
+
setFieldValue: vi.fn(),
|
|
378
|
+
setValues: vi.fn(),
|
|
355
379
|
} as unknown) as FormikProps<IGceServerGroupCommand>;
|
|
356
|
-
(formik.setValues as
|
|
380
|
+
(formik.setValues as Mock).mockImplementation((nextValues: IGceServerGroupCommand) => {
|
|
357
381
|
formik.values = nextValues;
|
|
358
382
|
});
|
|
359
383
|
return formik;
|
|
360
384
|
}
|
|
361
385
|
|
|
362
|
-
function imageValidatingAdapter():
|
|
386
|
+
function imageValidatingAdapter(): Mocked<IGceServerGroupWizardAdapter> {
|
|
363
387
|
return ({
|
|
364
|
-
applyCommandHandler:
|
|
388
|
+
applyCommandHandler: vi.fn().mockImplementation((working: IGceServerGroupCommand) => {
|
|
365
389
|
const imageAvailable = (working.backingData.allImages || []).some(
|
|
366
390
|
({ imageName }: { imageName: string }) => imageName === working.image,
|
|
367
391
|
);
|
|
@@ -370,7 +394,7 @@ function imageValidatingAdapter(): jasmine.SpyObj<IGceServerGroupWizardAdapter>
|
|
|
370
394
|
result: { dirty: {} },
|
|
371
395
|
});
|
|
372
396
|
}),
|
|
373
|
-
} as unknown) as
|
|
397
|
+
} as unknown) as Mocked<IGceServerGroupWizardAdapter>;
|
|
374
398
|
}
|
|
375
399
|
|
|
376
400
|
function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGroupCommand {
|
|
@@ -416,3 +440,8 @@ function deferred<T>() {
|
|
|
416
440
|
});
|
|
417
441
|
return { promise, resolve };
|
|
418
442
|
}
|
|
443
|
+
|
|
444
|
+
function renderPage(component: React.ReactElement): RenderResult {
|
|
445
|
+
const container = document.body.appendChild(document.createElement('div'));
|
|
446
|
+
return render(component, { baseElement: container, container });
|
|
447
|
+
}
|
|
@@ -17,26 +17,32 @@ interface ISelectOption {
|
|
|
17
17
|
value: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function validateGceServerGroupBasicSettings(
|
|
21
|
+
values: IGceServerGroupCommand,
|
|
22
|
+
): IGceServerGroupCommandValidationErrors {
|
|
23
|
+
const sharedErrors = validateGceServerGroupCommand(values);
|
|
24
|
+
const errors: IGceServerGroupCommandValidationErrors = {};
|
|
25
|
+
if (sharedErrors.credentials) errors.credentials = sharedErrors.credentials;
|
|
26
|
+
if (sharedErrors.region) errors.region = sharedErrors.region;
|
|
27
|
+
if (sharedErrors.zone) errors.zone = sharedErrors.zone;
|
|
28
|
+
if (sharedErrors.stack) errors.stack = sharedErrors.stack;
|
|
29
|
+
if (sharedErrors.freeFormDetails) errors.freeFormDetails = sharedErrors.freeFormDetails;
|
|
30
|
+
const stackPattern = values.viewState.templatingEnabled ? /^([a-zA-Z0-9]*(\${.+})*)*$/ : /^[a-zA-Z0-9]*$/;
|
|
31
|
+
const detailPattern = values.viewState.templatingEnabled ? /^([a-zA-Z0-9-]*(\${.+})*)*$/ : /^[a-zA-Z0-9-]*$/;
|
|
32
|
+
|
|
33
|
+
if (values.stack && !stackPattern.test(values.stack)) {
|
|
34
|
+
errors.stack = 'Stack can only contain letters and numbers.';
|
|
35
|
+
}
|
|
36
|
+
if (values.freeFormDetails && !detailPattern.test(values.freeFormDetails)) {
|
|
37
|
+
errors.freeFormDetails = 'Detail can only contain letters, numbers, and dashes.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return errors;
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
export class ServerGroupBasicSettings extends GceServerGroupWizardPage {
|
|
21
44
|
public validate(values: IGceServerGroupCommand): IGceServerGroupCommandValidationErrors {
|
|
22
|
-
|
|
23
|
-
const errors: IGceServerGroupCommandValidationErrors = {};
|
|
24
|
-
if (sharedErrors.credentials) errors.credentials = sharedErrors.credentials;
|
|
25
|
-
if (sharedErrors.region) errors.region = sharedErrors.region;
|
|
26
|
-
if (sharedErrors.zone) errors.zone = sharedErrors.zone;
|
|
27
|
-
if (sharedErrors.stack) errors.stack = sharedErrors.stack;
|
|
28
|
-
if (sharedErrors.freeFormDetails) errors.freeFormDetails = sharedErrors.freeFormDetails;
|
|
29
|
-
const stackPattern = values.viewState.templatingEnabled ? /^([a-zA-Z0-9]*(\${.+})*)*$/ : /^[a-zA-Z0-9]*$/;
|
|
30
|
-
const detailPattern = values.viewState.templatingEnabled ? /^([a-zA-Z0-9-]*(\${.+})*)*$/ : /^[a-zA-Z0-9-]*$/;
|
|
31
|
-
|
|
32
|
-
if (values.stack && !stackPattern.test(values.stack)) {
|
|
33
|
-
errors.stack = 'Stack can only contain letters and numbers.';
|
|
34
|
-
}
|
|
35
|
-
if (values.freeFormDetails && !detailPattern.test(values.freeFormDetails)) {
|
|
36
|
-
errors.freeFormDetails = 'Detail can only contain letters, numbers, and dashes.';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return errors;
|
|
45
|
+
return validateGceServerGroupBasicSettings(values);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
private parentChanged = (
|