@spinnaker/google 2026.3.2 → 2026.4.1

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.
Files changed (108) hide show
  1. package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
  2. package/dist/domain/loadBalancer.d.ts +6 -3
  3. package/dist/image/ImageSelect.d.ts +1 -1
  4. package/dist/index.js +159 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
  7. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
  8. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
  9. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
  10. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
  11. package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
  12. package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
  13. package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
  14. package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
  15. package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
  16. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  17. package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
  18. package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
  19. package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
  20. package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
  21. package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
  22. package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
  23. package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
  24. package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
  25. package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
  26. package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
  27. package/package.json +5 -5
  28. package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
  29. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
  30. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
  31. package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
  32. package/src/domain/loadBalancer.ts +6 -3
  33. package/src/gce.module.spec.ts +132 -153
  34. package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
  35. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
  36. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
  37. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
  38. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
  39. package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
  40. package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
  41. package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
  42. package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
  43. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
  44. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
  45. package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
  46. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
  47. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
  48. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
  49. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
  50. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
  51. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
  52. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
  53. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
  54. package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
  55. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
  56. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
  57. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
  58. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
  59. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
  60. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
  61. package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
  62. package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
  63. package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
  64. package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
  65. package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
  66. package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
  67. package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
  68. package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
  69. package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
  70. package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
  71. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
  72. package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
  73. package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
  74. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
  75. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
  76. package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
  77. package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
  78. package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
  79. package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
  80. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
  81. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
  82. package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
  83. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
  84. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
  85. package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
  86. package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
  87. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
  88. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
  89. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
  90. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
  91. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
  92. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
  93. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
  94. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
  95. package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
  96. package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
  97. package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
  98. package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
  99. package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
  100. package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
  101. package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
  102. package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
  103. package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
  104. package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
  105. package/src/serverGroup/serverGroup.transformer.js +1 -0
  106. package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
  107. package/src/validation/ApplicationNameValidator.spec.ts +1 -1
  108. /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
