diff --git a/libs/ui-lib/lib/common/api/assisted-service/HostsAPI.ts b/libs/ui-lib/lib/common/api/assisted-service/HostsAPI.ts index bdfe75efbf..18d8b69464 100644 --- a/libs/ui-lib/lib/common/api/assisted-service/HostsAPI.ts +++ b/libs/ui-lib/lib/common/api/assisted-service/HostsAPI.ts @@ -1,4 +1,5 @@ import { + Cluster, Host, HostUpdateParams, InfraEnv, @@ -62,6 +63,18 @@ const HostsAPI = { `${HostsAPI.makeActionsBaseURI(infraEnvId, hostId)}/install`, ); }, + bindHost(infraEnvId: InfraEnv['id'], hostId: Host['id'], clusterId: Cluster['id']) { + return client.post, { cluster_id: string }>( + `${HostsAPI.makeActionsBaseURI(infraEnvId, hostId)}/bind`, + { cluster_id: clusterId }, + { + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + ); + }, }; export default HostsAPI; diff --git a/libs/ui-lib/lib/common/api/assisted-service/InfraEnvsAPI.ts b/libs/ui-lib/lib/common/api/assisted-service/InfraEnvsAPI.ts index c637e38249..ff99a79c4f 100644 --- a/libs/ui-lib/lib/common/api/assisted-service/InfraEnvsAPI.ts +++ b/libs/ui-lib/lib/common/api/assisted-service/InfraEnvsAPI.ts @@ -4,6 +4,7 @@ import { InfraEnvCreateParams, PresignedUrl, InfraEnvUpdateParams, + Host, } from '@openshift-assisted/types/assisted-installer-service'; import { AxiosResponse } from 'axios'; @@ -67,6 +68,11 @@ const InfraEnvsAPI = { `${InfraEnvsAPI.makeBaseURI(infraEnvId)}/downloads/files-presigned?file_name=ipxe-script`, ); }, + getHosts(infraEnvId: InfraEnv['id']) { + return client.get(`${InfraEnvsAPI.makeBaseURI(infraEnvId)}/hosts`, { + signal: _getRequestAbortController.signal, + }); + }, }; export default InfraEnvsAPI; diff --git a/libs/ui-lib/lib/common/reducers/alertsSlice.ts b/libs/ui-lib/lib/common/reducers/alertsSlice.ts index c300f808ae..9c905eb0b9 100644 --- a/libs/ui-lib/lib/common/reducers/alertsSlice.ts +++ b/libs/ui-lib/lib/common/reducers/alertsSlice.ts @@ -6,6 +6,7 @@ export type AlertPayload = { title: string; message?: string; variant?: AlertVariant; + key?: string; }; export type AlertProps = { @@ -22,7 +23,7 @@ export const alertsSlice = createSlice({ name: 'alerts', reducers: { addAlert: (state, action: PayloadAction) => [ - { key: uuidv4(), variant: AlertVariant.danger, ...action.payload }, + { key: action.payload.key || uuidv4(), variant: AlertVariant.danger, ...action.payload }, ...state, ], removeAlert: (state, action: PayloadAction) => diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/HostDiscovery.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/HostDiscovery.tsx index dc3e4b5f82..17bde61c10 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/HostDiscovery.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/HostDiscovery.tsx @@ -22,6 +22,7 @@ import { Cluster, V2ClusterUpdateParams, } from '@openshift-assisted/types/assisted-installer-service'; +import useLateBinding from '../../hooks/useLateBinding'; const HostDiscoveryForm = ({ cluster }: { cluster: Cluster }) => { const { alerts } = useAlerts(); @@ -29,12 +30,14 @@ const HostDiscoveryForm = ({ cluster }: { cluster: Cluster }) => { const clusterWizardContext = useClusterWizardContext(); const isAutoSaveRunning = useFormikAutoSave(); const errorFields = getFormikErrorFields(errors, touched); + const isBinding = useLateBinding(cluster); const isNextDisabled = !isValid || !!alerts.length || isAutoSaveRunning || isSubmitting || + isBinding || !canNextHostDiscovery({ cluster }); const footer = ( diff --git a/libs/ui-lib/lib/ocm/components/hosts/HostsDiscoveryTable.tsx b/libs/ui-lib/lib/ocm/components/hosts/HostsDiscoveryTable.tsx index 5f655efb05..3bd2a6360c 100644 --- a/libs/ui-lib/lib/ocm/components/hosts/HostsDiscoveryTable.tsx +++ b/libs/ui-lib/lib/ocm/components/hosts/HostsDiscoveryTable.tsx @@ -55,6 +55,7 @@ const HostsDiscoveryTable = ({ cluster }: HostsDiscoveryTableProps) => { onMassDeleteHost, ...modalProps } = useHostsTable(cluster); + const { wizardPerPage, setWizardPerPage } = useClusterWizardContext(); const { isViewerMode } = useSelector(selectCurrentClusterPermissionsState); diff --git a/libs/ui-lib/lib/ocm/hooks/index.ts b/libs/ui-lib/lib/ocm/hooks/index.ts index 93ac1f0974..c23510fdc5 100644 --- a/libs/ui-lib/lib/ocm/hooks/index.ts +++ b/libs/ui-lib/lib/ocm/hooks/index.ts @@ -5,4 +5,5 @@ export { default as usePullSecret } from './usePullSecret'; export { default as useClusterPreflightRequirements } from './useClusterPreflightRequirements'; export { default as useUISettings } from './useUISettings'; export { default as useInfraEnv } from './useInfraEnv'; +export { default as useInfraEnvHosts } from './useInfraEnvHosts'; export { useFeatureDetection } from './use-feature-detection'; diff --git a/libs/ui-lib/lib/ocm/hooks/useInfraEnvHosts.ts b/libs/ui-lib/lib/ocm/hooks/useInfraEnvHosts.ts new file mode 100644 index 0000000000..009ba63145 --- /dev/null +++ b/libs/ui-lib/lib/ocm/hooks/useInfraEnvHosts.ts @@ -0,0 +1,60 @@ +import React from 'react'; +import useInfraEnvId from './useInfraEnvId'; +import { CpuArchitecture, DEFAULT_POLLING_INTERVAL } from '../../common'; +import { getErrorMessage } from '../../common/utils'; +import { InfraEnvsAPI } from '../services/apis'; +import InfraEnvIdsCacheService from '../services/InfraEnvIdsCacheService'; +import { Cluster, Host } from '@openshift-assisted/types/assisted-installer-service'; + +const useInfraEnvHosts = ( + clusterId: Cluster['id'], + cpuArchitecture: CpuArchitecture, + clusterName?: string, + pullSecret?: string, + openshiftVersion?: string, +) => { + const [hosts, setHosts] = React.useState(); + const [error, setError] = React.useState(''); + const { infraEnvId, error: infraEnvIdError } = useInfraEnvId( + clusterId, + cpuArchitecture, + clusterName, + pullSecret, + openshiftVersion, + ); + + const getHosts = React.useCallback(async () => { + try { + if (infraEnvId) { + const { data: hostsData } = await InfraEnvsAPI.getHosts(infraEnvId); + setHosts(hostsData); + } + } catch (e) { + // Invalidate this cluster's cached data + InfraEnvIdsCacheService.removeInfraEnvId(clusterId, cpuArchitecture); + setError(getErrorMessage(e)); + } + }, [clusterId, cpuArchitecture, infraEnvId]); + + React.useEffect(() => { + if (infraEnvIdError) { + setHosts(undefined); + setError(infraEnvIdError); + } else { + if (infraEnvId) { + // Initial fetch + void getHosts(); + + // Set up polling to refetch hosts periodically + const intervalId = setInterval(() => { + void getHosts(); + }, DEFAULT_POLLING_INTERVAL); + + return () => clearInterval(intervalId); + } + } + }, [getHosts, infraEnvId, infraEnvIdError]); + + return { hosts, error, isLoading: !hosts && !error }; +}; +export default useInfraEnvHosts; diff --git a/libs/ui-lib/lib/ocm/hooks/useLateBinding.ts b/libs/ui-lib/lib/ocm/hooks/useLateBinding.ts new file mode 100644 index 0000000000..10a6667419 --- /dev/null +++ b/libs/ui-lib/lib/ocm/hooks/useLateBinding.ts @@ -0,0 +1,71 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Cluster } from '@openshift-assisted/types/assisted-installer-service'; +import { CpuArchitecture, useAlerts } from '../../common'; +import useInfraEnvHosts from './useInfraEnvHosts'; +import HostsService from '../services/HostsService'; +import { getErrorMessage } from '../../common/utils'; +import { useFeature } from './use-feature'; + +const useLateBinding = (cluster: Cluster): boolean => { + const [isBinding, setIsBinding] = useState(false); + const { addAlert, removeAlert, alerts } = useAlerts(); + const isSingleClusterFeatureEnabled = useFeature('ASSISTED_INSTALLER_SINGLE_CLUSTER_FEATURE'); + + const { + hosts: infraEnvHosts, + error: infraEnvError, + isLoading: infraEnvLoading, + } = useInfraEnvHosts( + isSingleClusterFeatureEnabled ? cluster.id : '', + cluster.cpuArchitecture + ? (cluster.cpuArchitecture as CpuArchitecture) + : CpuArchitecture.USE_DAY1_ARCHITECTURE, + cluster.name, + undefined, + cluster.openshiftVersion, + ); + + const bindHosts = useCallback(async () => { + if (infraEnvHosts && !infraEnvError && !infraEnvLoading && isSingleClusterFeatureEnabled) { + for (const host of infraEnvHosts) { + if (host.clusterId !== cluster.id) { + setIsBinding(true); + try { + const alertKey = alerts.find((alert) => alert.key === host.id)?.key; + if (alertKey) { + removeAlert(alertKey); + } + await HostsService.bind(host, cluster.id); + } catch (error) { + addAlert({ + title: `Failed to bind host ${host.requestedHostname || ''}`, + message: getErrorMessage(error), + key: host.id, + }); + } finally { + setIsBinding(false); + } + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + infraEnvHosts, + infraEnvError, + infraEnvLoading, + isSingleClusterFeatureEnabled, + cluster.id, + addAlert, + removeAlert, + ]); + + useEffect(() => { + if (isSingleClusterFeatureEnabled) { + void bindHosts(); + } + }, [isSingleClusterFeatureEnabled, bindHosts]); + + return infraEnvLoading || isBinding; +}; + +export default useLateBinding; diff --git a/libs/ui-lib/lib/ocm/services/HostsService.ts b/libs/ui-lib/lib/ocm/services/HostsService.ts index a4f3bcbace..d43d936639 100644 --- a/libs/ui-lib/lib/ocm/services/HostsService.ts +++ b/libs/ui-lib/lib/ocm/services/HostsService.ts @@ -1,4 +1,5 @@ import { + Cluster, Disk, DiskRole, Host, @@ -84,6 +85,14 @@ const HostsService = { return HostsAPI.installHost(host.infraEnvId, host.id); }, + bind(host: Host, clusterId: Cluster['id']) { + if (!host.infraEnvId) { + throw new Error(`Cannot bind host ${host.id}, missing infraEnvId`); + } + + return HostsAPI.bindHost(host.infraEnvId, host.id, clusterId); + }, + installAll(hosts: Host[]) { const promises = []; for (const host of hosts) {