@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
@@ -9,6 +9,7 @@ import {
9
9
  NetworkConfigurationValues,
10
10
  DUAL_STACK,
11
11
  NO_SUBNET_SET,
12
+ isMajorMinorVersionEqualOrGreater,
12
13
  } from '../../../../common';
13
14
  import { selectCurrentClusterPermissionsState } from '../../../store/slices/current-cluster/selectors';
14
15
  import { SubnetsDropdown } from './SubnetsDropdown';
@@ -35,6 +36,7 @@ export interface AvailableSubnetsControlProps {
35
36
  hostSubnets: HostSubnet[];
36
37
  isRequired: boolean;
37
38
  isDisabled: boolean;
39
+ openshiftVersion?: string;
38
40
  }
39
41
 
40
42
  export const AvailableSubnetsControl = ({
@@ -42,11 +44,16 @@ export const AvailableSubnetsControl = ({
42
44
  hostSubnets,
43
45
  isRequired = false,
44
46
  isDisabled,
47
+ openshiftVersion,
45
48
  }: AvailableSubnetsControlProps) => {
46
49
  const { values, errors, setFieldValue } = useFormikContext<NetworkConfigurationValues>();
47
50
  const isDualStack = values.stackType === DUAL_STACK;
48
51
  const { isViewerMode } = useSelector(selectCurrentClusterPermissionsState);
49
52
 
53
+ // Check if OCP version supports IPv6 as primary network
54
+ const supportsIPv6Primary =
55
+ openshiftVersion && isMajorMinorVersionEqualOrGreater(openshiftVersion, '4.12');
56
+
50
57
  const IPv4Subnets = hostSubnets
51
58
  .filter((subnet) => Address4.isValid(subnet.subnet))
52
59
  .sort(subnetSort);
@@ -54,53 +61,106 @@ export const AvailableSubnetsControl = ({
54
61
  .filter((subnet) => Address6.isValid(subnet.subnet))
55
62
  .sort(subnetSort);
56
63
 
64
+ // For OCP >= 4.12, both networks can use either IPv4 or IPv6
65
+ const allSubnets = supportsIPv6Primary
66
+ ? [...IPv4Subnets, ...IPv6Subnets].sort(subnetSort)
67
+ : IPv4Subnets;
68
+
69
+ // For auto-selection, always use IPv4 subnets for single-stack
57
70
  const cidr = IPv4Subnets.length >= 1 ? IPv4Subnets[0].subnet : NO_SUBNET_SET;
58
71
  const hasEmptySelection = (values.machineNetworks ?? []).length === 0;
59
72
  const autoSelectNetwork = !isViewerMode && hasEmptySelection;
60
73
  useAutoSelectSingleAvailableSubnet(autoSelectNetwork, setFieldValue, cidr, clusterId);
61
74
 
62
75
  return (
63
- <FormGroup
64
- label="Machine network"
65
- labelInfo={isDualStack && 'Primary'}
66
- fieldId="machine-networks"
67
- isRequired={isRequired}
68
- >
69
- <FieldArray name="machineNetworks">
70
- {() => (
71
- <Stack>
72
- {isDualStack ? (
73
- values.machineNetworks?.map((_machineNetwork, index) => {
74
- const machineSubnets = index === 1 ? IPv6Subnets : IPv4Subnets;
75
-
76
- return (
77
- <StackItem key={index}>
78
- <SubnetsDropdown
79
- name={`machineNetworks.${index}.cidr`}
80
- machineSubnets={machineSubnets}
81
- isDisabled={isDisabled}
82
- data-testid={`subnets-dropdown-toggle-${index ? 'ipv6' : 'ipv4'}`}
83
- />
84
- </StackItem>
85
- );
86
- })
87
- ) : (
88
- <StackItem>
76
+ <>
77
+ <FormGroup
78
+ label="Machine network"
79
+ labelInfo={isDualStack && 'Primary'}
80
+ fieldId="machine-networks"
81
+ isRequired={isRequired}
82
+ >
83
+ <FieldArray name="machineNetworks">
84
+ {() => (
85
+ <Stack>
86
+ {isDualStack ? (
87
+ values.machineNetworks?.map((machineNetwork, index) => {
88
+ if (index > 0) return null;
89
+
90
+ const machineSubnets = supportsIPv6Primary ? allSubnets : IPv4Subnets;
91
+
92
+ return (
93
+ <StackItem key={index}>
94
+ <SubnetsDropdown
95
+ name={`machineNetworks.${index}.cidr`}
96
+ machineSubnets={machineSubnets}
97
+ isDisabled={isDisabled}
98
+ data-testid={`subnets-dropdown-toggle-${index ? 'ipv6' : 'ipv4'}`}
99
+ />
100
+ </StackItem>
101
+ );
102
+ })
103
+ ) : (
104
+ <StackItem>
105
+ <SubnetsDropdown
106
+ name={`machineNetworks.0.cidr`}
107
+ machineSubnets={IPv4Subnets}
108
+ isDisabled={isDisabled}
109
+ data-testid={'subnets-dropdown-toggle-primary'}
110
+ />
111
+ </StackItem>
112
+ )}
113
+ </Stack>
114
+ )}
115
+ </FieldArray>
116
+
117
+ {typeof errors.machineNetworks === 'string' && (
118
+ <Alert variant={AlertVariant.warning} title={errors.machineNetworks} isInline />
119
+ )}
120
+ </FormGroup>
121
+
122
+ {isDualStack && values.machineNetworks && values.machineNetworks.length > 1 && (
123
+ <FormGroup
124
+ label="Machine network"
125
+ labelInfo="Secondary"
126
+ fieldId="machine-networks-secondary"
127
+ isRequired={isRequired}
128
+ >
129
+ <FieldArray name="machineNetworks">
130
+ {() => {
131
+ const index = 1;
132
+ let machineSubnets;
133
+
134
+ if (supportsIPv6Primary) {
135
+ // For OCP >= 4.12, smart filtering based on the other network's selection
136
+ const firstNetworkCidr = values.machineNetworks?.[0]?.cidr;
137
+ if (firstNetworkCidr && Address6.isValid(firstNetworkCidr)) {
138
+ // If first is IPv6, second should be IPv4
139
+ machineSubnets = IPv4Subnets;
140
+ } else if (firstNetworkCidr && Address4.isValid(firstNetworkCidr)) {
141
+ // If first is IPv4, second should be IPv6
142
+ machineSubnets = IPv6Subnets;
143
+ } else {
144
+ // If first is not selected yet, show all subnets
145
+ machineSubnets = allSubnets;
146
+ }
147
+ } else {
148
+ // For older versions, second network is IPv6
149
+ machineSubnets = IPv6Subnets;
150
+ }
151
+
152
+ return (
89
153
  <SubnetsDropdown
90
- name={`machineNetworks.0.cidr`}
91
- machineSubnets={IPv4Subnets}
154
+ name={`machineNetworks.${index}.cidr`}
155
+ machineSubnets={machineSubnets}
92
156
  isDisabled={isDisabled}
93
- data-testid={'subnets-dropdown-toggle-ipv4'}
157
+ data-testid={`subnets-dropdown-toggle-${index ? 'ipv6' : 'ipv4'}`}
94
158
  />
95
- </StackItem>
96
- )}
97
- </Stack>
98
- )}
99
- </FieldArray>
100
-
101
- {typeof errors.machineNetworks === 'string' && (
102
- <Alert variant={AlertVariant.warning} title={errors.machineNetworks} isInline />
159
+ );
160
+ }}
161
+ </FieldArray>
162
+ </FormGroup>
103
163
  )}
104
- </FormGroup>
164
+ </>
105
165
  );
106
166
  };
@@ -224,10 +224,10 @@ const NetworkConfiguration = ({
224
224
  );
225
225
 
226
226
  React.useEffect(() => {
227
- if (!isViewerMode && isDualStack && !isUserManagedNetworking) {
227
+ if (!isViewerMode && isDualStack) {
228
228
  toggleAdvConfiguration(true);
229
229
  }
230
- }, [isDualStack, isUserManagedNetworking, isViewerMode, toggleAdvConfiguration]);
230
+ }, [isDualStack, isViewerMode, toggleAdvConfiguration]);
231
231
 
232
232
  const managedNetworkingState = React.useMemo(
233
233
  () =>
@@ -292,6 +292,7 @@ const NetworkConfiguration = ({
292
292
  hostSubnets.length === 0 ||
293
293
  false
294
294
  }
295
+ openshiftVersion={cluster.openshiftVersion}
295
296
  />
296
297
  </StackItem>
297
298
  )}
@@ -200,8 +200,9 @@ const NetworkConfigurationPage = ({ cluster }: { cluster: Cluster }) => {
200
200
  );
201
201
 
202
202
  const memoizedValidationSchema = React.useMemo(
203
- () => getNetworkConfigurationValidationSchema(initialValues, hostSubnets),
204
- [hostSubnets, initialValues],
203
+ () =>
204
+ getNetworkConfigurationValidationSchema(initialValues, hostSubnets, cluster.openshiftVersion),
205
+ [hostSubnets, initialValues, cluster.openshiftVersion],
205
206
  );
206
207
 
207
208
  React.useEffect(() => {
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { useSelector } from 'react-redux';
3
3
  import { ButtonVariant, FormGroup, Split, SplitItem, Tooltip } from '@patternfly/react-core';
4
4
  import { useFormikContext } from 'formik';
5
- import { Address6 } from 'ip-address';
5
+ import { Address4, Address6 } from 'ip-address';
6
6
 
7
7
  import {
8
8
  HostSubnets,
@@ -69,19 +69,67 @@ export const StackTypeControlGroup = ({
69
69
  setFieldValue('stackType', IPV4_STACK);
70
70
 
71
71
  if (values.machineNetworks && values.machineNetworks?.length >= 2) {
72
- setFieldValue('machineNetworks', values.machineNetworks.slice(0, 1));
72
+ // For single-stack IPv4, prefer IPv4 machine network
73
+ const firstNetwork = values.machineNetworks[0];
74
+ const isFirstIPv4 = firstNetwork?.cidr && Address4.isValid(firstNetwork.cidr);
75
+
76
+ if (isFirstIPv4) {
77
+ // Keep the first network if it's IPv4
78
+ setFieldValue('machineNetworks', [firstNetwork]);
79
+ } else {
80
+ // If first is IPv6, look for IPv4 network or set empty for auto-selection
81
+ const ipv4Network = values.machineNetworks.find(
82
+ (network) => network.cidr && Address4.isValid(network.cidr),
83
+ );
84
+ if (ipv4Network) {
85
+ setFieldValue('machineNetworks', [ipv4Network]);
86
+ } else {
87
+ // No IPv4 network found, set empty for auto-selection
88
+ setFieldValue('machineNetworks', []);
89
+ }
90
+ }
73
91
  }
74
- if (values.clusterNetworks && values.clusterNetworks?.length >= 2) {
75
- setFieldValue('clusterNetworks', values.clusterNetworks.slice(0, 1));
92
+ // Ensure single-stack inputs are IPv4 after switching from dual-stack
93
+ if (values.clusterNetworks && values.clusterNetworks.length >= 1) {
94
+ const defaultCluster =
95
+ defaultNetworkValues.clusterNetworksIpv4 && defaultNetworkValues.clusterNetworksIpv4[0];
96
+ const firstCluster = values.clusterNetworks[0];
97
+ const ipv4Cluster = defaultCluster
98
+ ? {
99
+ cidr: defaultCluster.cidr,
100
+ hostPrefix: defaultCluster.hostPrefix,
101
+ clusterId: firstCluster.clusterId, // ← Preserve clusterId from original network
102
+ }
103
+ : {
104
+ cidr: '',
105
+ hostPrefix: '',
106
+ clusterId: clusterId, // ← Use current clusterId
107
+ };
108
+ setFieldValue('clusterNetworks', [ipv4Cluster]);
76
109
  }
77
- if (values.serviceNetworks && values.serviceNetworks?.length >= 2) {
78
- setFieldValue('serviceNetworks', values.serviceNetworks.slice(0, 1));
110
+ if (values.serviceNetworks && values.serviceNetworks.length >= 1) {
111
+ const defaultService =
112
+ defaultNetworkValues.serviceNetworksIpv4 && defaultNetworkValues.serviceNetworksIpv4[0];
113
+ const firstService = values.serviceNetworks[0];
114
+ const ipv4Service = defaultService
115
+ ? {
116
+ cidr: defaultService.cidr,
117
+ clusterId: firstService.clusterId, // ← Preserve clusterId from original network
118
+ }
119
+ : {
120
+ cidr: '',
121
+ clusterId: clusterId, // ← Use current clusterId
122
+ };
123
+ setFieldValue('serviceNetworks', [ipv4Service]);
79
124
  }
80
125
 
81
126
  void validateForm();
82
127
  }, [
83
128
  setFieldValue,
84
129
  validateForm,
130
+ clusterId,
131
+ defaultNetworkValues.clusterNetworksIpv4,
132
+ defaultNetworkValues.serviceNetworksIpv4,
85
133
  values.clusterNetworks,
86
134
  values.machineNetworks,
87
135
  values.serviceNetworks,
@@ -96,11 +144,30 @@ export const StackTypeControlGroup = ({
96
144
  }
97
145
 
98
146
  if (values.machineNetworks && values.machineNetworks?.length < 2) {
99
- setFieldValue(
100
- 'machineNetworks',
101
- [...values.machineNetworks, { cidr: cidrIPv6, clusterId: clusterId }],
102
- false,
103
- );
147
+ // Ensure IPv4 is primary when switching to dual-stack
148
+ const firstNetwork = values.machineNetworks[0];
149
+ const isFirstIPv6 = firstNetwork?.cidr && Address6.isValid(firstNetwork.cidr);
150
+
151
+ if (isFirstIPv6) {
152
+ // If first network is IPv6, make IPv4 primary and IPv6 secondary
153
+ const IPv4Subnets = hostSubnets.filter((subnet) => Address4.isValid(subnet.subnet));
154
+ const cidrIPv4 = IPv4Subnets.length >= 1 ? IPv4Subnets[0].subnet : NO_SUBNET_SET;
155
+ setFieldValue(
156
+ 'machineNetworks',
157
+ [
158
+ { cidr: cidrIPv4, clusterId: clusterId },
159
+ { cidr: firstNetwork.cidr, clusterId: clusterId },
160
+ ],
161
+ false,
162
+ );
163
+ } else {
164
+ // If first network is IPv4, add IPv6 as secondary
165
+ setFieldValue(
166
+ 'machineNetworks',
167
+ [...values.machineNetworks, { cidr: cidrIPv6, clusterId: clusterId }],
168
+ false,
169
+ );
170
+ }
104
171
  }
105
172
  if (values.clusterNetworks && values.clusterNetworks?.length < 2) {
106
173
  setFieldValue(
@@ -2,12 +2,14 @@ import React from 'react';
2
2
  import {
3
3
  Dropdown,
4
4
  DropdownItem,
5
+ DropdownGroup,
5
6
  MenuToggle,
6
7
  MenuToggleElement,
7
8
  MenuToggleProps,
8
9
  } from '@patternfly/react-core';
9
10
  import { useField } from 'formik';
10
11
  import { getFieldId, HostSubnet, NO_SUBNET_SET } from '../../../../common';
12
+ import { Address4, Address6 } from 'ip-address';
11
13
 
12
14
  type SubnetsDropdownProps = {
13
15
  name: string;
@@ -48,7 +50,7 @@ export const SubnetsDropdown = ({
48
50
  const [isOpen, setOpen] = React.useState(false);
49
51
  const fieldId = getFieldId(name, 'input');
50
52
 
51
- const { itemsSubnets, currentDisplayValue } = React.useMemo(() => {
53
+ const { itemsSubnets, ipv4Subnets, ipv6Subnets, currentDisplayValue } = React.useMemo(() => {
52
54
  const itemsSubnets =
53
55
  machineSubnets.length === 0
54
56
  ? [noSubnetAvailableOption]
@@ -56,6 +58,10 @@ export const SubnetsDropdown = ({
56
58
  toFormSelectOptions(machineSubnets),
57
59
  );
58
60
 
61
+ // Separate IPv4 and IPv6 subnets
62
+ const ipv4Subnets = machineSubnets.filter((subnet) => Address4.isValid(subnet.subnet));
63
+ const ipv6Subnets = machineSubnets.filter((subnet) => Address6.isValid(subnet.subnet));
64
+
59
65
  let currentDisplayValue = itemsSubnets[0].label; // The placeholder is the fallback
60
66
  if (field.value) {
61
67
  const subnetItem = itemsSubnets.find((item) => item.value === field.value);
@@ -67,14 +73,65 @@ export const SubnetsDropdown = ({
67
73
  currentDisplayValue = itemsSubnets[1].label;
68
74
  }
69
75
 
70
- return { itemsSubnets, currentDisplayValue: currentDisplayValue };
76
+ return { itemsSubnets, ipv4Subnets, ipv6Subnets, currentDisplayValue: currentDisplayValue };
71
77
  }, [machineSubnets, field.value]);
72
78
 
73
- const dropdownItems = itemsSubnets.map(({ value, label, isDisabled }) => (
74
- <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
75
- {label}
76
- </DropdownItem>
77
- ));
79
+ const dropdownItems = React.useMemo(() => {
80
+ if (machineSubnets.length === 0) {
81
+ return [
82
+ <DropdownItem
83
+ key={noSubnetAvailableOption.value}
84
+ id={noSubnetAvailableOption.value}
85
+ isDisabled={noSubnetAvailableOption.isDisabled}
86
+ value={noSubnetAvailableOption.value}
87
+ >
88
+ {noSubnetAvailableOption.label}
89
+ </DropdownItem>,
90
+ ];
91
+ }
92
+
93
+ const hasIpv4 = ipv4Subnets.length > 0;
94
+ const hasIpv6 = ipv6Subnets.length > 0;
95
+
96
+ // If we have both IPv4 and IPv6, group them
97
+ if (hasIpv4 && hasIpv6) {
98
+ const ipv4Items = toFormSelectOptions(ipv4Subnets).map(({ value, label, isDisabled }) => (
99
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
100
+ {label}
101
+ </DropdownItem>
102
+ ));
103
+
104
+ const ipv6Items = toFormSelectOptions(ipv6Subnets).map(({ value, label, isDisabled }) => (
105
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
106
+ {label}
107
+ </DropdownItem>
108
+ ));
109
+
110
+ return [
111
+ <DropdownItem
112
+ key={makeNoSubnetSelectedOption(machineSubnets.length).value}
113
+ id={makeNoSubnetSelectedOption(machineSubnets.length).value}
114
+ isDisabled={makeNoSubnetSelectedOption(machineSubnets.length).isDisabled}
115
+ value={makeNoSubnetSelectedOption(machineSubnets.length).value}
116
+ >
117
+ {makeNoSubnetSelectedOption(machineSubnets.length).label}
118
+ </DropdownItem>,
119
+ <DropdownGroup label="IPv4" key="ipv4-group">
120
+ {ipv4Items}
121
+ </DropdownGroup>,
122
+ <DropdownGroup label="IPv6" key="ipv6-group">
123
+ {ipv6Items}
124
+ </DropdownGroup>,
125
+ ];
126
+ }
127
+
128
+ // If only one type, don't group
129
+ return itemsSubnets.map(({ value, label, isDisabled }) => (
130
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
131
+ {label}
132
+ </DropdownItem>
133
+ ));
134
+ }, [machineSubnets, ipv4Subnets, ipv6Subnets, itemsSubnets]);
78
135
 
79
136
  const onSelect = (event?: React.MouseEvent<Element, MouseEvent>): void => {
80
137
  setValue(event?.currentTarget.id);
@@ -100,7 +157,7 @@ export const SubnetsDropdown = ({
100
157
  <Dropdown
101
158
  {...field}
102
159
  id={`${fieldId}-dropdown`}
103
- onOpenChange={() => setOpen(!isOpen)}
160
+ onOpenChange={(isOpen: boolean) => setOpen(isOpen)}
104
161
  onSelect={onSelect}
105
162
  toggle={toggle}
106
163
  isOpen={isOpen}
@@ -16,7 +16,6 @@ import {
16
16
  NETWORK_TYPE_SDN,
17
17
  selectMachineNetworkCIDR,
18
18
  getVipValidationsById,
19
- DUAL_STACK,
20
19
  PopoverIcon,
21
20
  selectApiVip,
22
21
  selectIngressVip,
@@ -117,7 +116,6 @@ export const VirtualIPControlGroup = ({
117
116
  setFieldValue(field, [{ ip: e.target.value, clusterId: cluster.id }], true);
118
117
  };
119
118
 
120
- const ipFieldsSuffix = values.stackType === DUAL_STACK ? ' (IPv4)' : '';
121
119
  return (
122
120
  <>
123
121
  {!isVipDhcpAllocationDisabled && (
@@ -145,7 +143,7 @@ export const VirtualIPControlGroup = ({
145
143
  <FormikStaticField
146
144
  label={
147
145
  <>
148
- <span>API IP{ipFieldsSuffix}</span> <PopoverIcon bodyContent={ipPopoverContent} />
146
+ <span>API IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
149
147
  </>
150
148
  }
151
149
  name="apiVip"
@@ -164,8 +162,7 @@ export const VirtualIPControlGroup = ({
164
162
  <FormikStaticField
165
163
  label={
166
164
  <>
167
- <span>Ingress IP{ipFieldsSuffix}</span>{' '}
168
- <PopoverIcon bodyContent={ipPopoverContent} />
165
+ <span>Ingress IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
169
166
  </>
170
167
  }
171
168
  name="ingressVip"
@@ -191,8 +188,7 @@ export const VirtualIPControlGroup = ({
191
188
  <OcmInputField
192
189
  label={
193
190
  <>
194
- <span>API IP{ipFieldsSuffix}</span>{' '}
195
- <PopoverIcon bodyContent={ipPopoverContent} />
191
+ <span>API IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
196
192
  </>
197
193
  }
198
194
  name="apiVips.0.ip"
@@ -208,8 +204,7 @@ export const VirtualIPControlGroup = ({
208
204
  name="ingressVips.0.ip"
209
205
  label={
210
206
  <>
211
- <span>Ingress IP{ipFieldsSuffix}</span>{' '}
212
- <PopoverIcon bodyContent={ipPopoverContent} />
207
+ <span>Ingress IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
213
208
  </>
214
209
  }
215
210
  helperText={ipHelperText}
@@ -34,7 +34,7 @@ export const getNetworkInitialValues = (
34
34
  isClusterManagedNetworkingUnsupported: boolean,
35
35
  ): NetworkConfigurationValues => {
36
36
  const isSNOCluster = isSNO(cluster);
37
- const isDualStackType = isDualStack(cluster);
37
+ const isDualStackType = isDualStack({ ...cluster, openshiftVersion: cluster.openshiftVersion });
38
38
 
39
39
  return {
40
40
  apiVips: cluster.apiVips,
@@ -54,18 +54,25 @@ export const getNetworkInitialValues = (
54
54
  cluster.clusterNetworks ||
55
55
  (isDualStackType
56
56
  ? defaultNetworkValues.clusterNetworksDualstack
57
- : defaultNetworkValues.clusterNetworksIpv4),
57
+ : defaultNetworkValues.clusterNetworksIpv4?.map((network) => ({
58
+ ...network,
59
+ clusterId: cluster.id,
60
+ }))),
58
61
  serviceNetworks:
59
62
  cluster.serviceNetworks ||
60
63
  (isDualStackType
61
64
  ? defaultNetworkValues.serviceNetworksDualstack
62
- : defaultNetworkValues.serviceNetworksIpv4),
65
+ : defaultNetworkValues.serviceNetworksIpv4?.map((network) => ({
66
+ ...network,
67
+ clusterId: cluster.id,
68
+ }))),
63
69
  };
64
70
  };
65
71
 
66
72
  export const getNetworkConfigurationValidationSchema = (
67
73
  initialValues: NetworkConfigurationValues,
68
74
  hostSubnets: HostSubnets,
75
+ openshiftVersion?: string,
69
76
  ) =>
70
77
  Yup.lazy((values: NetworkConfigurationValues) =>
71
78
  Yup.object<NetworkConfigurationValues>().shape({
@@ -85,7 +92,7 @@ export const getNetworkConfigurationValidationSchema = (
85
92
  otherwise: () =>
86
93
  values.machineNetworks && values.machineNetworks?.length >= 2
87
94
  ? machineNetworksValidationSchema.concat(
88
- dualStackValidationSchema('machine networks'),
95
+ dualStackValidationSchema('machine networks', openshiftVersion),
89
96
  )
90
97
  : Yup.array(),
91
98
  }),
@@ -94,7 +101,9 @@ export const getNetworkConfigurationValidationSchema = (
94
101
  then: () => clusterNetworksValidationSchema.concat(IPv4ValidationSchema),
95
102
  otherwise: () =>
96
103
  values.clusterNetworks && values.clusterNetworks?.length >= 2
97
- ? clusterNetworksValidationSchema.concat(dualStackValidationSchema('cluster network'))
104
+ ? clusterNetworksValidationSchema.concat(
105
+ dualStackValidationSchema('cluster network', openshiftVersion),
106
+ )
98
107
  : Yup.array(),
99
108
  }),
100
109
  serviceNetworks: serviceNetworkValidationSchema.when('stackType', {
@@ -102,7 +111,9 @@ export const getNetworkConfigurationValidationSchema = (
102
111
  then: () => serviceNetworkValidationSchema.concat(IPv4ValidationSchema),
103
112
  otherwise: () =>
104
113
  values.serviceNetworks && values.serviceNetworks?.length >= 2
105
- ? serviceNetworkValidationSchema.concat(dualStackValidationSchema('service network'))
114
+ ? serviceNetworkValidationSchema.concat(
115
+ dualStackValidationSchema('service network', openshiftVersion),
116
+ )
106
117
  : Yup.array(),
107
118
  }),
108
119
  }),
@@ -58,7 +58,16 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
58
58
  )),
59
59
  props: { 'data-testid': 'machine-networks' },
60
60
  },
61
- isDualStack(cluster) ? { title: 'Primary' } : { title: '' },
61
+ isDualStack({ ...cluster, openshiftVersion: cluster.openshiftVersion })
62
+ ? {
63
+ title: cluster.machineNetworks?.map((network, index) => (
64
+ <span key={network.cidr}>
65
+ {index === 0 ? 'Primary' : 'Secondary'}
66
+ <br />
67
+ </span>
68
+ )),
69
+ }
70
+ : { title: '' },
62
71
  ],
63
72
  });
64
73
 
@@ -106,7 +115,14 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
106
115
  )),
107
116
  props: { 'data-testid': 'cluster-network-cidr' },
108
117
  },
109
- isDualStack(cluster) && { title: 'Primary' },
118
+ isDualStack({ ...cluster, openshiftVersion: cluster.openshiftVersion }) && {
119
+ title: cluster.clusterNetworks?.map((network, index) => (
120
+ <span key={network.cidr}>
121
+ {index === 0 ? 'Primary' : 'Secondary'}
122
+ <br />
123
+ </span>
124
+ )),
125
+ },
110
126
  ].filter(Boolean),
111
127
  },
112
128
  {
@@ -122,7 +138,14 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
122
138
  )),
123
139
  props: { 'data-testid': 'cluster-network-prefix' },
124
140
  },
125
- isDualStack(cluster) && { title: 'Primary' },
141
+ isDualStack({ ...cluster, openshiftVersion: cluster.openshiftVersion }) && {
142
+ title: cluster.clusterNetworks?.map((network, index) => (
143
+ <span key={network.hostPrefix}>
144
+ {index === 0 ? 'Primary' : 'Secondary'}
145
+ <br />
146
+ </span>
147
+ )),
148
+ },
126
149
  ].filter(Boolean),
127
150
  },
128
151
  {
@@ -138,7 +161,14 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => {
138
161
  )),
139
162
  props: { 'data-testid': 'service-network-cidr' },
140
163
  },
141
- isDualStack(cluster) && { title: 'Primary' },
164
+ isDualStack({ ...cluster, openshiftVersion: cluster.openshiftVersion }) && {
165
+ title: cluster.serviceNetworks?.map((network, index) => (
166
+ <span key={network.cidr}>
167
+ {index === 0 ? 'Primary' : 'Secondary'}
168
+ <br />
169
+ </span>
170
+ )),
171
+ },
142
172
  ].filter(Boolean),
143
173
  },
144
174
  {
@@ -1,9 +1,8 @@
1
1
  import React from 'react';
2
2
  import {
3
+ Content,
3
4
  Divider,
4
5
  ExpandableSection,
5
- Flex,
6
- FlexItem,
7
6
  Grid,
8
7
  GridItem,
9
8
  gridSpans,
@@ -94,12 +93,9 @@ const PreflightCheckInfo = ({
94
93
  }) => {
95
94
  return (
96
95
  <GridItem span={span} offset={offset} className={className}>
97
- <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
98
- <FlexItem style={{ display: 'flex', alignItems: 'center' }}>{icon}</FlexItem>
99
- <FlexItem style={{ display: 'flex', alignItems: 'center' }}>
100
- <b>{title}</b>
101
- </FlexItem>
102
- </Flex>
96
+ <Content className="pf-v6-u-text-wrap pf-v6-u-font-weight-bold">
97
+ {icon} {title}
98
+ </Content>
103
99
  </GridItem>
104
100
  );
105
101
  };
@@ -186,7 +182,9 @@ const ReviewPreflightChecks = ({ cluster }: { cluster: Cluster }) => {
186
182
  <ExpandableSection
187
183
  toggleContent={
188
184
  <Grid>
189
- <GridItem span={2}>Preflight checks</GridItem>
185
+ <GridItem span={2} className="pf-v6-u-text-wrap">
186
+ Preflight checks
187
+ </GridItem>
190
188
  {!isChecksExpanded && <PreflightChecksDetailCollapsed cluster={cluster} />}
191
189
  </Grid>
192
190
  }