@openshift-assisted/ui-lib 2.55.1 → 2.55.3

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 (61) hide show
  1. package/build/@types/common/config/docs_links.d.ts +1 -0
  2. package/build/@types/common/config/docs_links.d.ts.map +1 -1
  3. package/build/@types/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.d.ts.map +1 -1
  4. package/build/@types/ocm/components/clusterWizard/ClusterWizardContextProvider.d.ts.map +1 -1
  5. package/build/@types/ocm/components/clusterWizard/ClusterWizardNavigation.d.ts +7 -0
  6. package/build/@types/ocm/components/clusterWizard/ClusterWizardNavigation.d.ts.map +1 -1
  7. package/build/@types/ocm/components/clusterWizard/NewClusterWizard.d.ts.map +1 -1
  8. package/build/@types/ocm/components/clusterWizard/StaticIp.d.ts +1 -1
  9. package/build/@types/ocm/components/clusterWizard/StaticIp.d.ts.map +1 -1
  10. package/build/@types/ocm/components/clusterWizard/disconnected/BasicStep.d.ts +1 -1
  11. package/build/@types/ocm/components/clusterWizard/disconnected/BasicStep.d.ts.map +1 -1
  12. package/build/@types/ocm/components/clusterWizard/disconnected/DisconnectedStaticIp.d.ts +4 -0
  13. package/build/@types/ocm/components/clusterWizard/disconnected/DisconnectedStaticIp.d.ts.map +1 -0
  14. package/build/@types/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.d.ts.map +1 -1
  15. package/build/@types/ocm/components/clusterWizard/disconnected/ReviewStep.d.ts.map +1 -1
  16. package/build/@types/ocm/components/clusterWizard/utils.d.ts +7 -0
  17. package/build/@types/ocm/components/clusterWizard/utils.d.ts.map +1 -0
  18. package/build/@types/ocm/components/clusters/AssistedInstallerHeader.d.ts.map +1 -1
  19. package/build/cjs/common/components/clusterWizard/ClusterWizardStep.css +9 -0
  20. package/build/cjs/common/config/docs_links.js +3 -1
  21. package/build/cjs/common/config/docs_links.js.map +1 -1
  22. package/build/cjs/common/validationSchemas/addressValidation.js +2 -2
  23. package/build/cjs/common/validationSchemas/addressValidation.js.map +1 -1
  24. package/build/cjs/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.js +3 -1
  25. package/build/cjs/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.js.map +1 -1
  26. package/build/cjs/ocm/components/clusterWizard/ClusterWizardContextProvider.js +47 -47
  27. package/build/cjs/ocm/components/clusterWizard/ClusterWizardContextProvider.js.map +1 -1
  28. package/build/cjs/ocm/components/clusterWizard/ClusterWizardNavigation.js +31 -19
  29. package/build/cjs/ocm/components/clusterWizard/ClusterWizardNavigation.js.map +1 -1
  30. package/build/cjs/ocm/components/clusterWizard/NewClusterWizard.js +5 -0
  31. package/build/cjs/ocm/components/clusterWizard/NewClusterWizard.js.map +1 -1
  32. package/build/cjs/ocm/components/clusterWizard/StaticIp.js.map +1 -1
  33. package/build/cjs/ocm/components/clusterWizard/disconnected/BasicStep.js +1 -2
  34. package/build/cjs/ocm/components/clusterWizard/disconnected/BasicStep.js.map +1 -1
  35. package/build/cjs/ocm/components/clusterWizard/disconnected/DisconnectedStaticIp.js +26 -0
  36. package/build/cjs/ocm/components/clusterWizard/disconnected/DisconnectedStaticIp.js.map +1 -0
  37. package/build/cjs/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.js +52 -19
  38. package/build/cjs/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.js.map +1 -1
  39. package/build/cjs/ocm/components/clusterWizard/disconnected/ReviewStep.js +46 -48
  40. package/build/cjs/ocm/components/clusterWizard/disconnected/ReviewStep.js.map +1 -1
  41. package/build/cjs/ocm/components/clusterWizard/utils.js +63 -0
  42. package/build/cjs/ocm/components/clusterWizard/utils.js.map +1 -0
  43. package/build/cjs/ocm/components/clusters/AssistedInstallerHeader.js +0 -3
  44. package/build/cjs/ocm/components/clusters/AssistedInstallerHeader.js.map +1 -1
  45. package/lib/common/components/clusterWizard/ClusterWizardStep.css +9 -0
  46. package/lib/common/config/docs_links.ts +3 -0
  47. package/lib/common/validationSchemas/_validationSchemas.test.ts +1 -1
  48. package/lib/common/validationSchemas/addressValidation.tsx +2 -2
  49. package/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.tsx +4 -1
  50. package/lib/ocm/components/clusterWizard/ClusterWizardContextProvider.tsx +48 -71
  51. package/lib/ocm/components/clusterWizard/ClusterWizardNavigation.tsx +39 -16
  52. package/lib/ocm/components/clusterWizard/NewClusterWizard.tsx +5 -0
  53. package/lib/ocm/components/clusterWizard/StaticIp.tsx +1 -1
  54. package/lib/ocm/components/clusterWizard/_wizardTransition.test.ts +60 -1
  55. package/lib/ocm/components/clusterWizard/disconnected/BasicStep.tsx +2 -3
  56. package/lib/ocm/components/clusterWizard/disconnected/DisconnectedStaticIp.tsx +29 -0
  57. package/lib/ocm/components/clusterWizard/disconnected/OptionalConfigurationsStep.tsx +72 -12
  58. package/lib/ocm/components/clusterWizard/disconnected/ReviewStep.tsx +84 -89
  59. package/lib/ocm/components/clusterWizard/utils.ts +75 -0
  60. package/lib/ocm/components/clusters/AssistedInstallerHeader.tsx +1 -9
  61. package/package.json +3 -3
