@openshift-assisted/ui-lib 2.46.3 → 2.47.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.
Files changed (126) hide show
  1. package/build/@types/cim/components/ClusterDeployment/ClusterDetailsFormFields.d.ts.map +1 -1
  2. package/build/@types/cim/types/k8s/agent-cluster-install.d.ts +1 -0
  3. package/build/@types/cim/types/k8s/agent-cluster-install.d.ts.map +1 -1
  4. package/build/@types/common/api/assisted-service/BundleAPI.d.ts +2 -2
  5. package/build/@types/common/api/assisted-service/ClustersAPI.d.ts +25 -25
  6. package/build/@types/common/api/assisted-service/ComponentVersionsAPI.d.ts +1 -1
  7. package/build/@types/common/api/assisted-service/EventsAPI.d.ts +1 -1
  8. package/build/@types/common/api/assisted-service/HostsAPI.d.ts +6 -6
  9. package/build/@types/common/api/assisted-service/InfraEnvsAPI.d.ts +7 -7
  10. package/build/@types/common/api/assisted-service/ManagedDomainsAPI.d.ts +1 -1
  11. package/build/@types/common/api/assisted-service/NewFeatureSupportLevelsAPI.d.ts +2 -2
  12. package/build/@types/common/api/assisted-service/OperatorsAPI.d.ts +1 -1
  13. package/build/@types/common/api/assisted-service/SupportedOpenshiftVersionsAPI.d.ts +1 -1
  14. package/build/@types/common/api/assisted-service/UISettingsAPI.d.ts +2 -2
  15. package/build/@types/common/components/clusterConfiguration/ControlPlaneNodesDropdown.d.ts +2 -1
  16. package/build/@types/common/components/clusterConfiguration/ControlPlaneNodesDropdown.d.ts.map +1 -1
  17. package/build/@types/common/components/clusterConfiguration/DownloadIso.d.ts.map +1 -1
  18. package/build/@types/common/components/clusterConfiguration/OcmTNADisclaimer.d.ts.map +1 -0
  19. package/build/@types/common/components/clusterConfiguration/utils.d.ts +3 -1
  20. package/build/@types/common/components/clusterConfiguration/utils.d.ts.map +1 -1
  21. package/build/@types/common/components/clusterConfiguration/utils.test.d.ts +2 -0
  22. package/build/@types/common/components/clusterConfiguration/utils.test.d.ts.map +1 -0
  23. package/build/@types/common/components/ui/formik/validationSchemas.d.ts +1 -1
  24. package/build/@types/common/components/ui/formik/validationSchemas.d.ts.map +1 -1
  25. package/build/@types/common/config/constants.d.ts.map +1 -1
  26. package/build/@types/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.d.ts.map +1 -1
  27. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.d.ts.map +1 -1
  28. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.d.ts +2 -1
  29. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.d.ts.map +1 -1
  30. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.d.ts.map +1 -1
  31. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.d.ts.map +1 -1
  32. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.d.ts.map +1 -1
  33. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.d.ts.map +1 -1
  34. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.d.ts.map +1 -1
  35. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.d.ts +1 -1
  36. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.d.ts.map +1 -1
  37. package/build/@types/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.d.ts.map +1 -1
  38. package/build/@types/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.d.ts.map +1 -1
  39. package/build/@types/ocm/components/clusterDetail/ClusterProperties.d.ts.map +1 -1
  40. package/build/@types/ocm/components/clusterDetail/ClusterProperties.test.d.ts +2 -0
  41. package/build/@types/ocm/components/clusterDetail/ClusterProperties.test.d.ts.map +1 -0
  42. package/build/@types/ocm/services/ClustersService.d.ts +5 -5
  43. package/build/@types/ocm/services/HostsService.d.ts +11 -11
  44. package/build/@types/ocm/services/InfraEnvsService.d.ts +2 -2
  45. package/build/@types/ocm/services/UISettingsService.d.ts +1 -1
  46. package/build/@types/ocm/store/slices/infra-envs/thunks.d.ts +1 -1
  47. package/build/cjs/cim/components/ClusterDeployment/ClusterDetailsFormFields.js +9 -5
  48. package/build/cjs/cim/components/ClusterDeployment/ClusterDetailsFormFields.js.map +1 -1
  49. package/build/cjs/common/components/clusterConfiguration/ControlPlaneNodesDropdown.js +23 -6
  50. package/build/cjs/common/components/clusterConfiguration/ControlPlaneNodesDropdown.js.map +1 -1
  51. package/build/cjs/common/components/clusterConfiguration/DownloadIso.js +1 -2
  52. package/build/cjs/common/components/clusterConfiguration/DownloadIso.js.map +1 -1
  53. package/build/cjs/{ocm → common}/components/clusterConfiguration/OcmTNADisclaimer.js +1 -1
  54. package/build/cjs/common/components/clusterConfiguration/OcmTNADisclaimer.js.map +1 -0
  55. package/build/cjs/common/components/clusterConfiguration/utils.js +23 -12
  56. package/build/cjs/common/components/clusterConfiguration/utils.js.map +1 -1
  57. package/build/cjs/common/components/clusterConfiguration/utils.test.js +242 -0
  58. package/build/cjs/common/components/clusterConfiguration/utils.test.js.map +1 -0
  59. package/build/cjs/common/components/clusterWizard/clusterDetailsValidation.js +1 -1
  60. package/build/cjs/common/components/clusterWizard/clusterDetailsValidation.js.map +1 -1
  61. package/build/cjs/common/components/ui/formik/validationSchemas.js +13 -3
  62. package/build/cjs/common/components/ui/formik/validationSchemas.js.map +1 -1
  63. package/build/cjs/common/config/constants.js +5 -10
  64. package/build/cjs/common/config/constants.js.map +1 -1
  65. package/build/cjs/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.js +1 -1
  66. package/build/cjs/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.js.map +1 -1
  67. package/build/cjs/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.js +0 -1
  68. package/build/cjs/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.js.map +1 -1
  69. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js +55 -13
  70. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js.map +1 -1
  71. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js +49 -12
  72. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js.map +1 -1
  73. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.js +3 -3
  74. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.js.map +1 -1
  75. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.js +1 -1
  76. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.js.map +1 -1
  77. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js +67 -7
  78. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js.map +1 -1
  79. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js +28 -4
  80. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js.map +1 -1
  81. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.js +7 -16
  82. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.js.map +1 -1
  83. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js +8 -8
  84. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js.map +1 -1
  85. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js +26 -8
  86. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js.map +1 -1
  87. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.js +5 -5
  88. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.js.map +1 -1
  89. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js +2 -2
  90. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js.map +1 -1
  91. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.test.js +180 -0
  92. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.test.js.map +1 -0
  93. package/lib/cim/components/ClusterDeployment/ClusterDetailsFormFields.tsx +17 -35
  94. package/lib/cim/types/k8s/agent-cluster-install.ts +1 -0
  95. package/lib/common/components/clusterConfiguration/ControlPlaneNodesDropdown.tsx +61 -31
  96. package/lib/common/components/clusterConfiguration/DownloadIso.tsx +5 -2
  97. package/lib/{ocm → common}/components/clusterConfiguration/OcmTNADisclaimer.tsx +1 -1
  98. package/lib/common/components/clusterConfiguration/utils.test.ts +275 -0
  99. package/lib/common/components/clusterConfiguration/utils.ts +28 -9
  100. package/lib/common/components/clusterWizard/clusterDetailsValidation.ts +1 -1
  101. package/lib/common/components/ui/formik/_validationSchemas.test.ts +82 -0
  102. package/lib/common/components/ui/formik/validationSchemas.ts +16 -10
  103. package/lib/common/config/constants.ts +5 -11
  104. package/lib/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.tsx +1 -1
  105. package/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.tsx +1 -7
  106. package/lib/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.tsx +59 -13
  107. package/lib/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.tsx +98 -38
  108. package/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.tsx +3 -2
  109. package/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx +3 -2
  110. package/lib/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.tsx +78 -11
  111. package/lib/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.tsx +65 -8
  112. package/lib/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.tsx +4 -9
  113. package/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts +17 -6
  114. package/lib/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.tsx +34 -4
  115. package/lib/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.tsx +7 -9
  116. package/lib/ocm/components/clusterDetail/ClusterProperties.test.ts +215 -0
  117. package/lib/ocm/components/clusterDetail/ClusterProperties.tsx +2 -2
  118. package/package.json +3 -3
  119. package/build/@types/ocm/components/clusterConfiguration/OcmTNADisclaimer.d.ts.map +0 -1
  120. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.d.ts +0 -6
  121. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.d.ts.map +0 -1
  122. package/build/cjs/ocm/components/clusterConfiguration/OcmTNADisclaimer.js.map +0 -1
  123. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.js +0 -25
  124. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.js.map +0 -1
  125. package/lib/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.tsx +0 -30
  126. /package/build/@types/{ocm → common}/components/clusterConfiguration/OcmTNADisclaimer.d.ts +0 -0
