diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx index f7341039c93..687d84fd794 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx @@ -53,6 +53,7 @@ const CreateVMWizardFooterComponent: React.FC { const jumpToStepID = (isValid && diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx index 24f15599862..8d6c9ba32d2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx @@ -2,12 +2,8 @@ import * as React from 'react'; import * as _ from 'lodash'; import { connect } from 'react-redux'; import { createVm, createVmTemplate } from 'kubevirt-web-ui-components'; -import { Wizard } from '@patternfly/react-core'; -import { - PersistentVolumeClaimModel, - StorageClassModel, - TemplateModel, -} from '@console/internal/models'; +import { Wizard, WizardStep } from '@patternfly/react-core'; +import { TemplateModel } from '@console/internal/models'; import { Firehose, history, @@ -15,13 +11,15 @@ import { makeReduxID, units, } from '@console/internal/components/utils'; +import { k8sGet, TemplateKind } from '@console/internal/module/k8s'; import { withReduxID } from '../../utils/redux/common'; +import { VirtualMachineModel } from '../../models'; import { - DataVolumeModel, - NetworkAttachmentDefinitionModel, - VirtualMachineModel, -} from '../../models'; -import { TEMPLATE_TYPE_BASE, TEMPLATE_TYPE_LABEL, TEMPLATE_TYPE_VM } from '../../constants/vm'; + TEMPLATE_TYPE_BASE, + TEMPLATE_TYPE_LABEL, + TEMPLATE_TYPE_VM, + VolumeType, +} from '../../constants/vm'; import { getResource } from '../../utils'; import { EnhancedK8sMethods } from '../../k8s/enhancedK8sMethods/enhancedK8sMethods'; import { cleanupAndGetResults, getResults } from '../../k8s/enhancedK8sMethods/k8sMethodsUtils'; @@ -33,13 +31,25 @@ import { } from '../../utils/immutable'; import { getTemplateOperatingSystems } from '../../selectors/vm-template/advanced'; import { ResultsWrapper } from '../../k8s/enhancedK8sMethods/types'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; +import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; +import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; +import { DiskWrapper } from '../../k8s/wrapper/vm/disk-wrapper'; +import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; +import { + getDefaultSCAccessMode, + getDefaultSCVolumeMode, +} from '../../selectors/config-map/sc-defaults'; +import { getStorageClassConfigMap } from '../../k8s/requests/config-map/storage-class'; import { ChangedCommonData, CommonData, CreateVMWizardComponentProps, DetectCommonDataChanges, VMSettingsField, + VMWizardNetwork, VMWizardProps, + VMWizardStorage, VMWizardTab, } from './types'; import { CREATE_VM, CREATE_VM_TEMPLATE, TabTitleResolver } from './strings/strings'; @@ -47,14 +57,107 @@ import { vmWizardActions } from './redux/actions'; import { ActionType } from './redux/types'; import { iGetCommonData, iGetCreateVMWizardTabs } from './selectors/immutable/selectors'; import { isStepLocked, isStepPending, isStepValid } from './selectors/immutable/wizard-selectors'; -import { VMSettingsTab } from './tabs/vm-settings-tab/vm-settings-tab'; import { ResourceLoadErrors } from './resource-load-errors'; import { CreateVMWizardFooter } from './create-vm-wizard-footer'; +import { VMSettingsTab } from './tabs/vm-settings-tab/vm-settings-tab'; +import { NetworkingTab } from './tabs/networking-tab/networking-tab'; import { ReviewTab } from './tabs/review-tab/review-tab'; import { ResultTab } from './tabs/result-tab/result-tab'; +import { StorageTab } from './tabs/storage-tab/storage-tab'; +import { CloudInitTab } from './tabs/cloud-init-tab/cloud-init-tab'; import './create-vm-wizard.scss'; +// TODO remove after moving create functions from kubevirt-web-ui-components +/** * + * kubevirt-web-ui-components InterOP + */ +const kubevirtInterOP = async ({ + activeNamespace, + vmSettings, + networks, + storages, + templates, +}: { + activeNamespace: string; + vmSettings: any; + networks: VMWizardNetwork[]; + storages: VMWizardStorage[]; + templates: TemplateKind[]; +}) => { + const clonedVMsettings = _.cloneDeep(vmSettings); + const clonedNetworks = _.cloneDeep(networks); + const clonedStorages = _.cloneDeep(storages); + + clonedVMsettings.namespace = { value: activeNamespace }; + const operatingSystems = getTemplateOperatingSystems(templates); + const osField = clonedVMsettings[VMSettingsField.OPERATING_SYSTEM]; + const osID = osField.value; + osField.value = operatingSystems.find(({ id }) => id === osID); + + const interOPNetworks = clonedNetworks.map(({ networkInterface, network }) => { + const networkInterfaceWrapper = NetworkInterfaceWrapper.initialize(networkInterface); + const networkWrapper = NetworkWrapper.initialize(network); + + return { + name: networkInterfaceWrapper.getName(), + mac: networkInterfaceWrapper.getMACAddress(), + binding: networkInterfaceWrapper.getTypeValue(), + isBootable: networkInterfaceWrapper.isFirstBootableDevice(), + network: networkWrapper.getReadableName(), + networkType: networkWrapper.getTypeValue(), + templateNetwork: { + network, + interface: networkInterface, + }, + }; + }); + + const storageClassConfigMap = await getStorageClassConfigMap({ k8sGet }); + + const interOPStorages = clonedStorages.map(({ disk, volume, dataVolume }) => { + const diskWrapper = DiskWrapper.initialize(disk); + const volumeWrapper = VolumeWrapper.initialize(volume); + const dataVolumeWrapper = dataVolume && DataVolumeWrapper.initialize(dataVolume); + + return { + name: diskWrapper.getName(), + isBootable: diskWrapper.isFirstBootableDevice(), + storageType: + volumeWrapper.getType() === VolumeType.DATA_VOLUME && dataVolume ? 'datavolume' : undefined, + templateStorage: { + volume, + disk, + dataVolumeTemplate: dataVolumeWrapper + ? DataVolumeWrapper.mergeWrappers( + dataVolumeWrapper, + DataVolumeWrapper.initializeFromSimpleData({ + accessModes: + dataVolumeWrapper.getAccessModes() || + getDefaultSCAccessMode( + storageClassConfigMap, + dataVolumeWrapper.getStorageClassName(), + ), + volumeMode: + dataVolumeWrapper.getVolumeMode() || + getDefaultSCVolumeMode( + storageClassConfigMap, + dataVolumeWrapper.getStorageClassName(), + ), + }), + ).asResource() + : undefined, + }, + }; + }); + + return { + interOPVMSettings: clonedVMsettings, + interOPNetworks, + interOPStorages, + }; +}; + export class CreateVMWizardComponent extends React.Component { private isClosed = false; @@ -87,12 +190,18 @@ export class CreateVMWizardComponent extends React.Component { this.props.onResultsChanged({ errors: [], requestResults: [] }, null, true, true); // reset const create = this.props.isCreateTemplate ? createVmTemplate : createVm; const enhancedK8sMethods = new EnhancedK8sMethods(); const vmSettings = iGetIn(this.props.stepData, [VMWizardTab.VM_SETTINGS, 'value']).toJS(); + const networks = immutableListToShallowJS( + iGetIn(this.props.stepData, [VMWizardTab.NETWORKING, 'value']), + ); + const storages = immutableListToShallowJS( + iGetIn(this.props.stepData, [VMWizardTab.STORAGE, 'value']), + ); const templates = immutableListToShallowJS( concatImmutableLists( iGetLoadedData(this.props[VMWizardProps.commonTemplates]), @@ -100,26 +209,21 @@ export class CreateVMWizardComponent extends React.Component id === osID); - /** - * END kubevirt-web-ui-components InterOP - */ + const { interOPVMSettings, interOPNetworks, interOPStorages } = await kubevirtInterOP({ + vmSettings, + networks, + storages, + templates, + activeNamespace: this.props.activeNamespace, + }); create( enhancedK8sMethods, templates, - vmSettings, - iGetIn(this.props.stepData, [VMWizardTab.NETWORKS, 'value']).toJS(), - iGetIn(this.props.stepData, [VMWizardTab.STORAGE, 'value']).toJS(), - immutableListToShallowJS(iGetLoadedData(this.props[VMWizardProps.persistentVolumeClaims])), + interOPVMSettings, + interOPNetworks, + interOPStorages, + [], units, ) .then(() => getResults(enhancedK8sMethods)) @@ -128,7 +232,7 @@ export class CreateVMWizardComponent extends React.Component console.error(e)); // eslint-disable-line no-console - } + }; render() { const { isCreateTemplate, reduxID, stepData } = this.props; @@ -148,12 +252,38 @@ export class CreateVMWizardComponent extends React.Component ), }, - // { - // id: VMWizardTab.NETWORKS, - // }, - // { - // id: VMWizardTab.STORAGE, - // }, + { + id: VMWizardTab.NETWORKING, + component: ( + <> + + + + ), + }, + { + id: VMWizardTab.STORAGE, + component: ( + <> + + + + ), + }, + { + name: 'Advanced', + steps: [ + { + id: VMWizardTab.ADVANCED_CLOUD_INIT, + component: ( + <> + + + + ), + }, + ], + }, { id: VMWizardTab.REVIEW, component: , @@ -168,6 +298,44 @@ export class CreateVMWizardComponent extends React.Component isStepLocked(stepData, id)); + const calculateSteps = (initialSteps, initialAccumulator: WizardStep[] = []): WizardStep[] => + initialSteps.reduce((stepAcc: WizardStep[], step: any) => { + const isFirstStep = _.isEmpty(stepAcc); + let innerSteps; + if (step.steps) { + // pass reference to last step but remove it afterwards + innerSteps = calculateSteps(step.steps, isFirstStep ? [] : [_.last(stepAcc)]); + if (!isFirstStep) { + innerSteps.shift(); + } + } + let prevStep; + if (!isFirstStep) { + prevStep = _.last(stepAcc); + while (prevStep.steps) { + prevStep = _.last(prevStep.steps); + } + } + const isPrevStepValid = isFirstStep || isStepValid(stepData, prevStep.id as VMWizardTab); + const canJumpToPrevStep = isFirstStep || prevStep.canJumpTo; + + const calculatedStep = { + ...step, + name: TabTitleResolver[step.id] || step.name, + canJumpTo: isStepLocked(stepData, VMWizardTab.RESULT) // request finished + ? step.id === VMWizardTab.RESULT + : !isLocked && isPrevStepValid && canJumpToPrevStep && step.id !== VMWizardTab.RESULT, + component: step.component, + }; + + if (innerSteps) { + calculatedStep.steps = innerSteps; + } + + stepAcc.push(calculatedStep); + return stepAcc; + }, initialAccumulator); + return (
{!isStepValid(stepData, VMWizardTab.RESULT) && ( @@ -184,24 +352,7 @@ export class CreateVMWizardComponent extends React.Component { - const prevStep = stepAcc[idx - 1]; - const isPrevStepValid = idx === 0 ? true : isStepValid(stepData, prevStep.id); - const canJumpToPrevStep = idx === 0 ? true : prevStep.canJumpTo; - - stepAcc.push({ - ...step, - name: TabTitleResolver[step.id], - canJumpTo: isStepLocked(stepData, VMWizardTab.RESULT) // request finished - ? step.id === VMWizardTab.RESULT - : !isLocked && - isPrevStepValid && - canJumpToPrevStep && - step.id !== VMWizardTab.RESULT, - component: step.component, - }); - return stepAcc; - }, [])} + steps={calculateSteps(steps)} footer={} />
@@ -240,10 +391,10 @@ const wizardDispatchToProps = (dispatch, props) => ({ ); }, onClose: () => { + dispatch(vmWizardActions[ActionType.Dispose](props.reduxID, props)); if (props.onClose) { props.onClose(); } - dispatch(vmWizardActions[ActionType.Dispose](props.reduxID, props)); }, }); @@ -274,19 +425,6 @@ export const CreateVMWizardPageComponent: React.FC { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx index f6abd2eba56..9e4c43f9642 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx @@ -4,6 +4,7 @@ import { getFieldHelp, getFieldId, getFieldTitle } from '../utils/vm-settings-ta import { iGetFieldValue, isFieldHidden, isFieldRequired } from '../selectors/immutable/vm-settings'; import { iGet, iGetIn, iGetIsLoaded } from '../../../utils/immutable'; import { FormRow } from '../../form/form-row'; +import { ValidationObject } from '../../../utils/validations/types'; import { FormFieldContext } from './form-field-context'; import { FormFieldType } from './form-field'; import { FormFieldReviewContext } from './form-field-review-context'; @@ -18,6 +19,7 @@ export const FormFieldRow: React.FC = ({ fieldType, children, loadingResources, + validation, }) => { const fieldKey = iGet(field, 'key'); @@ -41,9 +43,10 @@ export const FormFieldRow: React.FC = ({ } help={getFieldHelp(fieldKey, iGetFieldValue(field))} isRequired={isFieldRequired(field)} - validationMessage={iGetIn(field, ['validation', 'message'])} - validationType={iGetIn(field, ['validation', 'type'])} + validationMessage={validation ? undefined : iGetIn(field, ['validation', 'message'])} + validationType={validation ? undefined : iGetIn(field, ['validation', 'type'])} isLoading={loading} + validation={validation} > {children} @@ -60,6 +63,7 @@ type FieldFormRowProps = { fieldType: FormFieldType; children?: React.ReactNode; loadingResources?: { [k: string]: any }; + validation?: ValidationObject; }; export const FormFieldMemoRow = React.memo( @@ -67,5 +71,7 @@ export const FormFieldMemoRow = React.memo( (prevProps, nextProps) => prevProps.field === nextProps.field && prevProps.fieldType === nextProps.fieldType && + _.get(prevProps.validation, ['type']) === _.get(nextProps.validation, ['type']) && + _.get(prevProps.validation, ['message']) === _.get(nextProps.validation, ['message']) && isLoading(prevProps.loadingResources) === isLoading(nextProps.loadingResources), ); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx index 6247bdfee50..18cc14bebc7 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx @@ -36,7 +36,7 @@ const setSupported = (fieldType: FormFieldType, supportedTypes: Set = ({ children, isDisabled }) => { +export const FormField: React.FC = ({ children, isDisabled, value }) => { return ( {({ @@ -49,7 +49,7 @@ export const FormField: React.FC = ({ children, isDisabled }) => isLoading: boolean; }) => { const set = setSupported.bind(undefined, fieldType); - const value = iGetFieldValue(field); + const val = value || iGetFieldValue(field); const key = iGetFieldKey(field); const disabled = isDisabled || isFieldDisabled(field) || isLoading; @@ -57,8 +57,8 @@ export const FormField: React.FC = ({ children, isDisabled }) => children, _.omitBy( { - value: hasValue.has(fieldType) ? value || getPlaceholder(key) || '' : undefined, - isChecked: set(hasIsChecked, value), + value: hasValue.has(fieldType) ? val || getPlaceholder(key) || '' : undefined, + isChecked: set(hasIsChecked, val), isDisabled: set(hasIsDisabled, disabled), disabled: set(hasDisabled, disabled), isRequired: set(hasIsRequired, isFieldRequired(field)), @@ -77,4 +77,5 @@ export const FormField: React.FC = ({ children, isDisabled }) => type FormFieldProps = { children: React.ReactNode; isDisabled?: boolean; + value?: any; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts index a8c8c8d028f..7f81822d9eb 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts @@ -5,7 +5,12 @@ import { CommonData, DetectCommonDataChanges, VMSettingsField, + VMWizardNetwork, + VMWizardTab, + VMWizardStorage, + CloudInitField, } from '../types'; +import { DeviceType } from '../../../constants/vm'; import { cleanup, updateAndValidateState } from './utils'; import { getTabInitialState } from './initial-state'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -14,28 +19,40 @@ import { vmWizardInternalActions } from './internal-actions'; type VMWizardActions = { [key in ActionType]: WizardActionDispatcher }; -export const vmWizardActions: VMWizardActions = { - [ActionType.Create]: (id, commonData: CommonData) => (dispatch, getState) => { - const prevState = getState(); // must be called before dispatch +const withUpdateAndValidateState = ( + id: string, + resolveAction, + changedCommonData?: Set, +) => (dispatch, getState) => { + const prevState = getState(); // must be called before dispatch in resolveAction - dispatch( - vmWizardInternalActions[InternalActionType.Create](id, { - tabs: ALL_VM_WIZARD_TABS.reduce((initial, tabKey) => { - initial[tabKey] = getTabInitialState(tabKey, commonData); - return initial; - }, {}), - commonData, - }), - ); + resolveAction(dispatch, getState); + + updateAndValidateState({ + id, + dispatch, + changedCommonData: changedCommonData || new Set(), + getState, + prevState, + }); +}; - updateAndValidateState({ +export const vmWizardActions: VMWizardActions = { + [ActionType.Create]: (id, commonData: CommonData) => + withUpdateAndValidateState( id, - changedCommonData: new Set(DetectCommonDataChanges), - dispatch, - getState, - prevState, - }); - }, + (dispatch) => + dispatch( + vmWizardInternalActions[InternalActionType.Create](id, { + tabs: ALL_VM_WIZARD_TABS.reduce((initial, tabKey) => { + initial[tabKey] = getTabInitialState(tabKey, commonData); + return initial; + }, {}), + commonData, + }), + ), + new Set(DetectCommonDataChanges), + ), [ActionType.Dispose]: (id) => (dispatch, getState) => { const prevState = getState(); // must be called before dispatch cleanup({ @@ -48,37 +65,57 @@ export const vmWizardActions: VMWizardActions = { dispatch(vmWizardInternalActions[InternalActionType.Dispose](id)); }, - [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => ( - dispatch, - getState, - ) => { - const prevState = getState(); // must be called before dispatch - dispatch(vmWizardInternalActions[InternalActionType.SetVmSettingsFieldValue](id, key, value)); - - updateAndValidateState({ + [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: any) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.SetVmSettingsFieldValue](id, key, value)), + ), + [ActionType.SetCloudInitFieldValue]: (id, key: CloudInitField, value: any) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.SetCloudInitFieldValue](id, key, value)), + ), + [ActionType.UpdateCommonData]: (id, commonData: CommonData, changedProps: ChangedCommonData) => + withUpdateAndValidateState( id, - dispatch, - changedCommonData: new Set(), - getState, - prevState, - }); + (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.UpdateCommonData](id, commonData)), + changedProps, + ), + [ActionType.SetTabLocked]: (id, tab: VMWizardTab, isLocked: boolean) => (dispatch) => { + dispatch(vmWizardInternalActions[InternalActionType.SetTabLocked](id, tab, isLocked)); }, - [ActionType.UpdateCommonData]: (id, commonData: CommonData, changedProps: ChangedCommonData) => ( - dispatch, - getState, - ) => { - const prevState = getState(); // must be called before dispatch + [ActionType.UpdateNIC]: (id, network: VMWizardNetwork) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.UpdateNIC](id, network)), + ), + [ActionType.RemoveNIC]: (id, networkID: string) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.RemoveNIC](id, networkID)), + ), - dispatch(vmWizardInternalActions[InternalActionType.UpdateCommonData](id, commonData)); - - updateAndValidateState({ id, dispatch, changedCommonData: changedProps, getState, prevState }); - }, - [ActionType.SetNetworks]: (id, value: any, isValid: boolean, isLocked: boolean) => (dispatch) => { - dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, value, isValid, isLocked)); - }, - [ActionType.SetStorages]: (id, value: any, isValid: boolean, isLocked: boolean) => (dispatch) => { - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, value, isValid, isLocked)); - }, + [ActionType.UpdateStorage]: (id, storage: VMWizardStorage) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.UpdateStorage](id, storage)), + ), + [ActionType.RemoveStorage]: (id, storageID: string) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.RemoveStorage](id, storageID)), + ), + [ActionType.SetDeviceBootOrder]: ( + id, + deviceID: string, + deviceType: DeviceType, + bootOrder: number, + ) => + withUpdateAndValidateState(id, (dispatch) => + dispatch( + vmWizardInternalActions[InternalActionType.SetDeviceBootOrder]( + id, + deviceID, + deviceType, + bootOrder, + ), + ), + ), [ActionType.SetResults]: ( id, value: any, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts new file mode 100644 index 00000000000..ee382ee15a9 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts @@ -0,0 +1,11 @@ +import { CloudInitField } from '../../types'; + +export const getCloudInitInitialState = () => ({ + value: { + [CloudInitField.IS_FORM]: { + value: true, + }, + }, + isValid: true, + hasAllRequiredFilled: true, +}); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts index a85bb786017..7a1c7ad17d1 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts @@ -4,11 +4,13 @@ import { getNetworksInitialState } from './networks-tab-initial-state'; import { getStorageInitialState } from './storage-tab-initial-state'; import { getResultInitialState } from './result-tab-initial-state'; import { getReviewInitialState } from './review-tab-initial-state'; +import { getCloudInitInitialState } from './cloud-init-tab-initial-state'; const initialStateGetterResolver = { [VMWizardTab.VM_SETTINGS]: getVmSettingsInitialState, - [VMWizardTab.NETWORKS]: getNetworksInitialState, + [VMWizardTab.NETWORKING]: getNetworksInitialState, [VMWizardTab.STORAGE]: getStorageInitialState, + [VMWizardTab.ADVANCED_CLOUD_INIT]: getCloudInitInitialState, [VMWizardTab.REVIEW]: getReviewInitialState, [VMWizardTab.RESULT]: getResultInitialState, }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts index 0e2ea0e4638..fec79c89713 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts @@ -1,15 +1,21 @@ -import { NetworkInterfaceType, NetworkType, POD_NETWORK } from '../../../../constants/vm'; +import { VMWizardNetwork, VMWizardNetworkType } from '../../types'; +import { NetworkInterfaceWrapper } from '../../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkInterfaceModel, NetworkType } from '../../../../constants/vm/network'; +import { NetworkWrapper } from '../../../../k8s/wrapper/vm/network-wrapper'; +import { getSequenceName } from '../../../../utils/strings'; -export const podNetwork = { - rootNetwork: {}, - id: 0, - name: 'nic0', - mac: '', - network: POD_NETWORK, - editable: true, - edit: false, - networkType: NetworkType.POD, - binding: NetworkInterfaceType.MASQUERADE, +export const podNetwork: VMWizardNetwork = { + id: '0', + type: VMWizardNetworkType.UI_DEFAULT_POD_NETWORK, + networkInterface: NetworkInterfaceWrapper.initializeFromSimpleData({ + name: getSequenceName('nic'), + model: NetworkInterfaceModel.VIRTIO, + interfaceType: NetworkType.POD.getDefaultInterfaceType(), + }).asResource(), + network: NetworkWrapper.initializeFromSimpleData({ + name: getSequenceName('nic'), + type: NetworkType.POD, + }).asResource(), }; export const getNetworksInitialState = () => ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts index 143fe5fa496..ce5275dab61 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts @@ -1,30 +1,65 @@ -// left intentionally empty -import { ProvisionSource } from '../../../../types/vm'; -import { StorageType } from '../../../../constants/vm/storage'; +import { VMWizardStorage, VMWizardStorageType } from '../../types'; +import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; +import { + DataVolumeSourceType, + DiskBus, + DiskType, + VolumeType, +} from '../../../../constants/vm/storage'; +import { VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { prefixedID } from '../../../../utils'; +import { DataVolumeWrapper } from '../../../../k8s/wrapper/vm/data-volume-wrapper'; +import { BinaryUnit } from '../../../form/size-unit-form-row'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; +import { VM_TEMPLATE_NAME_PARAMETER } from '../../../../constants/vm-templates'; -const rootDisk = { - rootStorage: {}, - name: 'rootdisk', - isBootable: true, -}; -export const rootContainerDisk = { - ...rootDisk, - storageType: StorageType.CONTAINER, +const ROOT_DISK_NAME = 'rootdisk'; + +const containerStorage: VMWizardStorage = { + type: VMWizardStorageType.PROVISION_SOURCE_DISK, + disk: DiskWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: DiskType.DISK, + bus: DiskBus.VIRTIO, + bootOrder: 1, + }).asResource(), + volume: VolumeWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: VolumeType.CONTAINER_DISK, + typeData: { image: '' }, + }).asResource(), }; -export const rootDataVolumeDisk = { - ...rootDisk, - storageType: StorageType.DATAVOLUME, - size: 10, + +const urlStorage = { + type: VMWizardStorageType.PROVISION_SOURCE_DISK, + disk: DiskWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: DiskType.DISK, + bus: DiskBus.VIRTIO, + bootOrder: 1, + }).asResource(), + volume: VolumeWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: VolumeType.DATA_VOLUME, + typeData: { name: prefixedID(VM_TEMPLATE_NAME_PARAMETER, ROOT_DISK_NAME) }, + }).asResource(), + dataVolume: DataVolumeWrapper.initializeFromSimpleData({ + name: prefixedID(VM_TEMPLATE_NAME_PARAMETER, ROOT_DISK_NAME), + type: DataVolumeSourceType.HTTP, + typeData: { url: '' }, + size: 10, + unit: BinaryUnit.Gi, + }).asResource(), }; -export const getInitialDisk = (provisionSource: ProvisionSource) => { - switch (provisionSource) { - case ProvisionSource.URL: - return rootDataVolumeDisk; - case ProvisionSource.CONTAINER: - return rootContainerDisk; - default: - return null; + +export const getProvisionSourceStorage = (provisionSource: ProvisionSource): VMWizardStorage => { + if (provisionSource === ProvisionSource.URL) { + return urlStorage; + } + if (provisionSource === ProvisionSource.CONTAINER) { + return containerStorage; } + return null; }; export const getStorageInitialState = () => ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts index ccd78d58327..368749c5340 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts @@ -1,7 +1,7 @@ import { OrderedSet } from 'immutable'; import { CommonData, VMSettingsField, VMWizardProps } from '../../types'; import { asHidden, asRequired } from '../../utils/utils'; -import { ProvisionSource } from '../../../../types/vm'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; export const getInitialVmSettings = (common: CommonData) => { const { @@ -9,11 +9,11 @@ export const getInitialVmSettings = (common: CommonData) => { } = common; const provisionSources = [ - // ProvisionSource.PXE, // TODO: uncomment when storage tab is implemented + ProvisionSource.PXE, ProvisionSource.URL, ProvisionSource.CONTAINER, - // ProvisionSource.CLONED_DISK, // TODO: uncomment when storage tab is implemented - ]; + ProvisionSource.DISK, + ].map((source) => source.getValue()); const fields = { [VMSettingsField.NAME]: { @@ -28,8 +28,12 @@ export const getInitialVmSettings = (common: CommonData) => { isRequired: asRequired(true), sources: OrderedSet(provisionSources), }, - [VMSettingsField.CONTAINER_IMAGE]: {}, - [VMSettingsField.IMAGE_URL]: {}, + [VMSettingsField.CONTAINER_IMAGE]: { + skipValidation: true, // validated in storage tab + }, + [VMSettingsField.IMAGE_URL]: { + skipValidation: true, // validated in storage tab + }, [VMSettingsField.OPERATING_SYSTEM]: { isRequired: asRequired(true), }, @@ -44,11 +48,6 @@ export const getInitialVmSettings = (common: CommonData) => { [VMSettingsField.START_VM]: { isHidden: asHidden(isCreateTemplate, VMWizardProps.isCreateTemplate), }, - [VMSettingsField.USE_CLOUD_INIT]: {}, - [VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT]: {}, - [VMSettingsField.HOST_NAME]: {}, - [VMSettingsField.AUTHKEYS]: {}, - [VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT]: {}, }; Object.keys(fields).forEach((k) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts index ad32d2ed158..bb89934b47c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts @@ -1,4 +1,11 @@ -import { VMSettingsField, VMWizardTab } from '../types'; +import { + CloudInitField, + VMSettingsField, + VMWizardNetwork, + VMWizardStorage, + VMWizardTab, +} from '../types'; +import { DeviceType } from '../../../constants/vm'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ActionBatch, InternalActionType, WizardInternalActionDispatcher } from './types'; @@ -47,7 +54,15 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetTabValidity, }), - [InternalActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => ({ + [InternalActionType.SetTabLocked]: (id, tab: VMWizardTab, isLocked: boolean) => ({ + payload: { + id, + tab, + isLocked, + }, + type: InternalActionType.SetTabLocked, + }), + [InternalActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: any) => ({ payload: { id, key, @@ -55,6 +70,14 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetVmSettingsFieldValue, }), + [InternalActionType.SetCloudInitFieldValue]: (id, key: CloudInitField, value: any) => ({ + payload: { + id, + key, + value, + }, + type: InternalActionType.SetCloudInitFieldValue, + }), [InternalActionType.UpdateVmSettingsField]: (id, key: VMSettingsField, value) => ({ payload: { id, @@ -85,21 +108,60 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.UpdateVmSettings, }), - [InternalActionType.SetNetworks]: (id, value, isValid: boolean, isLocked: boolean) => ({ + [InternalActionType.UpdateNIC]: (id, network: VMWizardNetwork) => ({ payload: { id, - value, - isValid, - isLocked, + network, + }, + type: InternalActionType.UpdateNIC, + }), + [InternalActionType.RemoveNIC]: (id, networkID: string) => ({ + payload: { + id, + networkID, + }, + type: InternalActionType.RemoveNIC, + }), + + [InternalActionType.UpdateStorage]: (id, storage: VMWizardStorage) => ({ + payload: { + id, + storage, + }, + type: InternalActionType.UpdateStorage, + }), + [InternalActionType.RemoveStorage]: (id, storageID: string) => ({ + payload: { + id, + storageID, + }, + type: InternalActionType.RemoveStorage, + }), + [InternalActionType.SetDeviceBootOrder]: ( + id, + deviceID: string, + deviceType: DeviceType, + bootOrder: number, + ) => ({ + payload: { + id, + deviceID, + deviceType, + bootOrder, + }, + type: InternalActionType.SetDeviceBootOrder, + }), + [InternalActionType.SetNetworks]: (id, networks: VMWizardNetwork[]) => ({ + payload: { + id, + value: networks, }, type: InternalActionType.SetNetworks, }), - [InternalActionType.SetStorages]: (id, value, isValid: boolean, isLocked: boolean) => ({ + [InternalActionType.SetStorages]: (id, value: VMWizardStorage[]) => ({ payload: { id, value, - isValid, - isLocked, }, type: InternalActionType.SetStorages, }), diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts index c1148426ced..ae2756dfa63 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts @@ -1,7 +1,74 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import * as _ from 'lodash'; +import { fromJS, Map as ImmutableMap } from 'immutable'; import { VMWizardTab } from '../types'; +import { iGet } from '../../../utils/immutable'; +import { DeviceType } from '../../../constants/vm'; import { InternalActionType, WizardInternalAction } from './types'; +const sequentializeBootOrderIndexes = (state, dialogId: string) => { + const bootOrderIndexes = [ + ...state + .getIn([dialogId, 'tabs', VMWizardTab.NETWORKING, 'value']) + .toArray() + .map((network) => network.getIn(['networkInterface', 'bootOrder'])), + ...state + .getIn([dialogId, 'tabs', VMWizardTab.STORAGE, 'value']) + .toArray() + .map((storage) => storage.getIn(['disk', 'bootOrder'])), + ] + .filter((bootOrder) => bootOrder != null) + .sort((a, b) => a - b); + + return [DeviceType.NIC, DeviceType.DISK].reduce((newState, deviceType) => { + const tab = deviceType === DeviceType.DISK ? VMWizardTab.STORAGE : VMWizardTab.NETWORKING; + const deviceName = deviceType === DeviceType.DISK ? 'disk' : 'networkInterface'; + + return newState.updateIn([dialogId, 'tabs', tab, 'value'], (deviceWrappers) => { + return deviceWrappers.map((deviceWrapper) => { + const oldBootOrder = deviceWrapper.getIn([deviceName, 'bootOrder']); + + if (oldBootOrder != null) { + const newBootOrder = bootOrderIndexes.indexOf(oldBootOrder) + 1; + if (newBootOrder !== oldBootOrder) { + return deviceWrapper.setIn([deviceName, 'bootOrder'], newBootOrder); + } + } + return deviceWrapper; + }); + }); + }, state); +}; + +const setDeviceBootOrder = ( + state, + dialogId: string, + deviceID: string, + updatedDeviceType: DeviceType, + updatedDeviceBootOrder: number, +) => { + const resultState = [DeviceType.NIC, DeviceType.DISK].reduce((newState, devType) => { + const tab = devType === DeviceType.DISK ? VMWizardTab.STORAGE : VMWizardTab.NETWORKING; + const deviceName = devType === DeviceType.DISK ? 'disk' : 'networkInterface'; + + return newState.updateIn([dialogId, 'tabs', tab, 'value'], (deviceWrappers) => { + return deviceWrappers.map((deviceWrapper) => { + const wrapperID = deviceWrapper.get('id'); + const oldBootOrder = deviceWrapper.getIn([deviceName, 'bootOrder']); + const isUpdatedDevice = updatedDeviceType === devType && wrapperID === deviceID; + if (isUpdatedDevice || (oldBootOrder != null && updatedDeviceBootOrder <= oldBootOrder)) { + return deviceWrapper.setIn( + [deviceName, 'bootOrder'], + isUpdatedDevice ? updatedDeviceBootOrder : oldBootOrder + 1, + ); + } + return deviceWrapper; + }); + }); + }, state); + + return sequentializeBootOrderIndexes(resultState, dialogId); +}; + // Merge deep in without updating the keys with undefined values const mergeDeepInSpecial = (state, path: string[], value) => state.updateIn(path, (oldValue) => { @@ -34,6 +101,25 @@ const setObjectValues = (state, path, obj) => { : state; }; +const updateIDItemInList = (state, path, item?) => { + const itemID = iGet(item, 'id'); + return state.updateIn(path, (items) => { + const networkIndex = itemID != null ? items.findIndex((t) => iGet(t, 'id') === itemID) : -1; + if (networkIndex === -1) { + const maxID = items.map((t) => iGet(t, 'id')).max() || 0; + return items.push(item.set('id', _.toString(_.toSafeInteger(maxID) + 1))); + } + return items.set(networkIndex, item); + }); +}; + +const removeIDItemFromList = (state, path, itemID?) => { + return state.updateIn(path, (items) => { + const networkIndex = itemID == null ? -1 : items.findIndex((t) => iGet(t, 'id') === itemID); + return networkIndex === -1 ? items : items.delete(networkIndex); + }); +}; + export default (state, action: WizardInternalAction) => { if (!state) { return ImmutableMap(); @@ -46,8 +132,40 @@ export default (state, action: WizardInternalAction) => { return state.set(dialogId, fromJS(payload.value)); case InternalActionType.Dispose: return state.delete(dialogId); + case InternalActionType.UpdateNIC: + return updateIDItemInList( + state, + [dialogId, 'tabs', VMWizardTab.NETWORKING, 'value'], + fromJS(payload.network), + ); + case InternalActionType.RemoveNIC: + return removeIDItemFromList( + state, + [dialogId, 'tabs', VMWizardTab.NETWORKING, 'value'], + payload.networkID, + ); + case InternalActionType.UpdateStorage: + return updateIDItemInList( + state, + [dialogId, 'tabs', VMWizardTab.STORAGE, 'value'], + fromJS(payload.storage), + ); + case InternalActionType.RemoveStorage: + return removeIDItemFromList( + state, + [dialogId, 'tabs', VMWizardTab.STORAGE, 'value'], + payload.storageID, + ); + case InternalActionType.SetDeviceBootOrder: + return setDeviceBootOrder( + state, + dialogId, + payload.deviceID, + payload.deviceType, + payload.bootOrder, + ); case InternalActionType.SetNetworks: - return setTabKeys(state, VMWizardTab.NETWORKS, action); + return setTabKeys(state, VMWizardTab.NETWORKING, action); case InternalActionType.SetStorages: return setTabKeys(state, VMWizardTab.STORAGE, action); case InternalActionType.SetResults: @@ -67,11 +185,18 @@ export default (state, action: WizardInternalAction) => { [dialogId, 'tabs', payload.tab, 'hasAllRequiredFilled'], payload.hasAllRequiredFilled, ); + case InternalActionType.SetTabLocked: + return state.setIn([dialogId, 'tabs', payload.tab, 'isLocked'], payload.isLocked); case InternalActionType.SetVmSettingsFieldValue: return state.setIn( [dialogId, 'tabs', VMWizardTab.VM_SETTINGS, 'value', payload.key, 'value'], fromJS(payload.value), ); + case InternalActionType.SetCloudInitFieldValue: + return state.setIn( + [dialogId, 'tabs', VMWizardTab.ADVANCED_CLOUD_INIT, 'value', payload.key, 'value'], + fromJS(payload.value), + ); case InternalActionType.SetInVmSettings: return state.setIn( [dialogId, 'tabs', VMWizardTab.VM_SETTINGS, 'value', ...payload.path], diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts index 929b2ad3ef4..4e344d814f2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts @@ -1,21 +1,36 @@ -import * as _ from 'lodash'; +import { createBasicLookup, getName } from '@console/shared/src'; import { InternalActionType, UpdateOptions } from '../../types'; -import { iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; -import { VMSettingsField, VMWizardProps } from '../../../types'; +import { iGetProvisionSource, iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; +import { + CloudInitField, + VMSettingsField, + VMWizardNetwork, + VMWizardNetworkType, + VMWizardProps, + VMWizardStorage, + VMWizardStorageType, +} from '../../../types'; import { iGetLoadedCommonData, iGetName } from '../../../selectors/immutable/selectors'; import { concatImmutableLists, immutableListToShallowJS } from '../../../../../utils/immutable'; -import { iGetNetworks as getDialogNetworks } from '../../../selectors/immutable/networks'; -import { iGetStorages } from '../../../selectors/immutable/storage'; +import { iGetNetworks } from '../../../selectors/immutable/networks'; import { podNetwork } from '../../initial-state/networks-tab-initial-state'; import { vmWizardInternalActions } from '../../internal-actions'; -import { CUSTOM_FLAVOR } from '../../../../../constants/vm'; +import { + CUSTOM_FLAVOR, + DiskBus, + DiskType, + NetworkInterfaceModel, + VolumeType, +} from '../../../../../constants/vm'; import { DEFAULT_CPU, - getCloudInitUserData, getCPU, + getDataVolumeTemplates, + getDisks, getInterfaces, getMemory, getNetworks, + getVolumes, hasAutoAttachPodInterface, parseCPU, } from '../../../../../selectors/vm'; @@ -25,18 +40,17 @@ import { getTemplateOperatingSystems, getTemplateWorkloadProfiles, } from '../../../../../selectors/vm-template/advanced'; -import { ProvisionSource } from '../../../../../types/vm'; -import { - getTemplateProvisionSource, - getTemplateStorages, -} from '../../../../../selectors/vm-template/combined'; +import { V1Network } from '../../../../../types/vm'; import { getFlavors } from '../../../../../selectors/vm-template/combined-dependent'; - -// used by user template; currently we do not support PROVISION_SOURCE_IMPORT -const provisionSourceDataFieldResolver = { - [ProvisionSource.CONTAINER]: VMSettingsField.CONTAINER_IMAGE, - [ProvisionSource.URL]: VMSettingsField.IMAGE_URL, -}; +import { getSimpleName } from '../../../../../selectors/utils'; +import { getNextIDResolver } from '../../../../../utils/utils'; +import { ProvisionSource } from '../../../../../constants/vm/provision-source'; +import { DiskWrapper } from '../../../../../k8s/wrapper/vm/disk-wrapper'; +import { V1Volume } from '../../../../../types/vm/disk/V1Volume'; +import { MutableVolumeWrapper, VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; +import { getProvisionSourceStorage } from '../../initial-state/storage-tab-initial-state'; +import { CloudInitDataHelper } from '../../../../../k8s/wrapper/vm/cloud-init-data-helper'; +import { getStorages } from '../../../selectors/selectors'; export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptions) => { const state = getState(); @@ -50,24 +64,31 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio ? iUserTemplates.find((template) => iGetName(template) === userTemplateName) : null; + let isCloudInitForm = null; const vmSettingsUpdate = {}; // filter out oldTemplates - const networkRowsUpdate = immutableListToShallowJS(getDialogNetworks(state, id)).filter( - (network) => !network.templateNetwork, + let networksUpdate = immutableListToShallowJS(iGetNetworks(state, id)).filter( + (network) => network.type !== VMWizardNetworkType.TEMPLATE, ); - const storageRowsUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( - (storage) => !(storage.templateStorage || storage.rootStorage), + const getNextNetworkID = getNextIDResolver(networksUpdate); + + const storagesUpdate = getStorages(state, id).filter( + (storage) => + ![ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.TEMPLATE, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(storage.type) && + VolumeWrapper.initialize(storage.volume).getType() !== VolumeType.CLOUD_INIT_NO_CLOUD, ); + const getNextStorageID = getNextIDResolver(storagesUpdate); - if (!networkRowsUpdate.find((row) => row.rootNetwork)) { - networkRowsUpdate.push(podNetwork); + if (!networksUpdate.find((row) => !!row.network.pod)) { + networksUpdate.unshift({ ...podNetwork, id: getNextNetworkID() }); } if (iUserTemplate) { - const dataVolumes = immutableListToShallowJS( - iGetLoadedCommonData(state, id, VMWizardProps.userTemplates), - ); const userTemplate = iUserTemplate.toJS(); const vm = selectVM(userTemplate); @@ -89,53 +110,73 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio const [workload] = getTemplateWorkloadProfiles([userTemplate]); vmSettingsUpdate[VMSettingsField.WORKLOAD_PROFILE] = { value: workload }; - // update cloud-init - const cloudInitUserData = getCloudInitUserData(vm); - if (cloudInitUserData) { - vmSettingsUpdate[VMSettingsField.USE_CLOUD_INIT] = { value: true }; - vmSettingsUpdate[VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT] = { value: true }; - vmSettingsUpdate[VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT] = { - value: cloudInitUserData || '', - }; - } - // update provision source - const provisionSource = getTemplateProvisionSource(userTemplate, dataVolumes); - if (provisionSource.type === ProvisionSource.UNKNOWN) { - vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { value: null }; - } else { - vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { value: provisionSource.type }; - const dataFieldName = provisionSourceDataFieldResolver[provisionSource.type]; - if (dataFieldName) { - vmSettingsUpdate[dataFieldName] = { value: provisionSource.source }; - } - } + const provisionSourceDetails = ProvisionSource.getProvisionSourceDetails(userTemplate); + vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { + value: provisionSourceDetails.type ? provisionSourceDetails.type.getValue() : null, + }; + const networkLookup = createBasicLookup(getNetworks(vm), getSimpleName); // prefill networks - const templateNetworks = getInterfaces(vm).map((i) => ({ - templateNetwork: { - network: getNetworks(vm).find((n) => n.name === i.name), - interface: i, + const templateNetworks: VMWizardNetwork[] = getInterfaces(vm).map((intface) => ({ + id: getNextNetworkID(), + type: VMWizardNetworkType.TEMPLATE, + network: networkLookup[getSimpleName(intface)], + networkInterface: { + ...intface, + model: intface.model || NetworkInterfaceModel.VIRTIO.getValue(), }, })); - // do not add root interface if there already is one or autoAttachPodInterface is set to false + // remove pod networks if there is already one in template or autoAttachPodInterface is set to false if ( - templateNetworks.some((network) => network.templateNetwork.network.pod) || + templateNetworks.some((network) => !!network.network.pod) || !hasAutoAttachPodInterface(vm, true) ) { - const index = _.findIndex(networkRowsUpdate, (network: any) => network.rootNetwork); - networkRowsUpdate.splice(index, 1); + networksUpdate = networksUpdate.filter((network) => !network.network.pod); } - networkRowsUpdate.push(...templateNetworks); + networksUpdate.push(...templateNetworks); + + const volumeLookup = createBasicLookup(getVolumes(vm), getSimpleName); + const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); + // // prefill storage + const templateStorages: VMWizardStorage[] = getDisks(vm).map((disk) => { + const diskWrapper = DiskWrapper.initialize(disk); + let volume = volumeLookup[diskWrapper.getName()]; + const volumeWrapper = VolumeWrapper.initialize(volume); + + if (volumeWrapper.getType() === VolumeType.CLOUD_INIT_NO_CLOUD) { + const helper = new CloudInitDataHelper(volumeWrapper.getCloudInitNoCloud()); + if (helper.includesOnlyFormValues()) { + isCloudInitForm = true; + helper.makeFormCompliant(); + volume = new MutableVolumeWrapper(volume, { copy: true }) + .replaceTypeData(helper.asCloudInitNoCloudSource()) + .asMutableResource(); + // do not overwrite with more cloud-init disks + } else if (isCloudInitForm == null) { + isCloudInitForm = false; + } + } - // prefill storage - const templateStorages = getTemplateStorages(userTemplate, dataVolumes).map((storage) => ({ - templateStorage: storage, - rootStorage: storage.disk.bootOrder === 1 ? {} : undefined, - })); - storageRowsUpdate.push(...templateStorages); + return { + id: getNextStorageID(), + type: diskWrapper.isFirstBootableDevice() + ? VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK + : VMWizardStorageType.TEMPLATE, + volume, + dataVolume: datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()], + disk: + diskWrapper.getType() === DiskType.DISK && !diskWrapper.getDiskBus() + ? DiskWrapper.mergeWrappers( + diskWrapper, + DiskWrapper.initializeFromSimpleData({ type: DiskType.DISK, bus: DiskBus.VIRTIO }), + ).asResource() + : disk, + }; + }); + storagesUpdate.unshift(...templateStorages); } else { const iCommonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates); @@ -150,9 +191,23 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio if (flavors.length === 1) { vmSettingsUpdate[VMSettingsField.FLAVOR] = { value: flavors[0] }; } + + const newSourceStorage = getProvisionSourceStorage(iGetProvisionSource(state, id)); + if (newSourceStorage) { + storagesUpdate.unshift({ ...newSourceStorage, id: getNextStorageID() }); + } } dispatch(vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, vmSettingsUpdate)); - dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networkRowsUpdate)); - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storageRowsUpdate)); + dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networksUpdate)); + dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storagesUpdate)); + if (isCloudInitForm != null) { + dispatch( + vmWizardInternalActions[InternalActionType.SetCloudInitFieldValue]( + id, + CloudInitField.IS_FORM, + isCloudInitForm, + ), + ); + } }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts new file mode 100644 index 00000000000..fb9ba55b1b7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts @@ -0,0 +1,61 @@ +import { + hasVmSettingsChanged, + iGetProvisionSource, +} from '../../../selectors/immutable/vm-settings'; +import { VMSettingsField } from '../../../types'; +import { InternalActionType, UpdateOptions } from '../../types'; +import { iGetProvisionSourceStorage } from '../../../selectors/immutable/storage'; +import { getProvisionSourceStorage } from '../../initial-state/storage-tab-initial-state'; +import { VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../../../k8s/wrapper/vm/data-volume-wrapper'; +import { StorageUISource } from '../../../../modals/disk-modal/storage-ui-source'; +import { getNextIDResolver } from '../../../../../utils/utils'; +import { getStorages } from '../../../selectors/selectors'; +import { vmWizardInternalActions } from '../../internal-actions'; + +export const prefillInitialDiskUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { + const state = getState(); + if (!hasVmSettingsChanged(prevState, state, id, VMSettingsField.PROVISION_SOURCE_TYPE)) { + return; + } + + const iOldSourceStorage = iGetProvisionSourceStorage(state, id); + const oldSourceStorage = iOldSourceStorage && iOldSourceStorage.toJSON(); + + const newSourceStorage = getProvisionSourceStorage(iGetProvisionSource(state, id)); + const oldType = + oldSourceStorage && + StorageUISource.fromTypes( + VolumeWrapper.initialize(oldSourceStorage.volume).getType(), + DataVolumeWrapper.initialize(oldSourceStorage.dataVolume).getType(), + ); + + const newType = + newSourceStorage && + StorageUISource.fromTypes( + VolumeWrapper.initialize(newSourceStorage.volume).getType(), + DataVolumeWrapper.initialize(newSourceStorage.dataVolume).getType(), + ); + + if (newType !== oldType) { + if (!newSourceStorage) { + if (oldSourceStorage) { + dispatch( + vmWizardInternalActions[InternalActionType.RemoveStorage](id, oldSourceStorage.id), + ); + } + } else { + dispatch( + vmWizardInternalActions[InternalActionType.UpdateStorage](id, { + id: oldSourceStorage ? oldSourceStorage.id : getNextIDResolver(getStorages(state, id))(), + ...newSourceStorage, + }), + ); + } + } +}; + +export const updateStorageTabState = (options: UpdateOptions) => + [prefillInitialDiskUpdater].forEach((updater) => { + updater && updater(options); + }); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts index 36677a0320a..a180069b2b6 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts @@ -1,5 +1,6 @@ import { hasVmSettingsChanged, + iGetProvisionSource, iGetVmSettingAttribute, iGetVmSettingValue, } from '../../../selectors/immutable/vm-settings'; @@ -12,16 +13,10 @@ import { iGetLoadedCommonData, iGetName, } from '../../../selectors/immutable/selectors'; -import { - iGetIsLoaded, - iGetLoadedData, - immutableListToShallowJS, -} from '../../../../../utils/immutable'; -import { ProvisionSource } from '../../../../../types/vm'; +import { iGetIsLoaded, iGetLoadedData } from '../../../../../utils/immutable'; import { ignoreCaseSort } from '../../../../../utils/sort'; import { CUSTOM_FLAVOR } from '../../../../../constants/vm'; -import { iGetStorages } from '../../../selectors/immutable/storage'; -import { getInitialDisk } from '../../initial-state/storage-tab-initial-state'; +import { ProvisionSource } from '../../../../../constants/vm/provision-source'; import { prefillVmTemplateUpdater } from './prefill-vm-template-state-update'; export const selectUserTemplateOnLoadedUpdater = ({ @@ -36,8 +31,7 @@ export const selectUserTemplateOnLoadedUpdater = ({ iGetVmSettingAttribute(state, id, VMSettingsField.USER_TEMPLATE, 'initialized') || !( changedCommonData.has(VMWizardProps.userTemplates) || - changedCommonData.has(VMWizardProps.commonTemplates) || - changedCommonData.has(VMWizardProps.dataVolumes) + changedCommonData.has(VMWizardProps.commonTemplates) ) ) { return; @@ -45,13 +39,8 @@ export const selectUserTemplateOnLoadedUpdater = ({ const iUserTemplatesWrapper = iGetCommonData(state, id, VMWizardProps.userTemplates); const iCommonTemplatesWrapper = iGetCommonData(state, id, VMWizardProps.commonTemplates); // flavor prefill - const iDataVolumeWrapper = iGetCommonData(state, id, VMWizardProps.dataVolumes); // template prefill - if ( - !iGetIsLoaded(iUserTemplatesWrapper) || - !iGetIsLoaded(iCommonTemplatesWrapper) || - !iGetIsLoaded(iDataVolumeWrapper) - ) { + if (!iGetIsLoaded(iUserTemplatesWrapper) || !iGetIsLoaded(iCommonTemplatesWrapper)) { return; } @@ -120,19 +109,17 @@ export const provisioningSourceUpdater = ({ id, prevState, dispatch, getState }: ) { return; } - const source = iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE); + const source = iGetProvisionSource(state, id); const isContainer = source === ProvisionSource.CONTAINER; const isUrl = source === ProvisionSource.URL; dispatch( vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, { [VMSettingsField.CONTAINER_IMAGE]: { - value: isContainer ? undefined : null, isRequired: asRequired(isContainer, VMSettingsField.PROVISION_SOURCE_TYPE), isHidden: asHidden(!isContainer, VMSettingsField.PROVISION_SOURCE_TYPE), }, [VMSettingsField.IMAGE_URL]: { - value: isUrl ? undefined : null, isRequired: asRequired(isUrl, VMSettingsField.PROVISION_SOURCE_TYPE), isHidden: asHidden(!isUrl, VMSettingsField.PROVISION_SOURCE_TYPE), }, @@ -140,37 +127,6 @@ export const provisioningSourceUpdater = ({ id, prevState, dispatch, getState }: ); }; -// TODO: move this logic to StorageTab? -export const prefillInitialDiskUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { - const state = getState(); - if ( - !hasVmSettingsChanged( - prevState, - state, - id, - VMSettingsField.PROVISION_SOURCE_TYPE, - VMSettingsField.USER_TEMPLATE, - ) - ) { - return; - } - - const storageRowsUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( - (storage) => storage.templateStorage || !storage.rootStorage, - ); - // template pre-fills its own storages - if (!iGetVmSettingValue(state, id, VMSettingsField.USER_TEMPLATE)) { - const storage = getInitialDisk( - iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE), - ); - if (storage) { - storageRowsUpdate.push(storage); - } - } - - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storageRowsUpdate)); -}; - export const flavorUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { const state = getState(); if (!hasVmSettingsChanged(prevState, state, id, VMSettingsField.FLAVOR)) { @@ -200,7 +156,6 @@ export const updateVmSettingsState = (options: UpdateOptions) => selectUserTemplateOnLoadedUpdater, selectedUserTemplateUpdater, provisioningSourceUpdater, - prefillInitialDiskUpdater, flavorUpdater, ].forEach((updater) => { updater && updater(options); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts index 174d69000af..41fa590f14b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts @@ -1,34 +1,50 @@ import { ChangedCommonData, ChangedCommonDataProp, + CloudInitField, VMSettingsField, VMSettingsFieldType, + VMWizardNetwork, + VMWizardStorage, VMWizardTab, } from '../types'; import { ValidationObject } from '../../../utils/validations/types'; +import { DeviceType } from '../../../constants/vm'; export enum ActionType { - Create = 'KubevirtVMWizardCreate', - Dispose = 'KubevirtVMWizardDispose', - UpdateCommonData = 'KubevirtVMWizardUpdateCommonData', - SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValue', - SetNetworks = 'KubevirtVMWizardSetNetworks', - SetStorages = 'KubevirtVMWizardSetStorages', - SetResults = 'KubevirtVMWizardSetResults', + Create = 'KubevirtVMWizardExternalCreate', + Dispose = 'KubevirtVMWiExternalDispose', + UpdateCommonData = 'KubevirtVMWizardExternalUpdateCommonData', + SetVmSettingsFieldValue = 'KubevirtVMWizardExternalSetVmSettingsFieldValue', + SetCloudInitFieldValue = 'KubevirtVMWizardExternalSetCloudInitFieldValue', + SetTabLocked = 'KubevirtVMWizardExternalSetTabLocked', + RemoveNIC = 'KubevirtVMWizardExternalRemoveNIC', + UpdateNIC = 'KubevirtVMWizardExternalUpdateNIC', + SetDeviceBootOrder = 'KubevirtVMWizardExternalSetDeviceBootOrder', + RemoveStorage = 'KubevirtVMWizardExternalRemoveStorage', + UpdateStorage = 'KubevirtVMWizardExternalUpdateStorage', + SetResults = 'KubevirtVMWizardExternalSetResults', } // should not be called directly from outside redux code (e.g. stateUpdate) export enum InternalActionType { Create = 'KubevirtVMWizardCreate', Dispose = 'KubevirtVMWizardDispose', - Update = 'KubevirtVMWizardUpdateInternal', + Update = 'KubevirtVMWizardUpdate', UpdateCommonData = 'KubevirtVMWizardUpdateCommonData', - SetTabValidity = 'KubevirtVMWizardSetTabValidityInternal', - SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValueInternal', - SetInVmSettings = 'KubevirtVMWizardSetInVmSettingsInternal', - SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatchInternal', - UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsFieldInternal', - UpdateVmSettings = 'KubevirtVMWizardUpdateVmSettingsInternal', + SetTabValidity = 'KubevirtVMWizardSetTabValidity', + SetTabLocked = 'KubevirtVMWizardSetTabLocked', + SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValue', + SetCloudInitFieldValue = 'KubevirtVMWizardSetCloudInitFieldValue', + SetInVmSettings = 'KubevirtVMWizardSetInVmSettings', + SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatch', + UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsField', + UpdateVmSettings = 'KubevirtVMWizardUpdateVmSettings', + RemoveNIC = 'KubevirtVMWizardRemoveNIC', + UpdateNIC = 'KubevirtVMWizardUpdateNIC', + SetDeviceBootOrder = 'KubevirtVMWizardSetDeviceBootOrder', + RemoveStorage = 'KubevirtVMWizardRemoveStorage', + UpdateStorage = 'KubevirtVMWizardUpdateStorage', SetNetworks = 'KubevirtVMWizardSetNetworks', SetStorages = 'KubevirtVMWizardSetStorages', SetResults = 'KubevirtVMWizardSetResults', @@ -44,9 +60,16 @@ export type WizardInternalAction = { isPending?: boolean; hasAllRequiredFilled?: boolean; path?: string[]; - key?: VMSettingsField; + key?: VMSettingsField | CloudInitField; tab?: VMWizardTab; batch?: ActionBatch; + network?: VMWizardNetwork; + networkID?: string; + storage?: VMWizardStorage; + storageID?: string; + deviceID?: string; + deviceType?: DeviceType; + bootOrder?: number; }; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts index f6b08d810f1..93975a18339 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts @@ -1,23 +1,31 @@ import { VMWizardTab } from '../types'; import { UpdateOptions } from './types'; import { updateVmSettingsState } from './stateUpdate/vmSettings/vm-settings-tab-state-update'; +import { updateStorageTabState } from './stateUpdate/vmSettings/storage-tab-state-update'; import { setVmSettingsTabValidity, validateVmSettings, } from './validations/vm-settings-tab-validation'; +import { setNetworksTabValidity, validateNetworks } from './validations/networks-tab-validation'; +import { setStoragesTabValidity, validateStorages } from './validations/storage-tab-validation'; -const UPDATE_TABS = [VMWizardTab.VM_SETTINGS]; +const UPDATE_TABS = [VMWizardTab.VM_SETTINGS, VMWizardTab.NETWORKING, VMWizardTab.STORAGE]; const updaterResolver = { [VMWizardTab.VM_SETTINGS]: updateVmSettingsState, + [VMWizardTab.STORAGE]: updateStorageTabState, }; const validateTabResolver = { [VMWizardTab.VM_SETTINGS]: validateVmSettings, + [VMWizardTab.NETWORKING]: validateNetworks, + [VMWizardTab.STORAGE]: validateStorages, }; const isTabValidResolver = { [VMWizardTab.VM_SETTINGS]: setVmSettingsTabValidity, + [VMWizardTab.NETWORKING]: setNetworksTabValidity, + [VMWizardTab.STORAGE]: setStoragesTabValidity, }; export const updateAndValidateState = (options: UpdateOptions) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts new file mode 100644 index 00000000000..66f4e5f9987 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts @@ -0,0 +1,90 @@ +import { VMWizardTab } from '../../types'; +import { InternalActionType, UpdateOptions } from '../types'; +import { vmWizardInternalActions } from '../internal-actions'; +import { validateNIC } from '../../../../utils/validations/vm'; +import { iGetIn } from '../../../../utils/immutable'; +import { iGetNetworks } from '../../selectors/immutable/networks'; +import { checkTabValidityChanged } from '../../selectors/immutable/selectors'; +import { iGetProvisionSource } from '../../selectors/immutable/vm-settings'; +import { getNetworksWithWrappers } from '../../selectors/selectors'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; + +export const validateNetworks = (options: UpdateOptions) => { + const { id, prevState, dispatch, getState } = options; + const state = getState(); + + const prevINetworks = iGetNetworks(prevState, id); + const iNetworks = iGetNetworks(state, id); + + if ( + prevINetworks && + iNetworks && + prevINetworks.size === iNetworks.size && + !prevINetworks.find( + (prevINetwork, prevINetworkIndex) => prevINetwork !== iNetworks.get(prevINetworkIndex), + ) + ) { + return; + } + + const networks = getNetworksWithWrappers(state, id); + + const validatedNetworks = networks.map( + ({ networkInterfaceWrapper, networkWrapper, ...networkBundle }) => { + const otherNetworkBundles = networks.filter((n) => n.id !== networkBundle.id); // to discard networks with a same name + const usedMultusNetworkNames = new Set( + otherNetworkBundles + .map(({ networkWrapper: nw }) => nw.getMultusNetworkName()) + .filter((n) => n), + ); + const usedInterfacesNames = new Set( + otherNetworkBundles.map(({ networkInterfaceWrapper: niw }) => niw.getName()), + ); + + return { + ...networkBundle, + validation: validateNIC(networkInterfaceWrapper, networkWrapper, { + usedInterfacesNames, + usedMultusNetworkNames, + }), + }; + }, + ); + + dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, validatedNetworks)); +}; + +export const setNetworksTabValidity = (options: UpdateOptions) => { + const { id, dispatch, getState } = options; + const state = getState(); + const iNetworks = iGetNetworks(state, id); + + let hasAllRequiredFilled = iNetworks.every((iNetwork) => + iGetIn(iNetwork, ['validation', 'hasAllRequiredFilled']), + ); + + if (hasAllRequiredFilled && iGetProvisionSource(state, id) === ProvisionSource.PXE) { + hasAllRequiredFilled = !!iNetworks.find( + (networkBundle) => + !iGetIn(networkBundle, ['network', 'pod']) && + iGetIn(networkBundle, ['networkInterface', 'bootOrder']) === 1, + ); + } + + let isValid = hasAllRequiredFilled; + + if (isValid) { + isValid = iNetworks.every((iNetwork) => iGetIn(iNetwork, ['validation', 'isValid'])); + } + + if (checkTabValidityChanged(state, id, VMWizardTab.NETWORKING, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.NETWORKING, + isValid, + hasAllRequiredFilled, + ), + ); + } +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts new file mode 100644 index 00000000000..a10f2bcb954 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts @@ -0,0 +1,91 @@ +import { VMWizardTab } from '../../types'; +import { InternalActionType, UpdateOptions } from '../types'; +import { vmWizardInternalActions } from '../internal-actions'; +import { validateDisk } from '../../../../utils/validations/vm'; +import { iGetIn } from '../../../../utils/immutable'; +import { checkTabValidityChanged } from '../../selectors/immutable/selectors'; +import { getStoragesWithWrappers } from '../../selectors/selectors'; +import { iGetStorages } from '../../selectors/immutable/storage'; +import { iGetProvisionSource } from '../../selectors/immutable/vm-settings'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; + +export const validateStorages = (options: UpdateOptions) => { + const { id, prevState, dispatch, getState } = options; + const state = getState(); + + const prevIStorages = iGetStorages(prevState, id); + const iStorages = iGetStorages(state, id); + + if ( + prevIStorages && + iStorages && + prevIStorages.size === iStorages.size && + !prevIStorages.find( + (prevINetwork, prevINetworkIndex) => prevINetwork !== iStorages.get(prevINetworkIndex), + ) + ) { + return; + } + + const storages = getStoragesWithWrappers(state, id); + + const validatedStorages = storages.map( + ({ diskWrapper, volumeWrapper, dataVolumeWrapper, ...storageBundle }) => { + const otherStorageBundles = storages.filter((n) => n.id !== storageBundle.id); // to discard networks with a same name + const usedDiskNames = new Set(otherStorageBundles.map(({ diskWrapper: dw }) => dw.getName())); + + const usedPVCNames: Set = new Set( + otherStorageBundles + .filter(({ dataVolume }) => dataVolume) + .map(({ dataVolumeWrapper: dvw }) => dvw.getName()), + ); + + return { + ...storageBundle, + validation: validateDisk(diskWrapper, volumeWrapper, dataVolumeWrapper, { + usedDiskNames, + usedPVCNames, + }), + }; + }, + ); + + dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, validatedStorages)); +}; + +export const setStoragesTabValidity = (options: UpdateOptions) => { + const { id, dispatch, getState } = options; + const state = getState(); + const iStorages = iGetStorages(state, id); + + let hasAllRequiredFilled = iStorages.every((iStorage) => + iGetIn(iStorage, ['validation', 'hasAllRequiredFilled']), + ); + + if (hasAllRequiredFilled && iGetProvisionSource(state, id) === ProvisionSource.DISK) { + hasAllRequiredFilled = !!iStorages.find( + (storageBundle) => + iGetIn(storageBundle, ['disk', 'bootOrder']) === 1 && + iGetIn(storageBundle, ['disk', 'disk']) && + (iGetIn(storageBundle, ['volume', 'dataVolume']) || + iGetIn(storageBundle, ['volume', 'persistentVolumeClaim'])), + ); + } + + let isValid = hasAllRequiredFilled; + + if (isValid) { + isValid = iStorages.every((iStorage) => iGetIn(iStorage, ['validation', 'isValid'])); + } + + if (checkTabValidityChanged(state, id, VMWizardTab.STORAGE, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.STORAGE, + isValid, + hasAllRequiredFilled, + ), + ); + } +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts index 59c395f6d9f..757bf8c9295 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts @@ -18,6 +18,10 @@ export const getValidationUpdate = ( const field = fields.get(validationFieldKey); + if (field.get('skipValidation')) { + return updateAcc; + } + const detectValues = _.isFunction(detectValueChanges) ? detectValueChanges(field, options) : detectValueChanges; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts index 36cdfebf344..152afaab272 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts @@ -2,10 +2,10 @@ import { isEmpty } from 'lodash'; import { List } from 'immutable'; import { VMSettingsField, VMWizardProps, VMWizardTab } from '../../types'; import { - iGetFieldKey, - iGetVmSettings, hasVmSettingsChanged, + iGetFieldKey, iGetFieldValue, + iGetVmSettings, isFieldRequired, } from '../../selectors/immutable/vm-settings'; import { @@ -24,19 +24,16 @@ import { VIRTUAL_MACHINE_EXISTS, VIRTUAL_MACHINE_TEMPLATE_EXISTS, } from '../../../../utils/validations/strings'; -import { concatImmutableLists, immutableListToShallowJS } from '../../../../utils/immutable'; +import { concatImmutableLists } from '../../../../utils/immutable'; import { getFieldTitle } from '../../utils/vm-settings-tab-utils'; import { + checkTabValidityChanged, iGetCommonData, iGetLoadedCommonData, iGetName, immutableListToShallowMetadataJS, } from '../../selectors/immutable/selectors'; -import { - validatePositiveInteger, - validateTrim, - validateURL, -} from '../../../../utils/validations/common'; +import { validatePositiveInteger } from '../../../../utils/validations/common'; import { getValidationUpdate } from './utils'; const validateVm: VmSettingsValidator = (field, options) => { @@ -77,11 +74,7 @@ export const validateUserTemplate: VmSettingsValidator = (field, options) => { (template) => iGetName(template) === userTemplateName, ); - const dataVolumes = immutableListToShallowJS( - iGetLoadedCommonData(state, id, VMWizardProps.userTemplates), - ); - - return validateUserTemplateProvisionSource(userTemplate && userTemplate.toJSON(), dataVolumes); + return validateUserTemplateProvisionSource(userTemplate && userTemplate.toJSON()); }; const asVMSettingsFieldValidator = ( @@ -110,19 +103,11 @@ const validationConfig: VMSettingsValidationConfig = { }, validator: validateVm, }, - [VMSettingsField.CONTAINER_IMAGE]: { - detectValueChanges: [VMSettingsField.CONTAINER_IMAGE], - validator: asVMSettingsFieldValidator(validateTrim), - }, [VMSettingsField.USER_TEMPLATE]: { detectValueChanges: [VMSettingsField.USER_TEMPLATE], - detectCommonDataChanges: [VMWizardProps.userTemplates, VMWizardProps.dataVolumes], + detectCommonDataChanges: [VMWizardProps.userTemplates], validator: validateUserTemplate, }, - [VMSettingsField.IMAGE_URL]: { - detectValueChanges: [VMSettingsField.IMAGE_URL], - validator: asVMSettingsFieldValidator(validateURL), - }, [VMSettingsField.CPU]: { detectValueChanges: [VMSettingsField.CPU], validator: asVMSettingsFieldValidator(validatePositiveInteger), @@ -152,7 +137,7 @@ export const setVmSettingsTabValidity = (options: UpdateOptions) => { // check if all required fields are defined const hasAllRequiredFilled = vmSettings - .filter((field) => isFieldRequired(field)) + .filter((field) => isFieldRequired(field) && !field.get('skipValidation')) .every((field) => field.get('value')); let isValid = hasAllRequiredFilled; @@ -163,12 +148,14 @@ export const setVmSettingsTabValidity = (options: UpdateOptions) => { ); } - dispatch( - vmWizardInternalActions[InternalActionType.SetTabValidity]( - id, - VMWizardTab.VM_SETTINGS, - isValid, - hasAllRequiredFilled, - ), - ); + if (checkTabValidityChanged(state, id, VMWizardTab.VM_SETTINGS, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.VM_SETTINGS, + isValid, + hasAllRequiredFilled, + ), + ); + } }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx index 3c905d9ea70..4b35143cbdd 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx @@ -22,12 +22,8 @@ const stateToProps = (state, { wizardReduxID }) => ({ errors: [ asError(state, wizardReduxID, VMWizardProps.commonTemplates), asError(state, wizardReduxID, VMWizardProps.userTemplates), - asError(state, wizardReduxID, VMWizardProps.networkConfigs), - asError(state, wizardReduxID, VMWizardProps.persistentVolumeClaims), - asError(state, wizardReduxID, VMWizardProps.dataVolumes), - asError(state, wizardReduxID, VMWizardProps.storageClasses), asError(state, wizardReduxID, VMWizardProps.virtualMachines, AlertVariant.warning), // for validation only - ], + ].filter((err) => err && err.message), }); export const ResourceLoadErrors = connect(stateToProps)(Errors); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts new file mode 100644 index 00000000000..d6b307cfe18 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts @@ -0,0 +1,15 @@ +import { iGetIn } from '../../../../utils/immutable'; +import { CloudInitField, VMWizardTab } from '../../types'; +import { iGetCreateVMWizardTabs } from './selectors'; + +export const iGetCloudInitValue = ( + state, + id: string, + key: CloudInitField, + defaultValue = undefined, +) => + iGetIn( + iGetCreateVMWizardTabs(state, id), + [VMWizardTab.ADVANCED_CLOUD_INIT, 'value', key, 'value'], + defaultValue, + ); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts index 6997327d3ed..27fbb1d4dcf 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts @@ -3,4 +3,4 @@ import { VMWizardTab } from '../../types'; import { iGetCreateVMWizardTabs } from './selectors'; export const iGetNetworks = (state, id: string) => - iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.NETWORKS, 'value']); + iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.NETWORKING, 'value']); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts index 3c687831a41..c26c6d82607 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts @@ -1,13 +1,28 @@ import { K8sResourceKind } from '@console/internal/module/k8s'; import { iGet, iGetIn, iGetLoadedData } from '../../../../utils/immutable'; import { getCreateVMWizards } from '../selectors'; -import { CommonDataProp } from '../../types'; +import { CommonDataProp, VMWizardTab } from '../../types'; +import { hasStepAllRequiredFilled, isStepValid } from './wizard-selectors'; export const iGetCreateVMWizard = (state, reduxID: string) => iGet(getCreateVMWizards(state), reduxID); export const iGetCreateVMWizardTabs = (state, reduxID: string) => iGet(iGetCreateVMWizard(state, reduxID), 'tabs'); +export const checkTabValidityChanged = ( + state, + reduxID: string, + tab: VMWizardTab, + nextIsValid, + nextHasAllRequiredFilled, +) => { + const tabs = iGetCreateVMWizardTabs(state, reduxID); + return ( + isStepValid(tabs, tab) !== nextIsValid || + hasStepAllRequiredFilled(tabs, tab) !== nextHasAllRequiredFilled + ); +}; + export const iGetCommonData = (state, reduxID: string, key: CommonDataProp) => { const wizard = iGetCreateVMWizard(state, reduxID); const data = iGetIn(wizard, ['commonData', 'data', key]); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts index 7767d701d58..0389c70ffe4 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts @@ -1,6 +1,17 @@ -import { iGetIn } from '../../../../utils/immutable'; -import { VMWizardTab } from '../../types'; +import { iGet, iGetIn } from '../../../../utils/immutable'; +import { VMWizardStorageType, VMWizardTab } from '../../types'; import { iGetCreateVMWizardTabs } from './selectors'; export const iGetStorages = (state, id: string) => iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.STORAGE, 'value']); + +export const iGetProvisionSourceStorage = (state, id: string) => + iGetStorages(state, id).find((storage) => + [ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(iGet(storage, 'type')), + ); + +export const iGetCloudInitNoCloudStorage = (state, id: string) => + iGetStorages(state, id).find((storage) => iGetIn(storage, ['volume', 'cloudInitNoCloud'])); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts index 794aab78e6f..c81fd040852 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts @@ -1,5 +1,6 @@ import { hasTruthyValue, iGet, iGetIn } from '../../../../utils/immutable'; import { VMSettingsField, VMWizardTab } from '../../types'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; import { iGetCreateVMWizardTabs } from './selectors'; export const VM_SETTINGS_METADATA_ID = 'VM_SETTINGS_METADATA_ID'; @@ -35,3 +36,6 @@ export const iGetVmSettingValue = ( key: VMSettingsField, defaultValue = undefined, ) => iGetVmSettingAttribute(state, id, key, 'value', defaultValue); + +export const iGetProvisionSource = (state, id: string) => + ProvisionSource.fromString(iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE)); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts index c962e44bcc3..8d10315fc04 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts @@ -1,5 +1,48 @@ import { get } from 'lodash'; import { Map } from 'immutable'; +import { iGetIn, immutableListToShallowJS } from '../../../utils/immutable'; +import { + VMWizardNetwork, + VMWizardNetworkWithWrappers, + VMWizardStorage, + VMWizardStorageWithWrappers, + VMWizardTab, +} from '../types'; +import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; +import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; export const getCreateVMWizards = (state): Map => get(state, ['kubevirt', 'createVmWizards']); + +export const getNetworks = (state, id: string): VMWizardNetwork[] => + immutableListToShallowJS( + iGetIn(getCreateVMWizards(state), [id, 'tabs', VMWizardTab.NETWORKING, 'value']), + ); + +export const getStorages = (state, id: string): VMWizardStorage[] => + immutableListToShallowJS( + iGetIn(getCreateVMWizards(state), [id, 'tabs', VMWizardTab.STORAGE, 'value']), + ); + +export const getNetworksWithWrappers = (state, id: string): VMWizardNetworkWithWrappers[] => + getNetworks(state, id).map(({ network, networkInterface, ...rest }) => ({ + networkInterfaceWrapper: NetworkInterfaceWrapper.initialize(networkInterface), + networkWrapper: NetworkWrapper.initialize(network), + networkInterface, + network, + ...rest, + })); + +export const getStoragesWithWrappers = (state, id: string): VMWizardStorageWithWrappers[] => + getStorages(state, id).map(({ disk, volume, dataVolume, ...rest }) => ({ + diskWrapper: DiskWrapper.initialize(disk), + volumeWrapper: VolumeWrapper.initialize(volume), + dataVolumeWrapper: dataVolume && DataVolumeWrapper.initialize(dataVolume), + disk, + volume, + dataVolume, + ...rest, + })); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts new file mode 100644 index 00000000000..d84fbb360fb --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts @@ -0,0 +1,4 @@ +export const ADD_NETWORK_INTERFACE = 'Add Network Interface'; +export const PXE_NIC_NOT_FOUND_ERROR = 'A PXE-capable network interface could not be found.'; +export const PXE_INFO = 'Pod network is not PXE bootable'; +export const SELECT_PXE_NIC = '--- Select PXE network interface ---'; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts new file mode 100644 index 00000000000..939db2b9cb2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts @@ -0,0 +1,3 @@ +export const ADD_DISK = 'Add Disk'; +export const NO_BOOTABLE_ATTACHED_DISK_ERROR = 'A bootable attached disk could not be found'; +export const SELECT_BOOTABLE_DISK = '--- Select Bootable Disk ---'; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts index fccadae4727..72e92c83dcc 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts @@ -11,8 +11,9 @@ export const getCreateVMLikeEntityLabel = (isTemplate: boolean) => export const TabTitleResolver = { [VMWizardTab.VM_SETTINGS]: 'General', - [VMWizardTab.NETWORKS]: 'Networking', + [VMWizardTab.NETWORKING]: 'Networking', [VMWizardTab.STORAGE]: 'Storage', + [VMWizardTab.ADVANCED_CLOUD_INIT]: 'Cloud-init', [VMWizardTab.REVIEW]: 'Review', [VMWizardTab.RESULT]: 'Result', }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts index b3de2c7ee59..5671c5f0b5d 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts @@ -1,5 +1,5 @@ import { VMSettingsField, VMSettingsRenderableFieldResolver } from '../types'; -import { ProvisionSource } from '../../../types/vm'; +import { ProvisionSource } from '../../../constants/vm/provision-source'; export const titleResolver: VMSettingsRenderableFieldResolver = { [VMSettingsField.NAME]: 'Name', @@ -15,11 +15,6 @@ export const titleResolver: VMSettingsRenderableFieldResolver = { [VMSettingsField.CPU]: 'CPUs', [VMSettingsField.WORKLOAD_PROFILE]: 'Workload Profile', [VMSettingsField.START_VM]: 'Start virtual machine on creation', - [VMSettingsField.USE_CLOUD_INIT]: 'Use cloud-init', - [VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT]: 'Use custom script', - [VMSettingsField.HOST_NAME]: 'Hostname', - [VMSettingsField.AUTHKEYS]: 'Authenticated SSH Keys', - [VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT]: 'Custom Script', }; export const placeholderResolver = { @@ -32,17 +27,15 @@ export const placeholderResolver = { }; const provisionSourceHelpResolver = { - [ProvisionSource.URL]: - 'An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.', - [ProvisionSource.PXE]: 'Discover provisionable virtual machines over the network.', - [ProvisionSource.CONTAINER]: - 'Ephemeral virtual machine disk image which will be pulled from container registry.', - [ProvisionSource.IMPORT]: 'Import a virtual machine from external service using a provider.', - [ProvisionSource.CLONED_DISK]: 'Select an existing PVC in Storage tab', + [ProvisionSource.URL.getValue()]: 'An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.', + [ProvisionSource.PXE.getValue()]: 'Discover provisionable virtual machines over the network.', + [ProvisionSource.CONTAINER.getValue()]: 'Ephemeral virtual machine disk image which will be pulled from container registry.', + [ProvisionSource.IMPORT.getValue()]: 'Import a virtual machine from external service using a provider.', + [ProvisionSource.DISK.getValue()]: 'Select an existing PVC in Storage tab', }; export const helpResolver = { - [VMSettingsField.PROVISION_SOURCE_TYPE]: (sourceType: ProvisionSource) => + [VMSettingsField.PROVISION_SOURCE_TYPE]: (sourceType: string) => provisionSourceHelpResolver[sourceType], [VMSettingsField.PROVIDER]: (provider) => `Not Implemented for ${provider}!!!`, [VMSettingsField.FLAVOR]: () => diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss new file mode 100644 index 00000000000..f4fbc62b330 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss @@ -0,0 +1,16 @@ +.kubevirt-create-vm-modal__cloud-init-base64 > input[type="checkbox"] { + margin: -0.1em 0 0 !important; +} + +.kubevirt-create-vm-modal__cloud-init-custom-script-text-area { + height: 50vh !important; + resize: vertical; +} + +.kubevirt-create-vm-modal__cloud-init-ssh-keys-text-area { + resize: vertical; +} + +.kubevirt-create-vm-modal__cloud-init-ssh-keys-row { + padding-bottom: 1rem; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx new file mode 100644 index 00000000000..c3b9185af75 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx @@ -0,0 +1,339 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { + AlertVariant, + Button, + ButtonVariant, + Checkbox, + Form, + Split, + SplitItem, + TextArea, + TextInput, +} from '@patternfly/react-core'; +import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import { confirmModal } from '@console/internal/components/modals'; +import { CloudInitField, VMWizardStorage, VMWizardStorageType } from '../../types'; +import { vmWizardActions } from '../../redux/actions'; +import { ActionType } from '../../redux/types'; +import { iGetCloudInitNoCloudStorage } from '../../selectors/immutable/storage'; +import { MutableVolumeWrapper, VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { iGet, iGetIn, ihasIn, toJS, toShallowJS } from '../../../../utils/immutable'; +import { DiskBus, DiskType, VolumeType } from '../../../../constants/vm/storage'; +import { FormRow } from '../../../form/form-row'; +import { joinIDs, prefixedID } from '../../../../utils'; +import { Errors } from '../../../errors/errors'; +import { CLOUDINIT_DISK } from '../../../../constants/vm'; +import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; +import { InlineBooleanRadio } from '../../../inline-boolean-radio'; +import { iGetCloudInitValue } from '../../selectors/immutable/cloud-init'; +import { + CloudInitDataFormKeys, + CloudInitDataHelper, +} from '../../../../k8s/wrapper/vm/cloud-init-data-helper'; + +import './cloud-init-tab.scss'; + +type CustomScriptProps = { + id: string; + isDisabled?: boolean; + value: string; + onChange: (value: string) => void; +}; + +const CustomScript: React.FC = ({ id, isDisabled, value, onChange }) => ( + +