@@ -10,7 +10,6 @@ import {
10
10
  isStepAfter,
11
11
  } from './wizardTransition';
12
12
  import { HostsNetworkConfigurationType } from '../../services';
13
- import { defaultWizardSteps, staticIpFormViewSubSteps } from './constants';
14
13
  import { StaticIpView } from '../clusterConfiguration/staticIp/data/dataTypes';
15
14
  import { getStaticIpInfo } from '../clusterConfiguration/staticIp/data/fromInfraEnv';
16
15
  import { AssistedInstallerOCMPermissionTypesListType, useAlerts } from '../../../common';
@@ -21,57 +20,7 @@ import { AlertVariant } from '@patternfly/react-core';
21
20
  import { useFeature } from '../../hooks/use-feature';
22
21
  import { isThirdPartyCNI } from '../../../common/types/networkType';
23
22
  import { isOciPlatformType } from '../utils';
24
-
25
- const addStepToClusterWizard = (
26
- wizardStepIds: ClusterWizardStepsType[],
27
- addAfterStep: ClusterWizardStepsType,
28
- itemsToAdd: ClusterWizardStepsType[],
29
- ) => {
30
- const stepsIds = [...wizardStepIds];
31
- const referencePosition = stepsIds.findIndex((step) => step === addAfterStep);
32
- const found = wizardStepIds.filter((step) => step === itemsToAdd[0]);
33
- if (referencePosition !== -1 && found.length === 0) {
34
- stepsIds.splice(referencePosition + 1, 0, ...itemsToAdd);
35
- }
36
- return stepsIds;
37
- };
38
-
39
- const removeStepFromClusterWizard = (
40
- wizardStepIds: ClusterWizardStepsType[],
41
- itemToRemove: ClusterWizardStepsType,
42
- numberItemsToRemove: number,
43
- ) => {
44
- const stepsIds = [...wizardStepIds];
45
- const position = stepsIds.findIndex((step) => step === itemToRemove);
46
- if (position !== -1) {
47
- stepsIds.splice(position, numberItemsToRemove);
48
- }
49
- return stepsIds;
50
- };
51
-
52
- const getWizardStepIds = (
53
- wizardStepIds: ClusterWizardStepsType[] | undefined,
54
- staticIpView?: StaticIpView | 'dhcp-selected',
55
-
56
- isSingleClusterFeatureEnabled?: boolean,
57
- ): ClusterWizardStepsType[] => {
58
- let stepsCopy = wizardStepIds ? [...wizardStepIds] : [...defaultWizardSteps];
59
- if (staticIpView === StaticIpView.YAML) {
60
- stepsCopy = removeStepFromClusterWizard(stepsCopy, 'static-ip-network-wide-configurations', 2);
61
- stepsCopy = addStepToClusterWizard(stepsCopy, 'cluster-details', ['static-ip-yaml-view']);
62
- } else if (staticIpView === StaticIpView.FORM) {
63
- stepsCopy = removeStepFromClusterWizard(stepsCopy, 'static-ip-yaml-view', 1);
64
- stepsCopy = addStepToClusterWizard(stepsCopy, 'cluster-details', staticIpFormViewSubSteps);
65
- } else if (staticIpView === 'dhcp-selected') {
66
- stepsCopy = removeStepFromClusterWizard(stepsCopy, 'static-ip-network-wide-configurations', 2);
67
- }
68
-
69
- if (isSingleClusterFeatureEnabled && !stepsCopy.includes('credentials-download')) {
70
- stepsCopy = addStepToClusterWizard(stepsCopy, 'custom-manifests', ['credentials-download']);
71
- }
72
-
73
- return stepsCopy;
74
- };
23
+ import { getDisconnectedWizardStepIds, getWizardStepIds } from './utils';
75
24
 