@@ -0,0 +1,275 @@
1
+ import { test, describe, expect } from 'vitest';
2
+ import { isDualStack } from './utils';
3
+ import {
4
+ MachineNetwork,
5
+ ClusterNetwork,
6
+ ServiceNetwork,
7
+ } from '@openshift-assisted/types/assisted-installer-service';
8
+
9
+ describe('isDualStack', () => {
10
+ const createMachineNetwork = (cidr: string): MachineNetwork => ({ cidr, clusterId: 'test' });
11
+ const createClusterNetwork = (cidr: string, hostPrefix: number): ClusterNetwork => ({
12
+ cidr,
13
+ hostPrefix,
14
+ clusterId: 'test',
15
+ });
16
+ const createServiceNetwork = (cidr: string): ServiceNetwork => ({ cidr, clusterId: 'test' });
17
+
18
+ describe('OCP < 4.12 (legacy behavior)', () => {
19
+ test('returns true for valid dual stack with IPv4 primary', () => {
20
+ const result = isDualStack({
21
+ machineNetworks: [
22
+ createMachineNetwork('192.168.1.0/24'),
23
+ createMachineNetwork('2001:db8::/64'),
24
+ ],
25
+ clusterNetworks: [
26
+ createClusterNetwork('10.128.0.0/14', 23),
27
+ createClusterNetwork('fd01::/48', 64),
28
+ ],
29
+ serviceNetworks: [
30
+ createServiceNetwork('172.30.0.0/16'),
31
+ createServiceNetwork('fd02::/112'),
32
+ ],
33
+ openshiftVersion: '4.11',
34
+ });
35
+
36
+ expect(result).toBe(true);
37
+ });
38
+
39
+ test('returns false for IPv6 primary in OCP < 4.12', () => {
40
+ const result = isDualStack({
41
+ machineNetworks: [
42
+ createMachineNetwork('2001:db8::/64'),
43
+ createMachineNetwork('192.168.1.0/24'),
44
+ ],
45
+ clusterNetworks: [
46
+ createClusterNetwork('fd01::/48', 64),
47
+ createClusterNetwork('10.128.0.0/14', 23),
48
+ ],
49
+ serviceNetworks: [
50
+ createServiceNetwork('fd02::/112'),
51
+ createServiceNetwork('172.30.0.0/16'),
52
+ ],
53
+ openshiftVersion: '4.11',
54
+ });
55
+
56
+ expect(result).toBe(false);
57
+ });
58
+
59
+ test('returns false for single stack', () => {
60
+ const result = isDualStack({
61
+ machineNetworks: [createMachineNetwork('192.168.1.0/24')],
62
+ clusterNetworks: [createClusterNetwork('10.128.0.0/14', 23)],
63
+ serviceNetworks: [createServiceNetwork('172.30.0.0/16')],
64
+ openshiftVersion: '4.11',
65
+ });
66
+
67
+ expect(result).toBe(false);
68
+ });
69
+
70
+ test('returns false for empty networks', () => {
71
+ const result = isDualStack({
72
+ machineNetworks: [],
73
+ clusterNetworks: [],
74
+ serviceNetworks: [],
75
+ openshiftVersion: '4.11',
76
+ });
77
+
78
+ expect(result).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('OCP >= 4.12 (new behavior)', () => {
83
+ test('returns true for IPv4 primary and IPv6 secondary', () => {
84
+ const result = isDualStack({
85
+ machineNetworks: [
86
+ createMachineNetwork('192.168.1.0/24'),
87
+ createMachineNetwork('2001:db8::/64'),
88
+ ],
89
+ clusterNetworks: [
90
+ createClusterNetwork('10.128.0.0/14', 23),
91
+ createClusterNetwork('fd01::/48', 64),
92
+ ],
93
+ serviceNetworks: [
94
+ createServiceNetwork('172.30.0.0/16'),
95
+ createServiceNetwork('fd02::/112'),
96
+ ],
97
+ openshiftVersion: '4.12',
98
+ });
99
+
100
+ expect(result).toBe(true);
101
+ });
102
+
103
+ test('returns true for IPv6 primary and IPv4 secondary', () => {
104
+ const result = isDualStack({
105
+ machineNetworks: [
106
+ createMachineNetwork('2001:db8::/64'),
107
+ createMachineNetwork('192.168.1.0/24'),
108
+ ],
109
+ clusterNetworks: [
110
+ createClusterNetwork('fd01::/48', 64),
111
+ createClusterNetwork('10.128.0.0/14', 23),
112
+ ],
113
+ serviceNetworks: [
114
+ createServiceNetwork('fd02::/112'),
115
+ createServiceNetwork('172.30.0.0/16'),
116
+ ],
117
+ openshiftVersion: '4.12',
118
+ });
119
+
120
+ expect(result).toBe(true);
121
+ });
122
+
123
+ test('returns false for IPv6 primary and IPv6 secondary (not dual-stack)', () => {
124
+ const result = isDualStack({
125
+ machineNetworks: [
126
+ createMachineNetwork('2001:db8::/64'),
127
+ createMachineNetwork('2001:db9::/64'),
128
+ ],
129
+ clusterNetworks: [
130
+ createClusterNetwork('fd01::/48', 64),
131
+ createClusterNetwork('fd02::/48', 64),
132
+ ],
133
+ serviceNetworks: [createServiceNetwork('fd03::/112'), createServiceNetwork('fd04::/112')],
134
+ openshiftVersion: '4.12',
135
+ });
136
+
137
+ expect(result).toBe(false);
138
+ });
139
+
140
+ test('returns false for IPv4 primary and IPv4 secondary (not dual-stack)', () => {
141
+ const result = isDualStack({
142
+ machineNetworks: [
143
+ createMachineNetwork('192.168.1.0/24'),
144
+ createMachineNetwork('10.0.0.0/24'),
145
+ ],
146
+ clusterNetworks: [
147
+ createClusterNetwork('10.128.0.0/14', 23),
148
+ createClusterNetwork('172.16.0.0/14', 23),
149
+ ],
150
+ serviceNetworks: [
151
+ createServiceNetwork('172.30.0.0/16'),
152
+ createServiceNetwork('172.31.0.0/16'),
153
+ ],
154
+ openshiftVersion: '4.12',
155
+ });
156
+
157
+ expect(result).toBe(false);
158
+ });
159
+
160
+ test('returns false for single stack', () => {
161
+ const result = isDualStack({
162
+ machineNetworks: [createMachineNetwork('192.168.1.0/24')],
163
+ clusterNetworks: [createClusterNetwork('10.128.0.0/14', 23)],
164
+ serviceNetworks: [createServiceNetwork('172.30.0.0/16')],
165
+ openshiftVersion: '4.12',
166
+ });
167
+
168
+ expect(result).toBe(false);
169
+ });
170
+
171
+ test('returns false if any network type is not dual stack', () => {
172
+ const result = isDualStack({
173
+ machineNetworks: [
174
+ createMachineNetwork('192.168.1.0/24'),
175
+ createMachineNetwork('2001:db8::/64'),
176
+ ],
177
+ clusterNetworks: [createClusterNetwork('10.128.0.0/14', 23)], // Single stack
178
+ serviceNetworks: [
179
+ createServiceNetwork('172.30.0.0/16'),
180
+ createServiceNetwork('fd02::/112'),
181
+ ],
182
+ openshiftVersion: '4.12',
183
+ });
184
+
185
+ expect(result).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe('no version specified (defaults to legacy behavior)', () => {
190
+ test('returns true for IPv4 primary and IPv6 secondary', () => {
191
+ const result = isDualStack({
192
+ machineNetworks: [
193
+ createMachineNetwork('192.168.1.0/24'),
194
+ createMachineNetwork('2001:db8::/64'),
195
+ ],
196
+ clusterNetworks: [
197
+ createClusterNetwork('10.128.0.0/14', 23),
198
+ createClusterNetwork('fd01::/48', 64),
199
+ ],
200
+ serviceNetworks: [
201
+ createServiceNetwork('172.30.0.0/16'),
202
+ createServiceNetwork('fd02::/112'),
203
+ ],
204
+ });
205
+
206
+ expect(result).toBe(true);
207
+ });
208
+
209
+ test('returns false for IPv6 primary when no version specified', () => {
210
+ const result = isDualStack({
211
+ machineNetworks: [
212
+ createMachineNetwork('2001:db8::/64'),
213
+ createMachineNetwork('192.168.1.0/24'),
214
+ ],
215
+ clusterNetworks: [
216
+ createClusterNetwork('fd01::/48', 64),
217
+ createClusterNetwork('10.128.0.0/14', 23),
218
+ ],
219
+ serviceNetworks: [
220
+ createServiceNetwork('fd02::/112'),
221
+ createServiceNetwork('172.30.0.0/16'),
222
+ ],
223
+ });
224
+
225
+ expect(result).toBe(false);
226
+ });
227
+ });
228
+
229
+ describe('edge cases', () => {
230
+ test('handles undefined networks', () => {
231
+ const result = isDualStack({
232
+ machineNetworks: undefined,
233
+ clusterNetworks: undefined,
234
+ serviceNetworks: undefined,
235
+ openshiftVersion: '4.12',
236
+ });
237
+
238
+ expect(result).toBe(false);
239
+ });
240
+
241
+ test('handles networks with empty CIDRs', () => {
242
+ const result = isDualStack({
243
+ machineNetworks: [
244
+ { cidr: '', clusterId: 'test' },
245
+ { cidr: '', clusterId: 'test' },
246
+ ],
247
+ clusterNetworks: [
248
+ { cidr: '', hostPrefix: 23, clusterId: 'test' },
249
+ { cidr: '', hostPrefix: 64, clusterId: 'test' },
250
+ ],
251
+ serviceNetworks: [
252
+ { cidr: '', clusterId: 'test' },
253
+ { cidr: '', clusterId: 'test' },
254
+ ],
255
+ openshiftVersion: '4.12',
256
+ });
257
+
258
+ expect(result).toBe(false);
259
+ });
260
+
261
+ test('handles invalid CIDRs', () => {
262
+ const result = isDualStack({
263
+ machineNetworks: [createMachineNetwork('invalid'), createMachineNetwork('also-invalid')],
264
+ clusterNetworks: [
265
+ createClusterNetwork('invalid', 23),
266
+ createClusterNetwork('also-invalid', 64),
267
+ ],
268
+ serviceNetworks: [createServiceNetwork('invalid'), createServiceNetwork('also-invalid')],
269
+ openshiftVersion: '4.12',
270
+ });
271
+
272
+ expect(result).toBe(false);
273
+ });
274
+ });
275
+ });
@@ -10,6 +10,7 @@ import {
10
10
  ServiceNetwork,
11
11
  } from '@openshift-assisted/types/assisted-installer-service';
12
12
  import { NO_SUBNET_SET } from '../../config';
13
+ import { isMajorMinorVersionEqualOrGreater } from '../../utils';
13
14
  import {
14
15
  selectClusterNetworkCIDR,
15
16
  selectClusterNetworkHostPrefix,
@@ -179,20 +180,38 @@ export const canBeDualStack = (subnets: HostSubnets) =>
179
180
 
180
181
  const areNetworksDualStack = (
181
182
  networks: (MachineNetwork | ClusterNetwork | ServiceNetwork)[] | undefined,
182
- ) =>
183
- networks &&
184
- networks.length > 1 &&
185
- Address4.isValid(networks[0].cidr || '') &&
186
- Address6.isValid(networks[1].cidr || '');
183
+ openshiftVersion?: string,
184
+ ) => {
185
+ if (!networks || networks.length < 2) {
186
+ return false;
187
+ }
188
+
189
+ const firstCidr = networks[0].cidr || '';
190
+ const secondCidr = networks[1].cidr || '';
191
+
192
+ // For OCP >= 4.12, allow either IPv4 or IPv6 as primary, with opposite as secondary
193
+ if (openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')) {
194
+ return (
195
+ (Address4.isValid(firstCidr) && Address6.isValid(secondCidr)) ||
196
+ (Address6.isValid(firstCidr) && Address4.isValid(secondCidr))
197
+ );
198
+ }
199
+
200
+ // For older versions, require IPv4 as primary and IPv6 as secondary
201
+ return Address4.isValid(firstCidr) && Address6.isValid(secondCidr);
202
+ };
187
203
 
188
204
  export const isDualStack = ({
189
205
  machineNetworks,
190
206
  clusterNetworks,
191
207
  serviceNetworks,
192
- }: Pick<Cluster, 'machineNetworks' | 'clusterNetworks' | 'serviceNetworks'>) =>
193
- areNetworksDualStack(machineNetworks) &&
194
- areNetworksDualStack(clusterNetworks) &&
195
- areNetworksDualStack(serviceNetworks);
208
+ openshiftVersion,
209
+ }: Pick<Cluster, 'machineNetworks' | 'clusterNetworks' | 'serviceNetworks'> & {
210
+ openshiftVersion?: string;
211
+ }) =>
212
+ areNetworksDualStack(machineNetworks, openshiftVersion) &&
213
+ areNetworksDualStack(clusterNetworks, openshiftVersion) &&
214
+ areNetworksDualStack(serviceNetworks, openshiftVersion);
196
215
 
197
216
  export const isSubnetInIPv6 = ({
198
217
  clusterNetworkCidr,
@@ -75,7 +75,7 @@ export const getClusterDetailsInitialValues = ({
75
75
  diskEncryptionTangServers: parseTangServers(cluster?.diskEncryption?.tangServers),
76
76
  diskEncryption: cluster?.diskEncryption ?? {},
77
77
  cpuArchitecture: cluster?.cpuArchitecture || getDefaultCpuArchitecture(),
78
- platform: cluster?.platform?.type || 'none',
78
+ platform: cluster?.platform?.type || 'baremetal',
79
79
  customOpenshiftSelect: null,
80
80
  userManagedNetworking: cluster?.userManagedNetworking || false,
81
81
  enableDiskEncryptionOnArbiters: ['all', 'arbiters'].includes(
@@ -2,6 +2,7 @@ import { test, describe, expect } from 'vitest';
2
2
  import {
3
3
  baseDomainValidationSchema,
4
4
  dnsNameValidationSchema,
5
+ dualStackValidationSchema,
5
6
  hostPrefixValidationSchema,
6
7
  ipBlockValidationSchema,
7
8
  ipValidationSchema,
@@ -347,4 +348,85 @@ describe('validationSchemas', () => {
347
348
 
348
349
  expect(counter).toBe(invalid.length);
349
350
  });
351
+
352
+ describe('dualStackValidationSchema', () => {
353
+ describe('OCP < 4.12 (legacy behavior)', () => {
354
+ const schema = dualStackValidationSchema('machine networks', '4.11');
355
+
356
+ test('validates IPv4 as primary and IPv6 as secondary', async () => {
357
+ const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
358
+
359
+ for (const value of validValues) {
360
+ await expect(schema.validate(value)).resolves.toBe(value);
361
+ }
362
+ });
363
+
364
+ test('rejects IPv6 as primary for OCP < 4.12', async () => {
365
+ const invalidValues = [[{ cidr: '2001:db8::/64' }, { cidr: '192.168.1.0/24' }]];
366
+
367
+ for (const value of invalidValues) {
368
+ await expect(schema.validate(value)).rejects.toThrow(
369
+ 'First network has to be IPv4 subnet',
370
+ );
371
+ }
372
+ });
373
+ });
374
+
375
+ describe('OCP >= 4.12 (new behavior)', () => {
376
+ const schema = dualStackValidationSchema('machine networks', '4.12');
377
+
378
+ test('validates IPv4 as primary and IPv6 as secondary', async () => {
379
+ const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
380
+
381
+ for (const value of validValues) {
382
+ await expect(schema.validate(value)).resolves.toBe(value);
383
+ }
384
+ });
385
+
386
+ test('validates IPv6 as primary and IPv4 as secondary', async () => {
387
+ const validValues = [[{ cidr: '2001:db8::/64' }, { cidr: '192.168.1.0/24' }]];
388
+
389
+ for (const value of validValues) {
390
+ await expect(schema.validate(value)).resolves.toBe(value);
391
+ }
392
+ });
393
+ });
394
+
395
+ describe('no version specified (defaults to legacy behavior)', () => {
396
+ const schema = dualStackValidationSchema('machine networks');
397
+
398
+ test('validates IPv4 as primary and IPv6 as secondary', async () => {
399
+ const validValues = [[{ cidr: '192.168.1.0/24' }, { cidr: '2001:db8::/64' }]];
400
+
401
+ for (const value of validValues) {
402
+ await expect(schema.validate(value)).resolves.toBe(value);
403
+ }
404
+ });
405
+
406
+ test('rejects IPv6 as primary when no version specified', async () => {
407
+ const invalidValues = [[{ cidr: '2001:db8::/64' }, { cidr: '192.168.1.0/24' }]];
408
+
409
+ for (const value of invalidValues) {
410
+ await expect(schema.validate(value)).rejects.toThrow(
411
+ 'First network has to be IPv4 subnet',
412
+ );
413
+ }
414
+ });
415
+ });
416
+
417
+ describe('edge cases', () => {
418
+ test('validates maximum 2 networks', async () => {
419
+ const schema = dualStackValidationSchema('machine networks', '4.12');
420
+ const invalidValue = [
421
+ { cidr: '192.168.1.0/24' },
422
+ { cidr: '2001:db8::/64' },
423
+ { cidr: '10.0.0.0/24' },
424
+ ];
425
+
426
+ await expect(schema.validate(invalidValue)).rejects.toThrow(
427
+ 'Maximum number of machine networks subnets in dual stack is 2',
428
+ );
429
+ });
430
+ });
431
+ });
350
432
  });
@@ -28,6 +28,7 @@ import {
28
28
  nameValidationMessages,
29
29
  } from './constants';
30
30
  import { allSubnetsIPv4, getAddress, trimCommaSeparatedList, trimSshPublicKey } from './utils';
31
+ import { isMajorMinorVersionEqualOrGreater } from '../../../utils';
31
32
  import { selectApiVip, selectIngressVip } from '../../../selectors';
32
33
  import { ClusterDetailsValues } from '../../clusterWizard/types';
33
34
 
@@ -703,20 +704,25 @@ export const serviceNetworkValidationSchema = Yup.array().of(
703
704
  }),
704
705
  );
705
706
 
706
- export const dualStackValidationSchema = (field: string) =>
707
+ export const dualStackValidationSchema = (field: string, openshiftVersion?: string) =>
707
708
  Yup.array()
708
709
  .max(2, `Maximum number of ${field} subnets in dual stack is 2.`)
709
710
  .test(
710
711
  'dual-stack-ipv4',
711
- 'First network has to be IPv4 subnet.',
712
- (values?: { cidr: MachineNetwork['cidr'] }[]): boolean =>
713
- !!values?.[0].cidr && Address4.isValid(values[0].cidr),
714
- )
715
- .test(
716
- 'dual-stack-ipv6',
717
- 'Second network has to be IPv6 subnet.',
718
- (values?: { cidr: MachineNetwork['cidr'] }[]): boolean =>
719
- !!values?.[1].cidr && Address6.isValid(values[1].cidr),
712
+ openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')
713
+ ? 'First network has to be a valid IPv4 or IPv6 subnet.'
714
+ : 'First network has to be IPv4 subnet.',
715
+ (values?: { cidr: MachineNetwork['cidr'] }[]): boolean => {
716
+ // For OCP versions > 4.11, allow IPv6 as primary network
717
+ if (openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12')) {
718
+ return (
719
+ !!values?.[0].cidr &&
720
+ (Address4.isValid(values[0].cidr) || Address6.isValid(values[0].cidr))
721
+ );
722
+ }
723
+ // For older versions, require IPv4 as primary network
724
+ return !!values?.[0].cidr && Address4.isValid(values[0].cidr);
725
+ },
720
726
  );
721
727
 
722
728
  export const IPv4ValidationSchema = Yup.array().test(
@@ -5,12 +5,10 @@ import {
5
5
  ClusterValidationId,
6
6
  DiskRole,
7
7
  Event,
8
- HostRoleUpdateParams,
9
8
  HostValidationId,
10
9
  } from '@openshift-assisted/types/assisted-installer-service';
11
10
  import { ValidationGroup as ClusterValidationGroup } from '../types/clusters';
12
11
  import buildManifest from '@openshift-assisted/ui-lib/package.json';
13
- import { isInOcm } from '../api/axiosClient';
14
12
 
15
13
  export const DEFAULT_POLLING_INTERVAL = 10 * 1000;
16
14
  export const REDUCED_POLLING_INTERVAL = 10 * 1000 * 6;
@@ -35,15 +33,11 @@ export const hostRoles = (t: TFunction): HostRole[] => [
35
33
  'ai:Runs application workloads. Connect at least 5 hosts to enable dedicated workers.',
36
34
  ),
37
35
  },
38
- ...(isInOcm
39
- ? [
40
- {
41
- value: 'arbiter' as HostRoleUpdateParams,
42
- label: t('ai:Arbiter'),
43
- description: t('ai:Prevents split-brain scenarios and maintains quorum.'),
44
- },
45
- ]
46
- : []),
36
+ {
37
+ value: 'arbiter',
38
+ label: t('ai:Arbiter'),
39
+ description: t('ai:Prevents split-brain scenarios and maintains quorum.'),
40
+ },
47
41
  ];
48
42
 
49
43
  export const clusterStatusLabels = (t: TFunction): { [key in Cluster['status']]: string } => ({
@@ -17,7 +17,7 @@ import {
17
17
  useNewFeatureSupportLevel,
18
18
  } from '../../../common/components/newFeatureSupportLevels';
19
19
  import { isFeatureSupportedAndAvailable } from '../featureSupportLevels/featureStateUtils';
20
- import OcmTNADisclaimer from './OcmTNADisclaimer';
20
+ import OcmTNADisclaimer from '../../../common/components/clusterConfiguration/OcmTNADisclaimer';
21
21
  import OcmSNODisclaimer from './OcmSNODisclaimer';
22
22
 
23
23
  const INPUT_NAME = 'controlPlaneCount';
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Language } from '@patternfly/react-code-editor';
3
- import { ExpandableSection, Grid, GridItem, TextInput } from '@patternfly/react-core';
3
+ import { ExpandableSection, Grid, GridItem } from '@patternfly/react-core';
4
4
  import { useField } from 'formik';
5
5
  import { OcmInputField, OcmCodeField } from '../../../ui/OcmFormFields';
6
6
  import { CustomManifestValues } from '../data/dataTypes';
@@ -30,12 +30,6 @@ const ExpandedManifest = ({ fieldName, manifestIdx }: CustomManifestComponentPro
30
30
  >
31
31
  <Grid hasGutter span={12}>
32
32
  <GridItem span={6}>
33
- <TextInput
34
- hidden={true}
35
- name={`${fieldName}.fakeId`}
36
- data-testid={`fakeId-${manifestIdx}`}
37
- id={`fakeId-${manifestIdx}`}
38
- />
39
33
  <FolderDropdown name={`${fieldName}.folder`} data-testid={`folder-${manifestIdx}`} />
40
34
  </GridItem>
41
35
  <GridItem span={6}>
@@ -8,6 +8,7 @@ import {
8
8
  Grid,
9
9
  } from '@patternfly/react-core';
10
10
  import { FieldArray, useFormikContext } from 'formik';
11
+ import { Address6 } from 'ip-address';
11
12
  import {
12
13
  DUAL_STACK,
13
14
  PREFIX_MAX_RESTRICTION,
@@ -16,10 +17,6 @@ import {
16
17
  } from '../../../../common';
17
18
  import { OcmInputField } from '../../ui/OcmFormFields';
18
19
 
19
- const getNetworkLabelSuffix = (index: number, isDualStack: boolean) => {
20
- return isDualStack ? ` (${index === 0 ? 'IPv4' : 'IPv6'})` : '';
21
- };
22
-
23
20
  const IPv4PrefixPopoverText =
24
21
  'For example, if Cluster Network Host Prefix is set to 23, then each node is assigned a /23 subnet out of the given cidr (clusterNetworkCIDR), which allows for 510 (2^(32 - 23) - 2) pod IPs addresses.';
25
22
 
@@ -36,10 +33,62 @@ const serviceCidrHelperText =
36
33
  'Enter only 1 IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.';
37
34
 
38
35
  const AdvancedNetworkFields = () => {
39
- const { values, errors } = useFormikContext<NetworkConfigurationValues>();
36
+ const { values, errors, setFieldValue } = useFormikContext<NetworkConfigurationValues>();
40
37
 
41
38
  const isDualStack = values.stackType === DUAL_STACK;
42
39
 
40
+ // Reorder Cluster Networks and Service Networks when Machine Network primary changes
41
+ React.useEffect(() => {
42
+ if (!isDualStack || !values.machineNetworks?.[0]?.cidr) return;
43
+
44
+ const primaryMachineNetworkCidr = values.machineNetworks[0].cidr;
45
+ const isPrimaryIPv6 = Address6.isValid(primaryMachineNetworkCidr);
46
+
47
+ // Check Cluster Networks
48
+ if (values.clusterNetworks && values.clusterNetworks.length >= 2) {
49
+ const firstClusterCidr = values.clusterNetworks[0].cidr;
50
+ if (!firstClusterCidr) return;
51
+
52
+ const isFirstClusterIPv6 = Address6.isValid(firstClusterCidr);
53
+
54
+ // If primary machine is IPv6 but first cluster is IPv4, swap them
55
+ if (isPrimaryIPv6 && !isFirstClusterIPv6) {
56
+ const reordered = [values.clusterNetworks[1], values.clusterNetworks[0]];
57
+ setFieldValue('clusterNetworks', reordered, false);
58
+ }
59
+ // If primary machine is IPv4 but first cluster is IPv6, swap them
60
+ else if (!isPrimaryIPv6 && isFirstClusterIPv6) {
61
+ const reordered = [values.clusterNetworks[1], values.clusterNetworks[0]];
62
+ setFieldValue('clusterNetworks', reordered, false);
63
+ }
64
+ }
65
+
66
+ // Check Service Networks
67
+ if (values.serviceNetworks && values.serviceNetworks.length >= 2) {
68
+ const firstServiceCidr = values.serviceNetworks[0].cidr;
69
+ if (!firstServiceCidr) return;
70
+
71
+ const isFirstServiceIPv6 = Address6.isValid(firstServiceCidr);
72
+
73
+ // If primary machine is IPv6 but first service is IPv4, swap them
74
+ if (isPrimaryIPv6 && !isFirstServiceIPv6) {
75
+ const reordered = [values.serviceNetworks[1], values.serviceNetworks[0]];
76
+ setFieldValue('serviceNetworks', reordered, false);
77
+ }
78
+ // If primary machine is IPv4 but first service is IPv6, swap them
79
+ else if (!isPrimaryIPv6 && isFirstServiceIPv6) {
80
+ const reordered = [values.serviceNetworks[1], values.serviceNetworks[0]];
81
+ setFieldValue('serviceNetworks', reordered, false);
82
+ }
83
+ }
84
+ }, [
85
+ isDualStack,
86
+ values.machineNetworks,
87
+ values.clusterNetworks,
88
+ values.serviceNetworks,
89
+ setFieldValue,
90
+ ]);
91
+
43
92
  const clusterNetworkCidrPrefix = (index: number) =>
44
93
  parseInt(
45
94
  ((values.clusterNetworks && values.clusterNetworks[index].cidr) || '').split('/')[1],
@@ -66,7 +115,6 @@ const AdvancedNetworkFields = () => {
66
115
  {() => (
67
116
  <FormGroup fieldId="clusterNetworks" labelInfo={isDualStack && 'Primary'}>
68
117
  {values.clusterNetworks?.map((_, index) => {
69
- const networkSuffix = getNetworkLabelSuffix(index, isDualStack);
70
118
  return (
71
119
  <Grid key={index} className="pf-v6-u-pb-md">
72
120
  <GridItem className={'network-field-group'}>
@@ -74,7 +122,7 @@ const AdvancedNetworkFields = () => {
74
122
  name={`clusterNetworks.${index}.cidr`}
75
123
  label={
76
124
  <>
77
- <span>{`Cluster network CIDR${networkSuffix} `}</span>
125
+ <span>Cluster network CIDR </span>
78
126
  <PopoverIcon
79
127
  bodyContent={'IP address blocks from which Pod IPs are allocated.'}
80
128
  />
@@ -82,7 +130,7 @@ const AdvancedNetworkFields = () => {
82
130
  }
83
131
  helperText={clusterCidrHelperText}
84
132
  isRequired
85
- labelInfo={index === 0 && isDualStack ? 'Primary' : ''}
133
+ labelInfo={isDualStack ? (index === 0 ? 'Primary' : 'Secondary') : ''}
86
134
  />
87
135
  </GridItem>
88
136
  <GridItem className={'network-field-group'}>
@@ -90,7 +138,7 @@ const AdvancedNetworkFields = () => {
90
138
  name={`clusterNetworks.${index}.hostPrefix`}
91
139
  label={
92
140
  <>
93
- <span>Cluster network host prefix{networkSuffix} </span>
141
+ <span>Cluster network host prefix </span>
94
142
  <PopoverIcon
95
143
  bodyContent={clusterNetworkHostPrefixPopoverText(index)}
96
144
  minWidth="30rem"
@@ -128,9 +176,7 @@ const AdvancedNetworkFields = () => {
128
176
  name={`serviceNetworks.${index}.cidr`}
129
177
  label={
130
178
  <>
131
- <span>
132
- {`Service network CIDR${getNetworkLabelSuffix(index, isDualStack)} `}
133
- </span>
179
+ <span>Service network CIDR </span>
134
180
  <PopoverIcon
135
181
  bodyContent={'The IP address pool used for service IP addresses.'}
136
182
  />
@@ -138,7 +184,7 @@ const AdvancedNetworkFields = () => {
138
184
  }
139
185
  helperText={serviceCidrHelperText}
140
186
  isRequired
141
- labelInfo={index === 0 && isDualStack ? 'Primary' : ''}
187
+ labelInfo={isDualStack ? (index === 0 ? 'Primary' : 'Secondary') : ''}
142
188
  />
143
189
  </GridItem>
144
190
  ))}