@@ -0,0 +1,401 @@
1
+ import React from 'react';
2
+
3
+ import type {
4
+ GceLoadBalancerProtocol,
5
+ IGceLoadBalancerCommand,
6
+ IGceLoadBalancerData,
7
+ IGceLoadBalancerDataItem,
8
+ IGceLoadBalancerHealthCheck,
9
+ IGceResourceReference,
10
+ } from '../common';
11
+ import { mergeGceResourceOptions } from '../common';
12
+
13
+ export type GceRegionalExternalNetworkSessionAffinity =
14
+ | 'NONE'
15
+ | 'CLIENT_IP'
16
+ | 'CLIENT_IP_PROTO'
17
+ | 'CLIENT_IP_PORT_PROTO';
18
+
19
+ export type IGceRegionalExternalNetworkLoadBalancerCommand = IGceLoadBalancerCommand & {
20
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK';
21
+ ports: string[];
22
+ };
23
+
24
+ interface IGceRegionalExternalNetworkDataItem extends IGceLoadBalancerDataItem {
25
+ account?: string;
26
+ address?: string;
27
+ addressType?: string;
28
+ credentials?: string;
29
+ networkTier?: string;
30
+ region?: string;
31
+ }
32
+
33
+ export interface IGceRegionalExternalNetworkLoadBalancerOptions {
34
+ accounts: IGceRegionalExternalNetworkDataItem[];
35
+ addresses: IGceRegionalExternalNetworkDataItem[];
36
+ regions: IGceRegionalExternalNetworkDataItem[];
37
+ }
38
+
39
+ export interface IGceRegionalExternalNetworkLoadBalancerEditorProps {
40
+ command: IGceRegionalExternalNetworkLoadBalancerCommand;
41
+ data: IGceLoadBalancerData;
42
+ onChange: (command: IGceRegionalExternalNetworkLoadBalancerCommand) => void;
43
+ }
44
+
45
+ const SESSION_AFFINITIES: GceRegionalExternalNetworkSessionAffinity[] = [
46
+ 'NONE',
47
+ 'CLIENT_IP',
48
+ 'CLIENT_IP_PROTO',
49
+ 'CLIENT_IP_PORT_PROTO',
50
+ ];
51
+ const PROTOCOLS: GceLoadBalancerProtocol[] = ['TCP', 'UDP'];
52
+ const NETWORK_TIERS = ['PREMIUM', 'STANDARD'];
53
+ const MAX_PORTS = 5;
54
+
55
+ export function createGceRegionalExternalNetworkHealthCheck(name: string): IGceLoadBalancerHealthCheck {
56
+ return {
57
+ healthCheckType: 'TCP',
58
+ name,
59
+ port: 80,
60
+ };
61
+ }
62
+
63
+ export function buildGceRegionalExternalNetworkLoadBalancerOptions(
64
+ command: IGceRegionalExternalNetworkLoadBalancerCommand,
65
+ data: IGceLoadBalancerData,
66
+ ): IGceRegionalExternalNetworkLoadBalancerOptions {
67
+ const accountMatches = (item: IGceRegionalExternalNetworkDataItem): boolean =>
68
+ !item.account && !item.credentials ? true : (item.account || item.credentials) === command.credentials;
69
+ const locationMatches = (item: IGceRegionalExternalNetworkDataItem): boolean =>
70
+ accountMatches(item) && (!item.region || item.region === command.region);
71
+ const listener = command.listeners[0];
72
+
73
+ return {
74
+ accounts: mergeGceResourceOptions(data.accounts, command.credentials ? [{ name: command.credentials }] : []),
75
+ addresses: mergeGceResourceOptions(
76
+ (data.addresses as IGceRegionalExternalNetworkDataItem[])
77
+ .filter(locationMatches)
78
+ .filter((item) => item.addressType === 'EXTERNAL'),
79
+ references(listener?.address),
80
+ ),
81
+ regions: mergeGceResourceOptions(data.regions, command.region ? [{ name: command.region }] : []),
82
+ };
83
+ }
84
+
85
+ export function validateGceRegionalExternalNetworkLoadBalancerCommand(
86
+ command: IGceRegionalExternalNetworkLoadBalancerCommand,
87
+ ): string[] {
88
+ const errors: string[] = [];
89
+ const backend = command.backendServices[0];
90
+ const ports = command.ports?.length ? command.ports : splitPorts(command.listeners[0]?.portRange);
91
+
92
+ if (!command.name.trim()) errors.push('Name is required.');
93
+ if (!command.credentials) errors.push('Account is required.');
94
+ if (!command.region) errors.push('Region is required.');
95
+ if (command.listeners[0]?.protocol !== 'TCP' && command.listeners[0]?.protocol !== 'UDP') {
96
+ errors.push('Protocol must be TCP or UDP.');
97
+ }
98
+
99
+ if (!ports.length) {
100
+ errors.push('Ports must be between 1 and 65535.');
101
+ } else {
102
+ if (ports.length > MAX_PORTS) {
103
+ errors.push('REGIONAL_EXTERNAL_NETWORK load balancers accept between one and five ports.');
104
+ }
105
+ if (ports.some((port) => !validPort(port))) {
106
+ errors.push('Ports must be between 1 and 65535.');
107
+ }
108
+ }
109
+
110
+ if (!backend?.name?.trim()) errors.push('Backend service name is required.');
111
+ if (!hasHealthCheck(backend)) errors.push('Each backend service requires a health check.');
112
+ if (!healthCheckName(backend, command.healthChecks[0])) errors.push('Health check name is required.');
113
+ const backendHealthCheck =
114
+ backend?.healthCheck && typeof backend.healthCheck === 'object'
115
+ ? (backend.healthCheck as IGceLoadBalancerHealthCheck)
116
+ : command.healthChecks[0];
117
+ if (!validPort(backendHealthCheck?.port)) {
118
+ errors.push('Health check port must be between 1 and 65535.');
119
+ }
120
+ const originalListener = command.original?.listeners[0];
121
+ if (
122
+ command.mode === 'edit' &&
123
+ originalListener &&
124
+ (command.listeners[0]?.protocol !== originalListener.protocol ||
125
+ ports.join(',') !== splitPorts(originalListener.portRange).join(','))
126
+ ) {
127
+ errors.push('Protocol and ports cannot be changed while editing a REGIONAL_EXTERNAL_NETWORK load balancer.');
128
+ }
129
+
130
+ const sessionAffinity = String(backend?.sessionAffinity || 'NONE').toUpperCase();
131
+ if (!SESSION_AFFINITIES.includes(sessionAffinity as GceRegionalExternalNetworkSessionAffinity)) {
132
+ errors.push('Session affinity must be NONE, CLIENT_IP, CLIENT_IP_PROTO, or CLIENT_IP_PORT_PROTO.');
133
+ }
134
+
135
+ return errors;
136
+ }
137
+
138
+ export function GceRegionalExternalNetworkLoadBalancerEditor({
139
+ command,
140
+ data,
141
+ onChange,
142
+ }: IGceRegionalExternalNetworkLoadBalancerEditorProps): JSX.Element {
143
+ const options = buildGceRegionalExternalNetworkLoadBalancerOptions(command, data);
144
+ const listener = command.listeners[0] || { name: command.name, portRange: '', protocol: 'TCP' };
145
+ const backend = command.backendServices[0] || { name: command.name, sessionAffinity: 'NONE' };
146
+ const healthCheck =
147
+ (typeof backend.healthCheck === 'object' ? backend.healthCheck : undefined) ||
148
+ command.healthChecks[0] ||
149
+ createGceRegionalExternalNetworkHealthCheck(command.name);
150
+ const editing = command.mode === 'edit';
151
+ const portsValue = (command.ports?.length ? command.ports : splitPorts(listener.portRange)).join(',');
152
+
153
+ const update = (updates: Partial<IGceRegionalExternalNetworkLoadBalancerCommand>): void =>
154
+ onChange({ ...command, ...updates } as IGceRegionalExternalNetworkLoadBalancerCommand);
155
+ // Reserved addresses belong to one account and region, so a scope change drops the selection.
156
+ const updateScope = (updates: Partial<IGceRegionalExternalNetworkLoadBalancerCommand>): void =>
157
+ update({ ...updates, listeners: [{ ...listener, address: undefined }] });
158
+ const updateListener = (updates: Partial<typeof listener>): void =>
159
+ update({ listeners: [{ ...listener, ...updates }] });
160
+ const updateBackend = (updates: Record<string, unknown>): void =>
161
+ update({ backendServices: [{ ...backend, ...updates }] });
162
+ const updateHealthCheck = (updates: Partial<IGceLoadBalancerHealthCheck>): void => {
163
+ const nextHealthCheck = { ...healthCheck, ...updates };
164
+ update({
165
+ backendServices: [{ ...backend, healthCheck: (nextHealthCheck as unknown) as IGceResourceReference }],
166
+ healthChecks: [nextHealthCheck],
167
+ });
168
+ };
169
+ const updateName = (name: string): void =>
170
+ update({
171
+ name,
172
+ listeners: [{ ...listener, name: listener.name === command.name ? name : listener.name }],
173
+ backendServices: [{ ...backend, name: backend.name === command.name ? name : backend.name }],
174
+ });
175
+ const updatePorts = (value: string): void => {
176
+ const ports = value.split(',');
177
+ update({
178
+ ports,
179
+ listeners: [{ ...listener, portRange: ports.join(',') }],
180
+ });
181
+ };
182
+ const updateAddress = (addressValue: string): void => {
183
+ const selected = (data.addresses as IGceRegionalExternalNetworkDataItem[]).find(
184
+ (item) => item.address === addressValue || item.name === addressValue,
185
+ );
186
+ const address: IGceResourceReference | undefined = addressValue
187
+ ? {
188
+ ...(selected || {}),
189
+ address: selected?.address || addressValue,
190
+ name: selected?.address || selected?.name || addressValue,
191
+ }
192
+ : undefined;
193
+ update({
194
+ listeners: [{ ...listener, address }],
195
+ networkTier: selected?.networkTier || (addressValue ? 'PREMIUM' : command.networkTier),
196
+ });
197
+ };
198
+
199
+ return (
200
+ <div className="form-horizontal gce-regional-external-network-load-balancer-editor">
201
+ {textField('Name', 'name', command.name, updateName, editing)}
202
+ {selectField(
203
+ 'Account',
204
+ 'credentials',
205
+ command.credentials,
206
+ options.accounts,
207
+ (credentials) => updateScope({ credentials }),
208
+ editing,
209
+ )}
210
+ {selectField('Region', 'region', command.region, options.regions, (region) => updateScope({ region }), editing)}
211
+ {selectField(
212
+ 'IP address',
213
+ 'address',
214
+ String(listener.address?.address || listener.address?.name || ''),
215
+ options.addresses,
216
+ updateAddress,
217
+ editing,
218
+ (item) => item.address || item.name,
219
+ )}
220
+ {selectField(
221
+ 'Network tier',
222
+ 'networkTier',
223
+ command.networkTier || 'PREMIUM',
224
+ NETWORK_TIERS.map((name) => ({ name })),
225
+ (networkTier) => update({ networkTier }),
226
+ editing || Boolean(listener.address),
227
+ ({ name }) => name,
228
+ false,
229
+ )}
230
+ {selectField(
231
+ 'Protocol',
232
+ 'protocol',
233
+ listener.protocol,
234
+ PROTOCOLS.map((name) => ({ name })),
235
+ (protocol) => updateListener({ protocol: protocol as GceLoadBalancerProtocol }),
236
+ editing,
237
+ ({ name }) => name,
238
+ false,
239
+ )}
240
+ {textField('Ports', 'ports', portsValue, updatePorts, editing)}
241
+ {selectField(
242
+ 'Session affinity',
243
+ 'sessionAffinity',
244
+ String(backend.sessionAffinity || 'NONE'),
245
+ SESSION_AFFINITIES.map((name) => ({ name })),
246
+ (sessionAffinity) => updateBackend({ sessionAffinity }),
247
+ false,
248
+ ({ name }) => name,
249
+ false,
250
+ )}
251
+
252
+ <h4>Health Check</h4>
253
+ <div className="form-group" data-field="healthCheck">
254
+ {textField('Name', 'healthCheckName', healthCheck.name || '', (name) => updateHealthCheck({ name }))}
255
+ {numberField('Port', 'healthCheckPort', healthCheck.port, (port) => updateHealthCheck({ port }))}
256
+ </div>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ function textField(
262
+ label: string,
263
+ field: string,
264
+ value: string,
265
+ onChange: (value: string) => void,
266
+ disabled = false,
267
+ ): JSX.Element {
268
+ return (
269
+ <div className="form-group" data-field={field}>
270
+ <label className="col-md-3 sm-label-right" htmlFor={controlId(field)}>
271
+ {label}
272
+ </label>
273
+ <div className="col-md-7">
274
+ <input
275
+ className="form-control input-sm"
276
+ disabled={disabled}
277
+ id={controlId(field)}
278
+ onChange={(event) => onChange(event.target.value)}
279
+ value={value || ''}
280
+ />
281
+ </div>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ function numberField(
287
+ label: string,
288
+ field: string,
289
+ value: unknown,
290
+ onChange: (value: number | string | undefined) => void,
291
+ ): JSX.Element {
292
+ return (
293
+ <div className="form-group" data-field={field}>
294
+ <label className="col-md-3 sm-label-right" htmlFor={controlId(field)}>
295
+ {label}
296
+ </label>
297
+ <div className="col-md-7">
298
+ <input
299
+ className="form-control input-sm"
300
+ id={controlId(field)}
301
+ inputMode="numeric"
302
+ onChange={(event) =>
303
+ onChange(
304
+ event.target.value === ''
305
+ ? undefined
306
+ : /^\d+$/.test(event.target.value)
307
+ ? Number(event.target.value)
308
+ : event.target.value,
309
+ )
310
+ }
311
+ type="text"
312
+ value={value === undefined || value === null ? '' : String(value)}
313
+ />
314
+ </div>
315
+ </div>
316
+ );
317
+ }
318
+
319
+ function selectField(
320
+ label: string,
321
+ field: string,
322
+ value: string | undefined,
323
+ options: readonly IGceRegionalExternalNetworkDataItem[],
324
+ onChange: (value: string) => void,
325
+ disabled = false,
326
+ getValue: (item: IGceRegionalExternalNetworkDataItem) => string = ({ name }) => name,
327
+ includeEmptyOption = true,
328
+ ): JSX.Element {
329
+ return (
330
+ <div className="form-group" data-field={field}>
331
+ <label className="col-md-3 sm-label-right" htmlFor={controlId(field)}>
332
+ {label}
333
+ </label>
334
+ <div className="col-md-7">
335
+ <select
336
+ className="form-control input-sm"
337
+ disabled={disabled}
338
+ id={controlId(field)}
339
+ onChange={(event) => onChange(event.target.value)}
340
+ value={value || ''}
341
+ >
342
+ {includeEmptyOption && <option value="">Select...</option>}
343
+ {options.map((option) => {
344
+ const optionValue = getValue(option);
345
+ return (
346
+ <option key={optionValue} value={optionValue}>
347
+ {optionValue}
348
+ </option>
349
+ );
350
+ })}
351
+ </select>
352
+ </div>
353
+ </div>
354
+ );
355
+ }
356
+
357
+ function controlId(field: string): string {
358
+ return `gce-regional-external-network-${field}`;
359
+ }
360
+
361
+ function references<T extends IGceLoadBalancerDataItem>(referenceValue?: T): T[] {
362
+ return referenceValue?.name || (referenceValue as IGceRegionalExternalNetworkDataItem | undefined)?.address
363
+ ? [referenceValue]
364
+ : [];
365
+ }
366
+
367
+ function splitPorts(value: string | undefined): string[] {
368
+ return String(value || '').split(',');
369
+ }
370
+
371
+ function validPort(value: unknown): boolean {
372
+ const text = String(value ?? '');
373
+ if (!/^\d+$/.test(text)) return false;
374
+ const port = Number(text);
375
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
376
+ }
377
+
378
+ function hasHealthCheck(backend?: { healthCheck?: unknown }): boolean {
379
+ const healthCheck = backend?.healthCheck;
380
+ if (!healthCheck) return false;
381
+ if (typeof healthCheck === 'string') return Boolean(healthCheck);
382
+ if (typeof healthCheck === 'object') {
383
+ const record = healthCheck as Record<string, unknown>;
384
+ return Boolean(record.healthCheckType || record.port || record.name);
385
+ }
386
+ return false;
387
+ }
388
+
389
+ function healthCheckName(
390
+ backend: { healthCheck?: unknown } | undefined,
391
+ commandHealthCheck: IGceLoadBalancerHealthCheck | undefined,
392
+ ): string {
393
+ const backendHealthCheck = backend?.healthCheck;
394
+ const name =
395
+ typeof backendHealthCheck === 'string'
396
+ ? backendHealthCheck
397
+ : backendHealthCheck !== null && typeof backendHealthCheck === 'object'
398
+ ? (backendHealthCheck as IGceResourceReference).name
399
+ : commandHealthCheck?.name;
400
+ return String(name || '').trim();
401
+ }