76
25
  const ClusterWizardContextProvider = ({
77
26
  children,
@@ -90,6 +39,8 @@ const ClusterWizardContextProvider = ({
90
39
  const [installDisconnected, setInstallDisconnected] = React.useState(false);
91
40
  const [disconnectedCluster, setDisconnectedCluster] = React.useState<Cluster | undefined>();
92
41
  const [disconnectedInfraEnv, setDisconnectedInfraEnv] = React.useState<InfraEnv | undefined>();
42
+ const [disconnectedWizardStepIds, setDisconnectedWizardStepIds] =
43
+ React.useState<ClusterWizardStepsType[]>(disconnectedSteps);
93
44
  const location = useLocation();
94
45
  const locationState = location.state as ClusterWizardFlowStateType | undefined;
95
46
  const {
@@ -101,7 +52,7 @@ const ClusterWizardContextProvider = ({
101
52
  const { clearAlerts, addAlert, alerts } = useAlerts();
102
53
  const setClusterPermissions = useSetClusterPermissions();
103
54
 
104
- const wizardStepIds = installDisconnected ? disconnectedSteps : connectedWizardStepIds;
55
+ const wizardStepIds = installDisconnected ? disconnectedWizardStepIds : connectedWizardStepIds;
105
56
 
106
57
  React.useEffect(() => {
107
58
  if (!UISettingsLoading) {
@@ -159,17 +110,26 @@ const ClusterWizardContextProvider = ({
159
110
 
160
111
  const handleMoveFromStaticIp = () => {
161
112
  //if static ip view change wasn't persisted, moving from static ip step should change the wizard steps to match the view in the infra env
162
- const staticIpInfo = infraEnv ? getStaticIpInfo(infraEnv) : undefined;
163
- if (!staticIpInfo) {
164
- throw `Wizard step is currently ${currentStepId}, but no static ip info is defined`;
113
+ if (installDisconnected) {
114
+ const staticIpInfo = disconnectedInfraEnv
115
+ ? getStaticIpInfo(disconnectedInfraEnv)
116
+ : undefined;
117
+ if (!staticIpInfo) {
118
+ throw `Wizard step is currently ${currentStepId}, but no static ip info is defined`;
119
+ }
120
+ setDisconnectedWizardStepIds(getDisconnectedWizardStepIds(staticIpInfo.view));
121
+ } else {
122
+ const staticIpInfo = infraEnv ? getStaticIpInfo(infraEnv) : undefined;
123
+ if (!staticIpInfo) {
124
+ throw `Wizard step is currently ${currentStepId}, but no static ip info is defined`;
125
+ }
126
+ const newStepIds = getWizardStepIds(
127
+ wizardStepIds,
128
+ staticIpInfo.view,
129
+ isSingleClusterFeatureEnabled,
130
+ );
131
+ setWizardStepIds(newStepIds);
165
132
  }
166
-
167
- const newStepIds = getWizardStepIds(
168
- wizardStepIds,
169
- staticIpInfo.view,
170
- isSingleClusterFeatureEnabled,
171
- );
172
- setWizardStepIds(newStepIds);
173
133
  };
174
134
 
175
135
  const onSetCurrentStepId = (stepId: ClusterWizardStepsType) => {
@@ -201,17 +161,33 @@ const ClusterWizardContextProvider = ({
201
161
  } else {
202
162
  setCurrentStepId('static-ip-network-wide-configurations');
203
163
  }
204
- setWizardStepIds(getWizardStepIds(wizardStepIds, view, isSingleClusterFeatureEnabled));
164
+ if (installDisconnected) {
165
+ setDisconnectedWizardStepIds(getDisconnectedWizardStepIds(view));
166
+ } else {
167
+ setWizardStepIds(getWizardStepIds(wizardStepIds, view, isSingleClusterFeatureEnabled));
168
+ }
205
169
  },
206
170
  onUpdateHostNetworkConfigType(type: HostsNetworkConfigurationType): void {
207
- if (type === HostsNetworkConfigurationType.STATIC) {
208
- setWizardStepIds(
209
- getWizardStepIds(wizardStepIds, StaticIpView.FORM, isSingleClusterFeatureEnabled),
210
- );
171
+ if (installDisconnected) {
172
+ if (type === HostsNetworkConfigurationType.STATIC) {
173
+ const staticIpInfo = disconnectedInfraEnv
174
+ ? getStaticIpInfo(disconnectedInfraEnv)
175
+ : undefined;
176
+ const view = staticIpInfo?.view ?? StaticIpView.FORM;
177
+ setDisconnectedWizardStepIds(getDisconnectedWizardStepIds(view));
178
+ } else {
179
+ setDisconnectedWizardStepIds(getDisconnectedWizardStepIds('dhcp-selected'));
180
+ }
211
181
  } else {
212
- setWizardStepIds(
213
- getWizardStepIds(wizardStepIds, 'dhcp-selected', isSingleClusterFeatureEnabled),
214
- );
182
+ if (type === HostsNetworkConfigurationType.STATIC) {
183
+ const staticIpInfo = infraEnv ? getStaticIpInfo(infraEnv) : undefined;
184
+ const view = staticIpInfo?.view ?? StaticIpView.FORM;
185
+ setWizardStepIds(getWizardStepIds(wizardStepIds, view, isSingleClusterFeatureEnabled));
186
+ } else {
187
+ setWizardStepIds(
188
+ getWizardStepIds(wizardStepIds, 'dhcp-selected', isSingleClusterFeatureEnabled),
189
+ );
190
+ }
215
191
  }
216
192
  },
217
193
  wizardStepIds: wizardStepIds,
@@ -225,6 +201,7 @@ const ClusterWizardContextProvider = ({
225
201
  setInstallDisconnected: (enabled: boolean) => {
226
202
  setInstallDisconnected(enabled);
227
203
  if (enabled) {
204
+ setDisconnectedWizardStepIds(disconnectedSteps);
228
205
  onSetCurrentStepId(disconnectedSteps[0]);
229
206
  } else {
230
207
  connectedWizardStepIds?.length && onSetCurrentStepId(connectedWizardStepIds[0]);
@@ -13,6 +13,30 @@ import { staticIpFormViewSubSteps, wizardStepNames } from './constants';
13
13
  import WizardNavItem from '../../../common/components/ui/WizardNavItem';
14
14
  import { Cluster } from '@openshift-assisted/types/assisted-installer-service';
15
15
 
16
+ export type WizardNavEntry = {
17
+ stepId: ClusterWizardStepsType;
18
+ visualNumber: number;
19
+ isStaticIpFormGroup: boolean;
20
+ };
21
+
22
+ export const getWizardNavEntries = (wizardStepIds: ClusterWizardStepsType[]): WizardNavEntry[] => {
23
+ const entries: WizardNavEntry[] = [];
24
+ let i = 0;
25
+ let visualNumber = 1;
26
+ while (i < wizardStepIds.length) {
27
+ const stepId = wizardStepIds[i];
28
+ if (stepId === 'static-ip-network-wide-configurations') {
29
+ entries.push({ stepId, visualNumber, isStaticIpFormGroup: true });
30
+ i += 2;
31
+ } else {
32
+ entries.push({ stepId, visualNumber, isStaticIpFormGroup: false });
33
+ i += 1;
34
+ }
35
+ visualNumber += 1;
36
+ }
37
+ return entries;
38
+ };
39
+
16
40
  const isStepValid = (stepId: ClusterWizardStepsType, cluster?: Cluster): boolean => {
17
41
  if (!cluster) {
18
42
  return true;
@@ -48,7 +72,11 @@ const ClusterWizardNavigation = ({ cluster }: { cluster?: Cluster }) => {
48
72
  return stepId === 'cluster-details' ? false : isStepIdxAfterCurrent(idx);
49
73
  };
50
74
 
51
- const getNavItem = (idx: number, stepId: ClusterWizardStepsType): ReactNode => {
75
+ const getNavItem = (
76
+ idx: number,
77
+ stepId: ClusterWizardStepsType,
78
+ visualNumber: number,
79
+ ): ReactNode => {
52
80
  return (
53
81
  <WizardNavItem
54
82
  stepIndex={idx}
@@ -60,11 +88,12 @@ const ClusterWizardNavigation = ({ cluster }: { cluster?: Cluster }) => {
60
88
  isDisabled={isStepDisabled(idx, stepId)}
61
89
  isValid={() => isStepValid(stepId, cluster)}
62
90
  data-testid={`cluster-wizard-nav-item-${stepId}`}
91
+ style={{ '--cluster-wizard-step-number': `"${visualNumber}"` } as React.CSSProperties}
63
92
  />
64
93
  );
65
94
  };
66
95
 
67
- const getStaticIpFormViewNavItem = (idx: number): ReactNode => {
96
+ const getStaticIpFormViewNavItem = (idx: number, visualNumber: number): ReactNode => {
68
97
  return (
69
98
  <WizardNavItem
70
99
  stepIndex={idx}
@@ -74,13 +103,14 @@ const ClusterWizardNavigation = ({ cluster }: { cluster?: Cluster }) => {
74
103
  isCurrent={isStaticIpStep(clusterWizardContext.currentStepId)}
75
104
  isDisabled={isStepIdxAfterCurrent(idx)}
76
105
  data-testid={`cluster-wizard-nav-item-static-network-configuration-form-view`}
106
+ style={{ '--cluster-wizard-step-number': `"${visualNumber}"` } as React.CSSProperties}
77
107
  >
78
108
  <WizardNav
79
109
  isInnerList
80
110
  data-testid="cluster-wizard-nav-item-static-network-configuration-form-view-inner-list"
81
111
  >
82
112
  {staticIpFormViewSubSteps.map((stepId, subIdx) => {
83
- return getNavItem(idx + subIdx, stepId);
113
+ return getNavItem(idx + subIdx, stepId, visualNumber);
84
114
  })}
85
115
  </WizardNav>
86
116
  </WizardNavItem>
@@ -88,20 +118,13 @@ const ClusterWizardNavigation = ({ cluster }: { cluster?: Cluster }) => {
88
118
  };
89
119
 
90
120
  const getWizardNavItems = (): ReactNode[] => {
91
- const navItems: ReactNode[] = [];
92
- let i = 0;
93
- while (i < clusterWizardContext.wizardStepIds.length) {
94
- const stepId = clusterWizardContext.wizardStepIds[i];
95
- if (stepId !== 'static-ip-network-wide-configurations') {
96
- navItems.push(getNavItem(i, stepId));
97
- i += 1;
98
- } else {
99
- navItems.push(getStaticIpFormViewNavItem(i));
100
- //skip iteration on form view sub steps
101
- i += 2;
121
+ return getWizardNavEntries(clusterWizardContext.wizardStepIds).map((entry) => {
122
+ const idx = clusterWizardContext.wizardStepIds.indexOf(entry.stepId);
123
+ if (entry.isStaticIpFormGroup) {
124
+ return getStaticIpFormViewNavItem(idx, entry.visualNumber);
102
125
  }
103
- }
104
- return navItems;
126
+ return getNavItem(idx, entry.stepId, entry.visualNumber);
127
+ });
105
128
  };
106
129
 
107
130
  return <WizardNav data-testid="cluster-wizard-nav">{getWizardNavItems()}</WizardNav>;
@@ -5,6 +5,7 @@ import { useClusterWizardContext } from './ClusterWizardContext';
5
5
  import ReviewStep from './disconnected/ReviewStep';
6
6
  import BasicStep from './disconnected/BasicStep';
7
7
  import OptionalConfigurationsStep from './disconnected/OptionalConfigurationsStep';
8
+ import DisconnectedStaticIp from './disconnected/DisconnectedStaticIp';
8
9
  import { ClusterWizardStepsType } from './wizardTransition';
9
10
  import { useInfraEnv } from '../../hooks';
10
11
  import { CpuArchitecture } from '../../../common/types';
@@ -18,6 +19,10 @@ const getCurrentStep = (currentStepId: ClusterWizardStepsType, infraEnv?: InfraE
18
19
  return <OptionalConfigurationsStep />;
19
20
  case 'disconnected-basic':
20
21
  return <BasicStep />;
22
+ case 'static-ip-yaml-view':
23
+ case 'static-ip-network-wide-configurations':
24
+ case 'static-ip-host-configurations':
25
+ return <DisconnectedStaticIp />;
21
26
  default:
22
27
  return <ClusterDetails infraEnv={infraEnv} />;
23
28
  }
@@ -23,7 +23,7 @@ const getInitialFormStateProps = () => {
23
23
  isEmpty: true,
24
24
  };
25
25
  };
26
- const StaticIp: React.FC<StaticIpProps & { cluster: Cluster }> = ({
26
+ const StaticIp: React.FC<StaticIpProps & { cluster?: Cluster }> = ({
27
27
  cluster,
28
28
  infraEnv,
29
29
  updateInfraEnv,
@@ -8,7 +8,15 @@ import {
8
8
  Validation as HostValidation,
9
9
  } from '../../../common/types/hosts';
10
10
 
11
- import { canNextClusterDetails, canNextHostDiscovery, canNextStorage } from './wizardTransition';
11
+ import {
12
+ canNextClusterDetails,
13
+ canNextHostDiscovery,
14
+ canNextStorage,
15
+ disconnectedSteps,
16
+ } from './wizardTransition';
17
+ import { getWizardNavEntries } from './ClusterWizardNavigation';
18
+ import { getDisconnectedWizardStepIds } from './utils';
19
+ import { StaticIpView } from '../clusterConfiguration/staticIp/data/dataTypes';
12
20
  import { Cluster, Host } from '@openshift-assisted/types/assisted-installer-service';
13
21
 
14
22
  const clusterBase: Cluster = {
@@ -441,3 +449,54 @@ describe('OCM wizard transition of Storage', () => {
441
449
  expect(canNextStorage({ cluster })).toBe(true);
442
450
  });
443
451
  });
452
+
453
+ describe('Disconnected wizard step ids', () => {
454
+ test('starts with the three base disconnected steps', () => {
455
+ expect(getDisconnectedWizardStepIds()).toEqual(disconnectedSteps);
456
+ expect(getDisconnectedWizardStepIds('dhcp-selected')).toEqual(disconnectedSteps);
457
+ });
458
+
459
+ test('inserts static IP form steps and removes them again for DHCP', () => {
460
+ const withStatic = getDisconnectedWizardStepIds(StaticIpView.FORM);
461
+ expect(withStatic).toEqual([
462
+ 'disconnected-basic',
463
+ 'disconnected-optional-configurations',
464
+ 'static-ip-network-wide-configurations',
465
+ 'static-ip-host-configurations',
466
+ 'disconnected-review',
467
+ ]);
468
+ expect(getDisconnectedWizardStepIds('dhcp-selected')).toEqual(disconnectedSteps);
469
+ });
470
+
471
+ test('inserts the YAML static IP step', () => {
472
+ expect(getDisconnectedWizardStepIds(StaticIpView.YAML)).toEqual([
473
+ 'disconnected-basic',
474
+ 'disconnected-optional-configurations',
475
+ 'static-ip-yaml-view',
476
+ 'disconnected-review',
477
+ ]);
478
+ });
479
+ });
480
+
481
+ describe('Wizard nav visual numbers', () => {
482
+ test('numbers review as 3 when static IP steps are absent', () => {
483
+ const entries = getWizardNavEntries(disconnectedSteps);
484
+ expect(entries.map((entry) => [entry.stepId, entry.visualNumber])).toEqual([
485
+ ['disconnected-basic', 1],
486
+ ['disconnected-optional-configurations', 2],
487
+ ['disconnected-review', 3],
488
+ ]);
489
+ });
490
+
491
+ test('keeps a single number for form-view static IP and numbers review as 4', () => {
492
+ const entries = getWizardNavEntries(getDisconnectedWizardStepIds(StaticIpView.FORM));
493
+ expect(
494
+ entries.map((entry) => [entry.stepId, entry.visualNumber, entry.isStaticIpFormGroup]),
495
+ ).toEqual([
496
+ ['disconnected-basic', 1, false],
497
+ ['disconnected-optional-configurations', 2, false],
498
+ ['static-ip-network-wide-configurations', 3, true],
499
+ ['disconnected-review', 4, false],
500
+ ]);
501
+ });
502
+ });
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { ClusterWizardStep, TechnologyPreview, StaticTextField } from '../../../../common';
2
+ import { ClusterWizardStep, StaticTextField } from '../../../../common';
3
3
  import { Flex, Grid, GridItem, Form, Content } from '@patternfly/react-core';
4
4
  import { useClusterWizardContext } from '../ClusterWizardContext';
5
5
  import ClusterWizardFooter from '../ClusterWizardFooter';
@@ -7,7 +7,7 @@ import ClusterWizardNavigation from '../ClusterWizardNavigation';
7
7
  import { WithErrorBoundary } from '../../../../common/components/ErrorHandling/WithErrorBoundary';
8
8
  import InstallDisconnectedSwitch from './InstallDisconnectedSwitch';
9
9
 
10
- export const DISCONNECTED_OPENSHIFT_VERSION = '4.21';
10
+ export const DISCONNECTED_OPENSHIFT_VERSION = '4.22.13';
11
11
 
12
12
  const BasicStep = () => {
13
13
  const { moveNext } = useClusterWizardContext();
@@ -24,7 +24,6 @@ const BasicStep = () => {
24
24
  </GridItem>
25
25
  <GridItem>
26
26
  <Flex alignItems={{ default: 'alignItemsCenter' }} gap={{ default: 'gapSm' }}>
27
- <TechnologyPreview />
28
27
  <InstallDisconnectedSwitch />
29
28
  <span>I'm installing on a disconnected/air-gapped/secured environment</span>
30
29
  </Flex>
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { InfraEnvUpdateParams } from '@openshift-assisted/types/assisted-installer-service';
3
+ import { useClusterWizardContext } from '../ClusterWizardContext';
4
+ import { InfraEnvsAPI } from '../../../services/apis';
5
+ import StaticIp from '../StaticIp';
6
+
7
+ export const DisconnectedStaticIp: React.FC = () => {
8
+ const { disconnectedInfraEnv, setDisconnectedInfraEnv } = useClusterWizardContext();
9
+
10
+ const updateInfraEnv = React.useCallback(
11
+ async (params: InfraEnvUpdateParams) => {
12
+ if (!disconnectedInfraEnv?.id) {
13
+ throw new Error('No disconnected infraEnv available');
14
+ }
15
+ const { data: updatedInfraEnv } = await InfraEnvsAPI.update(disconnectedInfraEnv.id, params);
16
+ setDisconnectedInfraEnv(updatedInfraEnv);
17
+ return updatedInfraEnv;
18
+ },
19
+ [disconnectedInfraEnv, setDisconnectedInfraEnv],
20
+ );
21
+
22
+ if (!disconnectedInfraEnv) {
23
+ return null;
24
+ }
25
+
26
+ return <StaticIp infraEnv={disconnectedInfraEnv} updateInfraEnv={updateInfraEnv} />;
27
+ };
28
+
29
+ export default DisconnectedStaticIp;
@@ -1,8 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { Formik, useFormikContext } from 'formik';
3
3
  import * as Yup from 'yup';
4
- import { AlertVariant } from '@patternfly/react-core';
5
- import { Form, Grid, GridItem, Content } from '@patternfly/react-core';
4
+ import { AlertVariant, Form, Grid, GridItem, Content } from '@patternfly/react-core';
5
+ import {
6
+ HostStaticNetworkConfig,
7
+ InfraEnv,
8
+ InfraEnvUpdateParams,
9
+ ImageType,
10
+ } from '@openshift-assisted/types/assisted-installer-service';
6
11
  import {
7
12
  ClusterWizardStep,
8
13
  PullSecret,
@@ -14,15 +19,21 @@ import {
14
19
  sshPublicKeyValidationSchema,
15
20
  useAlerts,
16
21
  useTranslation,
22
+ InputField,
23
+ ipValidationSchema,
24
+ getFormikErrorFields,
17
25
  } from '../../../../common';
18
26
  import { WithErrorBoundary } from '../../../../common/components/ErrorHandling/WithErrorBoundary';
19
27
  import { useClusterWizardContext } from '../ClusterWizardContext';
20
28
  import ClusterWizardFooter from '../ClusterWizardFooter';
21
29
  import ClusterWizardNavigation from '../ClusterWizardNavigation';
22
30
  import { InfraEnvsAPI, ClustersAPI } from '../../../services/apis';
23
- import { ImageType } from '@openshift-assisted/types/assisted-installer-service';
24
31
  import usePullSecret from '../../../hooks/usePullSecret';
25
32
  import { DISCONNECTED_OPENSHIFT_VERSION } from './BasicStep';
33
+ import { HostsNetworkConfigurationControlGroup } from '../../clusterConfiguration/HostsNetworkConfigurationControlGroup';
34
+ import { HostsNetworkConfigurationType } from '../../../services/types';
35
+ import { getDummyInfraEnvField } from '../../clusterConfiguration/staticIp/data/dummyData';
36
+ import { getStaticNetworkConfig } from '../../clusterConfiguration/staticIp/data/fromInfraEnv';
26
37
 
27
38
  const DISCONNECTED_IMAGE_TYPE: ImageType = 'disconnected-iso';
28
39
  const DISCONNECTED_CLUSTER_NAME = 'disconnected-cluster';
@@ -30,6 +41,8 @@ const DISCONNECTED_CLUSTER_NAME = 'disconnected-cluster';
30
41
  type OptionalConfigurationsValues = {
31
42
  sshPublicKey: string;
32
43
  pullSecret: string;
44
+ rendezvousIp: string;
45
+ hostsNetworkConfigurationType: HostsNetworkConfigurationType;
33
46
  };
34
47
 
35
48
  type OptionalConfigurationsFormProps = {
@@ -42,7 +55,8 @@ const OptionalConfigurationsForm: React.FC<OptionalConfigurationsFormProps> = ({
42
55
  isSubmitting,
43
56
  }) => {
44
57
  const { moveBack } = useClusterWizardContext();
45
- const { isValid, submitForm } = useFormikContext<OptionalConfigurationsValues>();
58
+ const { isValid, submitForm, errors, touched } = useFormikContext<OptionalConfigurationsValues>();
59
+ const errorFields = getFormikErrorFields(errors, touched);
46
60
 
47
61
  return (
48
62
  <ClusterWizardStep
@@ -53,6 +67,7 @@ const OptionalConfigurationsForm: React.FC<OptionalConfigurationsFormProps> = ({
53
67
  onBack={moveBack}
54
68
  isSubmitting={isSubmitting}
55
69
  isNextDisabled={!isValid}
70
+ errorFields={errorFields}
56
71
  />
57
72
  }
58
73
  >
@@ -63,8 +78,15 @@ const OptionalConfigurationsForm: React.FC<OptionalConfigurationsFormProps> = ({
63
78
  </GridItem>
64
79
  <GridItem>
65
80
  <Form id="wizard-cluster-optional-config__form">
81
+ <InputField
82
+ label="Rendezvous IP"
83
+ name="rendezvousIp"
84
+ helperText="The IP address that hosts will use to communicate with the bootstrap node during installation."
85
+ maxLength={45}
86
+ />
66
87
  <UploadSSH />
67
88
  {!isInOcm && <PullSecret isOcm={false} defaultPullSecret={defaultPullSecret} />}
89
+ <HostsNetworkConfigurationControlGroup clusterExists={false} isDisabled={false} />
68
90
  </Form>
69
91
  </GridItem>
70
92
  </Grid>
@@ -73,6 +95,29 @@ const OptionalConfigurationsForm: React.FC<OptionalConfigurationsFormProps> = ({
73
95
  );
74
96
  };
75
97
 
98
+ const getStaticNetworkConfigUpdate = (
99
+ values: OptionalConfigurationsValues,
100
+ infraEnv: InfraEnv | undefined,
101
+ ): HostStaticNetworkConfig[] => {
102
+ if (values.hostsNetworkConfigurationType === HostsNetworkConfigurationType.DHCP) {
103
+ return [];
104
+ }
105
+ // Static IP selected — resend existing config, or seed with dummy if none exists yet.
106
+ if (!infraEnv?.staticNetworkConfig) {
107
+ return getDummyInfraEnvField();
108
+ }
109
+ return getStaticNetworkConfig(infraEnv) ?? getDummyInfraEnvField();
110
+ };
111
+
112
+ const buildInfraEnvUpdateParams = (
113
+ values: OptionalConfigurationsValues,
114
+ disconnectedInfraEnv: InfraEnv | undefined,
115
+ ): InfraEnvUpdateParams => ({
116
+ sshAuthorizedKey: values.sshPublicKey,
117
+ rendezvousIp: values.rendezvousIp,
118
+ staticNetworkConfig: getStaticNetworkConfigUpdate(values, disconnectedInfraEnv),
119
+ });
120
+
76
121
  const OptionalConfigurationsStep = () => {
77
122
  const { t } = useTranslation();
78
123
  const {
@@ -91,6 +136,9 @@ const OptionalConfigurationsStep = () => {
91
136
  Yup.object({
92
137
  sshPublicKey: sshPublicKeyValidationSchema(t),
93
138
  pullSecret: isInOcm ? Yup.string() : pullSecretValidationSchema(t),
139
+ rendezvousIp: Yup.string()
140
+ .max(45, 'IP address must be at most 45 characters')
141
+ .concat(ipValidationSchema(t)),
94
142
  }),
95
143
  [t],
96
144
  );
@@ -98,6 +146,10 @@ const OptionalConfigurationsStep = () => {
98
146
  const initialValues: OptionalConfigurationsValues = {
99
147
  sshPublicKey: disconnectedInfraEnv?.sshAuthorizedKey ?? '',
100
148
  pullSecret: defaultPullSecret ?? '',
149
+ rendezvousIp: disconnectedInfraEnv?.rendezvousIp ?? '',
150
+ hostsNetworkConfigurationType: disconnectedInfraEnv?.staticNetworkConfig
151
+ ? HostsNetworkConfigurationType.STATIC
152
+ : HostsNetworkConfigurationType.DHCP,
101
153
  };
102
154
 
103
155
  const handleNext = React.useCallback(
@@ -107,12 +159,9 @@ const OptionalConfigurationsStep = () => {
107
159
  try {
108
160
  const pullSecretToUse = isInOcm ? defaultPullSecret ?? '' : values.pullSecret;
109
161
 
110
- if (disconnectedCluster?.id && disconnectedInfraEnv?.id) {
111
- const { data: updatedInfraEnv } = await InfraEnvsAPI.update(disconnectedInfraEnv.id, {
112
- sshAuthorizedKey: values.sshPublicKey,
113
- });
114
- setDisconnectedInfraEnv(updatedInfraEnv);
115
- } else {
162
+ let infraEnvToUse: InfraEnv | undefined = disconnectedInfraEnv;
163
+
164
+ if (!disconnectedCluster?.id || !infraEnvToUse?.id) {
116
165
  const { data: cluster } = await ClustersAPI.registerDisconnected({
117
166
  name: DISCONNECTED_CLUSTER_NAME,
118
167
  openshiftVersion: DISCONNECTED_OPENSHIFT_VERSION,
@@ -126,10 +175,22 @@ const OptionalConfigurationsStep = () => {
126
175
  imageType: DISCONNECTED_IMAGE_TYPE,
127
176
  openshiftVersion: DISCONNECTED_OPENSHIFT_VERSION,
128
177
  sshAuthorizedKey: values.sshPublicKey || undefined,
178
+ rendezvousIp: values.rendezvousIp || undefined,
179
+ staticNetworkConfig:
180
+ values.hostsNetworkConfigurationType === HostsNetworkConfigurationType.STATIC
181
+ ? getDummyInfraEnvField()
182
+ : undefined,
129
183
  });
130
- setDisconnectedInfraEnv(createdInfraEnv);
184
+ infraEnvToUse = createdInfraEnv;
185
+ } else {
186
+ const { data: updatedInfraEnv } = await InfraEnvsAPI.update(
187
+ infraEnvToUse.id,
188
+ buildInfraEnvUpdateParams(values, infraEnvToUse),
189
+ );
190
+ infraEnvToUse = updatedInfraEnv;
131
191
  }
132
192
 
193
+ setDisconnectedInfraEnv(infraEnvToUse);
133
194
  moveNext();
134
195
  } catch (error) {
135
196
  handleApiError(error, () => {
@@ -158,7 +219,6 @@ const OptionalConfigurationsStep = () => {
158
219
  return (
159
220
  <Formik<OptionalConfigurationsValues>
160
221
  initialValues={initialValues}
161
- enableReinitialize
162
222
  validationSchema={validationSchema}
163
223
  onSubmit={(values) => void handleNext(values)}
164
224
  >