@openshift-assisted/ui-lib 2.46.3 → 2.47.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 (133) 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/components/clusterWizard/OperatorsBundle.d.ts.map +1 -1
  43. package/build/@types/ocm/services/ClustersService.d.ts +5 -5
  44. package/build/@types/ocm/services/HostsService.d.ts +11 -11
  45. package/build/@types/ocm/services/InfraEnvsService.d.ts +2 -2
  46. package/build/@types/ocm/services/UISettingsService.d.ts +1 -1
  47. package/build/@types/ocm/store/slices/infra-envs/thunks.d.ts +1 -1
  48. package/build/cjs/cim/components/ClusterDeployment/ClusterDetailsFormFields.js +9 -5
  49. package/build/cjs/cim/components/ClusterDeployment/ClusterDetailsFormFields.js.map +1 -1
  50. package/build/cjs/common/components/clusterConfiguration/ControlPlaneNodesDropdown.js +23 -6
  51. package/build/cjs/common/components/clusterConfiguration/ControlPlaneNodesDropdown.js.map +1 -1
  52. package/build/cjs/common/components/clusterConfiguration/DownloadIso.js +1 -2
  53. package/build/cjs/common/components/clusterConfiguration/DownloadIso.js.map +1 -1
  54. package/build/cjs/{ocm → common}/components/clusterConfiguration/OcmTNADisclaimer.js +1 -1
  55. package/build/cjs/common/components/clusterConfiguration/OcmTNADisclaimer.js.map +1 -0
  56. package/build/cjs/common/components/clusterConfiguration/utils.js +23 -12
  57. package/build/cjs/common/components/clusterConfiguration/utils.js.map +1 -1
  58. package/build/cjs/common/components/clusterConfiguration/utils.test.js +242 -0
  59. package/build/cjs/common/components/clusterConfiguration/utils.test.js.map +1 -0
  60. package/build/cjs/common/components/clusterWizard/clusterDetailsValidation.js +1 -1
  61. package/build/cjs/common/components/clusterWizard/clusterDetailsValidation.js.map +1 -1
  62. package/build/cjs/common/components/ui/formik/validationSchemas.js +13 -3
  63. package/build/cjs/common/components/ui/formik/validationSchemas.js.map +1 -1
  64. package/build/cjs/common/config/constants.js +5 -10
  65. package/build/cjs/common/config/constants.js.map +1 -1
  66. package/build/cjs/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.js +1 -1
  67. package/build/cjs/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.js.map +1 -1
  68. package/build/cjs/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.js +0 -1
  69. package/build/cjs/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.js.map +1 -1
  70. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js +55 -13
  71. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.js.map +1 -1
  72. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js +49 -12
  73. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.js.map +1 -1
  74. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.js +3 -3
  75. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.js.map +1 -1
  76. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.js +1 -1
  77. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.js.map +1 -1
  78. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js +67 -7
  79. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.js.map +1 -1
  80. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js +36 -6
  81. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.js.map +1 -1
  82. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.js +29 -20
  83. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.js.map +1 -1
  84. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js +40 -35
  85. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.js.map +1 -1
  86. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js +26 -8
  87. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.js.map +1 -1
  88. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.js +5 -5
  89. package/build/cjs/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.js.map +1 -1
  90. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js +2 -2
  91. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.js.map +1 -1
  92. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.test.js +180 -0
  93. package/build/cjs/ocm/components/clusterDetail/ClusterProperties.test.js.map +1 -0
  94. package/build/cjs/ocm/components/clusterWizard/OperatorsBundle.js +3 -1
  95. package/build/cjs/ocm/components/clusterWizard/OperatorsBundle.js.map +1 -1
  96. package/build/cjs/ocm/components/featureSupportLevels/featureStateUtils.js +0 -3
  97. package/build/cjs/ocm/components/featureSupportLevels/featureStateUtils.js.map +1 -1
  98. package/lib/cim/components/ClusterDeployment/ClusterDetailsFormFields.tsx +17 -35
  99. package/lib/cim/types/k8s/agent-cluster-install.ts +1 -0
  100. package/lib/common/components/clusterConfiguration/ControlPlaneNodesDropdown.tsx +61 -31
  101. package/lib/common/components/clusterConfiguration/DownloadIso.tsx +5 -2
  102. package/lib/{ocm → common}/components/clusterConfiguration/OcmTNADisclaimer.tsx +1 -1
  103. package/lib/common/components/clusterConfiguration/utils.test.ts +275 -0
  104. package/lib/common/components/clusterConfiguration/utils.ts +28 -9
  105. package/lib/common/components/clusterWizard/clusterDetailsValidation.ts +1 -1
  106. package/lib/common/components/ui/formik/_validationSchemas.test.ts +82 -0
  107. package/lib/common/components/ui/formik/validationSchemas.ts +16 -10
  108. package/lib/common/config/constants.ts +5 -11
  109. package/lib/ocm/components/clusterConfiguration/ControlPlaneNodesDropdown.tsx +1 -1
  110. package/lib/ocm/components/clusterConfiguration/manifestsConfiguration/components/ExpandedManifest.tsx +1 -7
  111. package/lib/ocm/components/clusterConfiguration/networkConfiguration/AdvancedNetworkFields.tsx +59 -13
  112. package/lib/ocm/components/clusterConfiguration/networkConfiguration/AvailableSubnetsControl.tsx +98 -38
  113. package/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfiguration.tsx +3 -2
  114. package/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationForm.tsx +3 -2
  115. package/lib/ocm/components/clusterConfiguration/networkConfiguration/StackTypeControl.tsx +78 -11
  116. package/lib/ocm/components/clusterConfiguration/networkConfiguration/SubnetsDropdown.tsx +83 -10
  117. package/lib/ocm/components/clusterConfiguration/networkConfiguration/VirtualIPControlGroup.tsx +70 -14
  118. package/lib/ocm/components/clusterConfiguration/networkConfiguration/networkConfigurationValidation.ts +30 -11
  119. package/lib/ocm/components/clusterConfiguration/review/ReviewNetworkingTable.tsx +34 -4
  120. package/lib/ocm/components/clusterConfiguration/review/ReviewPreflightChecks.tsx +7 -9
  121. package/lib/ocm/components/clusterDetail/ClusterProperties.test.ts +215 -0
  122. package/lib/ocm/components/clusterDetail/ClusterProperties.tsx +2 -2
  123. package/lib/ocm/components/clusterWizard/OperatorsBundle.tsx +3 -1
  124. package/lib/ocm/components/featureSupportLevels/featureStateUtils.ts +1 -3
  125. package/package.json +3 -3
  126. package/build/@types/ocm/components/clusterConfiguration/OcmTNADisclaimer.d.ts.map +0 -1
  127. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.d.ts +0 -6
  128. package/build/@types/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.d.ts.map +0 -1
  129. package/build/cjs/ocm/components/clusterConfiguration/OcmTNADisclaimer.js.map +0 -1
  130. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.js +0 -25
  131. package/build/cjs/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.js.map +0 -1
  132. package/lib/ocm/components/clusterConfiguration/networkConfiguration/UserManagedNetworkingTextContent.tsx +0 -30
  133. /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,21 @@ import React from 'react';
2
2
  import {
3
3
  Dropdown,
4
4
  DropdownItem,
5
+ DropdownGroup,
5
6
  MenuToggle,
6
7
  MenuToggleElement,
7
8
  MenuToggleProps,
9
+ Divider,
8
10
  } from '@patternfly/react-core';
9
11
  import { useField } from 'formik';
10
- import { getFieldId, HostSubnet, NO_SUBNET_SET } from '../../../../common';
12
+ import {
13
+ getFieldId,
14
+ HostSubnet,
15
+ NO_SUBNET_SET,
16
+ TechnologyPreview,
17
+ PreviewBadgePosition,
18
+ } from '../../../../common';
19
+ import { Address4, Address6 } from 'ip-address';
11
20
 
12
21
  type SubnetsDropdownProps = {
13
22
  name: string;
@@ -48,7 +57,7 @@ export const SubnetsDropdown = ({
48
57
  const [isOpen, setOpen] = React.useState(false);
49
58
  const fieldId = getFieldId(name, 'input');
50
59
 
51
- const { itemsSubnets, currentDisplayValue } = React.useMemo(() => {
60
+ const { itemsSubnets, ipv4Subnets, ipv6Subnets, currentDisplayValue } = React.useMemo(() => {
52
61
  const itemsSubnets =
53
62
  machineSubnets.length === 0
54
63
  ? [noSubnetAvailableOption]
@@ -56,7 +65,12 @@ export const SubnetsDropdown = ({
56
65
  toFormSelectOptions(machineSubnets),
57
66
  );
58
67
 
59
- let currentDisplayValue = itemsSubnets[0].label; // The placeholder is the fallback
68
+ // Separate IPv4 and IPv6 subnets
69
+ const ipv4Subnets = machineSubnets.filter((subnet) => Address4.isValid(subnet.subnet));
70
+ const ipv6Subnets = machineSubnets.filter((subnet) => Address6.isValid(subnet.subnet));
71
+
72
+ let currentDisplayValue =
73
+ itemsSubnets.length > 1 ? itemsSubnets[1].label : itemsSubnets[0].label; // The placeholder is the fallback in some cases
60
74
  if (field.value) {
61
75
  const subnetItem = itemsSubnets.find((item) => item.value === field.value);
62
76
  if (subnetItem) {
@@ -67,20 +81,74 @@ export const SubnetsDropdown = ({
67
81
  currentDisplayValue = itemsSubnets[1].label;
68
82
  }
69
83
 
70
- return { itemsSubnets, currentDisplayValue: currentDisplayValue };
84
+ return { itemsSubnets, ipv4Subnets, ipv6Subnets, currentDisplayValue: currentDisplayValue };
71
85
  }, [machineSubnets, field.value]);
72
86
 
73
- const dropdownItems = itemsSubnets.map(({ value, label, isDisabled }) => (
74
- <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
75
- {label}
76
- </DropdownItem>
77
- ));
87
+ const dropdownItems = React.useMemo(() => {
88
+ if (machineSubnets.length === 0) {
89
+ return [
90
+ <DropdownItem
91
+ key={noSubnetAvailableOption.value}
92
+ id={noSubnetAvailableOption.value}
93
+ isDisabled={noSubnetAvailableOption.isDisabled}
94
+ value={noSubnetAvailableOption.value}
95
+ >
96
+ {noSubnetAvailableOption.label}
97
+ </DropdownItem>,
98
+ ];
99
+ }
100
+
101
+ const hasIpv4 = ipv4Subnets.length > 0;
102
+ const hasIpv6 = ipv6Subnets.length > 0;
103
+
104
+ // If we have both IPv4 and IPv6, group them
105
+ if (hasIpv4 && hasIpv6) {
106
+ const ipv4Items = toFormSelectOptions(ipv4Subnets).map(({ value, label, isDisabled }) => (
107
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
108
+ {label}
109
+ </DropdownItem>
110
+ ));
111
+ ipv4Items.push(<Divider key="ipv4-divider" />);
112
+ const ipv6Items = toFormSelectOptions(ipv6Subnets).map(({ value, label, isDisabled }) => (
113
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
114
+ {label}
115
+ </DropdownItem>
116
+ ));
117
+
118
+ return [
119
+ <DropdownItem
120
+ key={makeNoSubnetSelectedOption(machineSubnets.length).value}
121
+ id={makeNoSubnetSelectedOption(machineSubnets.length).value}
122
+ isDisabled={makeNoSubnetSelectedOption(machineSubnets.length).isDisabled}
123
+ value={makeNoSubnetSelectedOption(machineSubnets.length).value}
124
+ >
125
+ {makeNoSubnetSelectedOption(machineSubnets.length).label}
126
+ </DropdownItem>,
127
+ <DropdownGroup label="IPv4" key="ipv4-group">
128
+ {ipv4Items}
129
+ </DropdownGroup>,
130
+ <DropdownGroup label="IPv6 (Technology Preview)" key="ipv6-group">
131
+ {ipv6Items}
132
+ </DropdownGroup>,
133
+ ];
134
+ }
135
+
136
+ // If only one type, don't group
137
+ return itemsSubnets.map(({ value, label, isDisabled }) => (
138
+ <DropdownItem key={value} id={value} isDisabled={isDisabled} value={value}>
139
+ {label}
140
+ </DropdownItem>
141
+ ));
142
+ }, [machineSubnets, ipv4Subnets, ipv6Subnets, itemsSubnets]);
78
143
 
79
144
  const onSelect = (event?: React.MouseEvent<Element, MouseEvent>): void => {
80
145
  setValue(event?.currentTarget.id);
81
146
  setOpen(false);
82
147
  };
83
148
 
149
+ const currentItem = itemsSubnets.find((i) => i.label === currentDisplayValue);
150
+ const showBadge = Boolean(currentItem && Address6.isValid(currentItem.value));
151
+
84
152
  const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
85
153
  <MenuToggle
86
154
  ref={toggleRef}
@@ -93,6 +161,11 @@ export const SubnetsDropdown = ({
93
161
  {...props}
94
162
  >
95
163
  {currentDisplayValue}
164
+ {showBadge && (
165
+ <span onClick={(e) => e.stopPropagation()}>
166
+ <TechnologyPreview position={PreviewBadgePosition.inlineRight} />
167
+ </span>
168
+ )}
96
169
  </MenuToggle>
97
170
  );
98
171
 
@@ -100,7 +173,7 @@ export const SubnetsDropdown = ({
100
173
  <Dropdown
101
174
  {...field}
102
175
  id={`${fieldId}-dropdown`}
103
- onOpenChange={() => setOpen(!isOpen)}
176
+ onOpenChange={(isOpen: boolean) => setOpen(isOpen)}
104
177
  onSelect={onSelect}
105
178
  toggle={toggle}
106
179
  isOpen={isOpen}
@@ -14,9 +14,9 @@ import {
14
14
  NetworkConfigurationValues,
15
15
  FormikStaticField,
16
16
  NETWORK_TYPE_SDN,
17
+ DUAL_STACK,
17
18
  selectMachineNetworkCIDR,
18
19
  getVipValidationsById,
19
- DUAL_STACK,
20
20
  PopoverIcon,
21
21
  selectApiVip,
22
22
  selectIngressVip,
@@ -25,7 +25,12 @@ import { selectCurrentClusterPermissionsState } from '../../../store/slices/curr
25
25
  import { OcmCheckboxField, OcmInputField } from '../../ui/OcmFormFields';
26
26
  import { useTranslation } from '../../../../common/hooks/use-translation-wrapper';
27
27
  import NewFeatureSupportLevelBadge from '../../../../common/components/newFeatureSupportLevels/NewFeatureSupportLevelBadge';
28
- import { Cluster, Ip, SupportLevel } from '@openshift-assisted/types/assisted-installer-service';
28
+ import {
29
+ ApiVip,
30
+ Cluster,
31
+ Ip,
32
+ SupportLevel,
33
+ } from '@openshift-assisted/types/assisted-installer-service';
29
34
 
30
35
  interface VipStaticValueProps {
31
36
  id?: string;
@@ -97,6 +102,7 @@ export const VirtualIPControlGroup = ({
97
102
  );
98
103
 
99
104
  const enableAllocation = values.networkType === NETWORK_TYPE_SDN;
105
+ const isDualStack = values.stackType === DUAL_STACK;
100
106
 
101
107
  React.useEffect(() => {
102
108
  if (!isViewerMode && !enableAllocation) {
@@ -113,11 +119,22 @@ export const VirtualIPControlGroup = ({
113
119
  [cluster.apiVips, cluster.ingressVips, setFieldValue],
114
120
  );
115
121
 
116
- const setVipValue = (field: string, e: React.ChangeEvent<HTMLInputElement>) => {
117
- setFieldValue(field, [{ ip: e.target.value, clusterId: cluster.id }], true);
122
+ const setVipValueAtIndex = (
123
+ field: 'apiVips' | 'ingressVips',
124
+ index: number,
125
+ e: React.ChangeEvent<HTMLInputElement>,
126
+ ) => {
127
+ const fieldArray: ApiVip[] =
128
+ field === 'apiVips' ? values.apiVips || [] : values.ingressVips || [];
129
+ const next: ApiVip[] = Array.isArray(fieldArray) ? [...fieldArray] : [];
130
+ // Ensure array has the desired length
131
+ while (next.length <= index) {
132
+ next.push({ ip: '', clusterId: cluster.id });
133
+ }
134
+ next[index] = { ip: e.target.value, clusterId: cluster.id };
135
+ setFieldValue(field, next, true);
118
136
  };
119
137
 
120
- const ipFieldsSuffix = values.stackType === DUAL_STACK ? ' (IPv4)' : '';
121
138
  return (
122
139
  <>
123
140
  {!isVipDhcpAllocationDisabled && (
@@ -145,7 +162,7 @@ export const VirtualIPControlGroup = ({
145
162
  <FormikStaticField
146
163
  label={
147
164
  <>
148
- <span>API IP{ipFieldsSuffix}</span> <PopoverIcon bodyContent={ipPopoverContent} />
165
+ <span>API IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
149
166
  </>
150
167
  }
151
168
  name="apiVip"
@@ -164,8 +181,7 @@ export const VirtualIPControlGroup = ({
164
181
  <FormikStaticField
165
182
  label={
166
183
  <>
167
- <span>Ingress IP{ipFieldsSuffix}</span>{' '}
168
- <PopoverIcon bodyContent={ipPopoverContent} />
184
+ <span>Ingress IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
169
185
  </>
170
186
  }
171
187
  name="ingressVip"
@@ -191,34 +207,74 @@ export const VirtualIPControlGroup = ({
191
207
  <OcmInputField
192
208
  label={
193
209
  <>
194
- <span>API IP{ipFieldsSuffix}</span>{' '}
195
- <PopoverIcon bodyContent={ipPopoverContent} />
210
+ <span>API IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
196
211
  </>
197
212
  }
198
213
  name="apiVips.0.ip"
199
214
  helperText={ipHelperText}
200
215
  isRequired
216
+ labelInfo={isDualStack ? 'Primary' : undefined}
201
217
  onChange={(e) =>
202
- setVipValue('apiVips', e as React.ChangeEvent<HTMLInputElement>)
218
+ setVipValueAtIndex('apiVips', 0, e as React.ChangeEvent<HTMLInputElement>)
203
219
  }
204
220
  />
205
221
  </StackItem>
222
+ {isDualStack && (
223
+ <StackItem>
224
+ <OcmInputField
225
+ label={
226
+ <>
227
+ <span>API IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
228
+ </>
229
+ }
230
+ name="apiVips.1.ip"
231
+ helperText={ipHelperText}
232
+ isRequired
233
+ labelInfo={'Secondary'}
234
+ onChange={(e) =>
235
+ setVipValueAtIndex('apiVips', 1, e as React.ChangeEvent<HTMLInputElement>)
236
+ }
237
+ />
238
+ </StackItem>
239
+ )}
206
240
  <StackItem>
207
241
  <OcmInputField
208
242
  name="ingressVips.0.ip"
209
243
  label={
210
244
  <>
211
- <span>Ingress IP{ipFieldsSuffix}</span>{' '}
212
- <PopoverIcon bodyContent={ipPopoverContent} />
245
+ <span>Ingress IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
213
246
  </>
214
247
  }
215
248
  helperText={ipHelperText}
216
249
  isRequired
250
+ labelInfo={isDualStack ? 'Primary' : undefined}
217
251
  onChange={(e) =>
218
- setVipValue('ingressVips', e as React.ChangeEvent<HTMLInputElement>)
252
+ setVipValueAtIndex('ingressVips', 0, e as React.ChangeEvent<HTMLInputElement>)
219
253
  }
220
254
  />
221
255
  </StackItem>
256
+ {isDualStack && (
257
+ <StackItem>
258
+ <OcmInputField
259
+ name="ingressVips.1.ip"
260
+ label={
261
+ <>
262
+ <span>Ingress IP</span> <PopoverIcon bodyContent={ipPopoverContent} />
263
+ </>
264
+ }
265
+ helperText={ipHelperText}
266
+ isRequired
267
+ labelInfo={'Secondary'}
268
+ onChange={(e) =>
269
+ setVipValueAtIndex(
270
+ 'ingressVips',
271
+ 1,
272
+ e as React.ChangeEvent<HTMLInputElement>,
273
+ )
274
+ }
275
+ />
276
+ </StackItem>
277
+ )}
222
278
  </Stack>
223
279
  )}
224
280
  </FieldArray>