diff --git a/package.json b/package.json index 305185c67..6b404eb04 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,12 @@ "patternfly-react": "2.x", "prettier": "1.x", "prop-types": "15.x", + "redux": "3.x", "react": "16.x", "react-cosmos": "4.7.x", "react-cosmos-test": "4.7.x", "react-dom": "16.x", + "react-redux": "5.x", "react-router-dom": "4.x", "require-all": "3.x", "sass-loader": "7.x", diff --git a/sass/components/Wizard/create-vm-wizard.scss b/sass/components/Wizard/create-vm-wizard.scss index 01aa5ecb7..54bc48071 100644 --- a/sass/components/Wizard/create-vm-wizard.scss +++ b/sass/components/Wizard/create-vm-wizard.scss @@ -23,10 +23,8 @@ min-height: 100px; } -.kubevirt-create-vm-wizard__import-vmware-passwordcheck-button-section { - margin-top: 10px; /* More compact, higher-level sections are 15px */ - display: inline-flex; - width: 100%; +.kubevirt-create-vm-wizard__import-vmware-password { + margin-bottom: 10px; /* More compact, higher-level sections are 15px */ } .kubevirt-create-vm-wizard__import-vmware-passwordcheck-button { diff --git a/src/components/Form/Dropdown.js b/src/components/Form/Dropdown.js index a5f9b6de1..95b591ed0 100644 --- a/src/components/Form/Dropdown.js +++ b/src/components/Form/Dropdown.js @@ -1,10 +1,11 @@ import React from 'react'; +import { isObject } from 'lodash'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { ButtonGroup, DropdownButton, MenuItem, noop, Tooltip, OverlayTrigger } from 'patternfly-react'; export const Dropdown = ({ id, value, disabled, onChange, choices, className, withTooltips }) => { - const title = typeof value === 'object' ? value.name || value.id : value; + const title = isObject(value) ? value.name || value.id : value; return ( {choices.map(choice => { - const isObject = typeof choice === 'object'; - const key = isObject ? choice.id || choice.name : choice; - const val = isObject ? choice.name : choice; + const key = isObject(choice) ? choice.id || choice.name : choice; + const val = isObject(choice) ? choice.name : choice; const content = ( diff --git a/src/components/Form/FormRow.js b/src/components/Form/FormRow.js new file mode 100644 index 000000000..6c74b3049 --- /dev/null +++ b/src/components/Form/FormRow.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Col, ControlLabel, FormGroup, HelpBlock, FieldLevelHelp } from 'patternfly-react'; +import { get } from 'lodash'; + +import { VALIDATION_INFO_TYPE } from '../../constants'; +import { prefixedId } from '../../utils'; + +export const FormControlLabel = ({ isRequired, title, help }) => ( + + {title} + {help && } + +); + +FormControlLabel.defaultProps = { + isRequired: false, + title: '', + help: '', +}; + +FormControlLabel.propTypes = { + isRequired: PropTypes.bool, + title: PropTypes.string, + help: PropTypes.string, +}; + +export const ValidationFormRow = ({ + children, + id, + className, + isHidden, + validation, + controlSize, + labelSize, + textPosition, + ...props +}) => { + if (isHidden) { + return null; + } + + return ( + + + + + {children} + + ); +}; + +ValidationFormRow.defaultProps = { + ...FormControlLabel.defaultProps, + children: null, + id: null, + className: null, + isHidden: false, + validation: {}, + controlSize: 5, + labelSize: 3, + textPosition: 'text-right', +}; + +ValidationFormRow.propTypes = { + ...FormControlLabel.propTypes, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + id: PropTypes.string, + className: PropTypes.string, + isHidden: PropTypes.bool, + validation: PropTypes.object, + controlSize: PropTypes.number, + labelSize: PropTypes.number, + textPosition: PropTypes.string, +}; + +export const FormRow = ({ children, validation, ...props }) => { + const validationMessage = get(validation, 'message', null); + + return ( + + {children} + {validationMessage && {validationMessage}} + + ); +}; + +FormRow.defaultProps = ValidationFormRow.defaultProps; + +FormRow.propTypes = ValidationFormRow.propTypes; diff --git a/src/components/Form/fixtures/FormRow.fixture.js b/src/components/Form/fixtures/FormRow.fixture.js new file mode 100644 index 000000000..d44118051 --- /dev/null +++ b/src/components/Form/fixtures/FormRow.fixture.js @@ -0,0 +1,27 @@ +import { FormRow, ValidationFormRow } from '../FormRow'; +import { VALIDATION_ERROR_TYPE } from '../../../constants'; + +export default [ + { + component: FormRow, + name: 'FormRow', + props: { + id: '1', + children: 'text', + onChange: () => {}, + }, + }, + { + component: ValidationFormRow, + name: 'error ValidationFormRow', + props: { + id: '1', + children: 'test', + validation: { + message: 'error validation', + type: VALIDATION_ERROR_TYPE, + }, + onChange: () => {}, + }, + }, +]; diff --git a/src/components/Form/tests/FormRow.test.js b/src/components/Form/tests/FormRow.test.js new file mode 100644 index 000000000..c5e291035 --- /dev/null +++ b/src/components/Form/tests/FormRow.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { FormRow, ValidationFormRow, FormControlLabel } from '../FormRow'; + +describe('', () => { + it('renders FormRow correctly', () => { + const component = shallow(test); + expect(component).toMatchSnapshot(); + }); + it('renders ValidationFormRow correctly', () => { + const component = shallow(test); + expect(component).toMatchSnapshot(); + }); + it('renders FormControlLabel correctly', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/Form/tests/__snapshots__/FormRow.test.js.snap b/src/components/Form/tests/__snapshots__/FormRow.test.js.snap new file mode 100644 index 000000000..d3f201d7e --- /dev/null +++ b/src/components/Form/tests/__snapshots__/FormRow.test.js.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders FormControlLabel correctly 1`] = ` + + + test + + +`; + +exports[` renders FormRow correctly 1`] = ` + + test + +`; + +exports[` renders ValidationFormRow correctly 1`] = ` + + + + + + test + + +`; diff --git a/src/components/Wizard/CreateVmWizard/BasicSettingsTab.js b/src/components/Wizard/CreateVmWizard/BasicSettingsTab.js deleted file mode 100644 index d3b4725bf..000000000 --- a/src/components/Wizard/CreateVmWizard/BasicSettingsTab.js +++ /dev/null @@ -1,507 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { get } from 'lodash'; - -import { - FormFactory, - validateForm, - getFieldValidation, - DROPDOWN, - CHECKBOX, - TEXT_AREA, - POSITIVE_NUMBER, -} from '../../Form'; -import { getName, getMemory, getCpu, getCloudInitUserData } from '../../../selectors'; -import { getTemplate, getTemplateProvisionSource } from '../../../utils/templates'; -import { validateDNS1123SubdomainValue, validateURL, validateContainer } from '../../../utils/validations'; -import { - NO_TEMPLATE, - HELP_OS, - HELP_FLAVOR, - HELP_MEMORY, - HELP_CPU, - HELP_WORKLOAD, - getProvisionSourceHelp, -} from './strings'; - -import { - getFlavors, - getOperatingSystems, - getWorkloadProfiles, - isFlavorType, - isImageSourceType, - selectVm, - getTemplateFlavors, - getTemplateWorkloadProfiles, - getTemplateOperatingSystems, - settingsValue, - getV2VVmwareName, -} from '../../../k8s/selectors'; - -import { - CUSTOM_FLAVOR, - PROVISION_SOURCE_PXE, - PROVISION_SOURCE_CONTAINER, - PROVISION_SOURCE_URL, - PROVISION_SOURCE_IMPORT, - PROVISION_SOURCE_CLONED_DISK, - TEMPLATE_TYPE_VM, -} from '../../../constants'; - -import { - NAME_KEY, - NAMESPACE_KEY, - DESCRIPTION_KEY, - PROVISION_SOURCE_TYPE_KEY, - CONTAINER_IMAGE_KEY, - IMAGE_URL_KEY, - USER_TEMPLATE_KEY, - OPERATING_SYSTEM_KEY, - FLAVOR_KEY, - MEMORY_KEY, - CPU_KEY, - WORKLOAD_PROFILE_KEY, - START_VM_KEY, - CLOUD_INIT_KEY, - USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY, - CLOUD_INIT_CUSTOM_SCRIPT_KEY, - HOST_NAME_KEY, - AUTHKEYS_KEY, - BATCH_CHANGES_KEY, - INTERMEDIARY_NETWORKS_TAB_KEY, - INTERMEDIARY_STORAGE_TAB_KEY, -} from './constants'; - -import { importProviders } from './providers'; -import { V2VVMwareModel } from '../../../models'; - -export const getFormFields = ( - basicSettings, - namespaces, - templates, - selectedNamespace, - createTemplate, - WithResources, - k8sCreate, - k8sGet, - k8sPatch -) => { - const userTemplate = get(basicSettings[USER_TEMPLATE_KEY], 'value'); - const workloadProfiles = getWorkloadProfiles(basicSettings, templates, userTemplate); - const operatingSystems = getOperatingSystems(basicSettings, templates, userTemplate); - const flavors = getFlavors(basicSettings, templates, userTemplate); - const imageSources = [ - PROVISION_SOURCE_PXE, - PROVISION_SOURCE_URL, - PROVISION_SOURCE_CONTAINER, - PROVISION_SOURCE_CLONED_DISK, - ]; - const userTemplateNames = getTemplate(templates, TEMPLATE_TYPE_VM).map(getName); - userTemplateNames.push(NO_TEMPLATE); - - let namespaceDropdown; - let startVmCheckbox; - let userTemplateDropdown; - let providersSection = {}; - - if (!selectedNamespace) { - namespaceDropdown = { - id: 'namespace-dropdown', - title: 'Namespace', - type: DROPDOWN, - defaultValue: '--- Select Namespace ---', - choices: namespaces.map(getName), - required: true, - }; - } - - if (!createTemplate) { - startVmCheckbox = { - id: 'start-vm', - title: 'Start virtual machine on creation', - type: CHECKBOX, - isVisible: basicVmSettings => !isImageSourceType(basicVmSettings, PROVISION_SOURCE_IMPORT), - noBottom: true, - }; - userTemplateDropdown = { - id: 'template-dropdown', - title: 'Template', - type: DROPDOWN, - defaultValue: '--- Select Template ---', - choices: userTemplateNames, - }; - - providersSection = importProviders(basicSettings, operatingSystems, WithResources, k8sCreate, k8sGet, k8sPatch); - imageSources.push(PROVISION_SOURCE_IMPORT); - } - - return { - [NAME_KEY]: { - id: 'vm-name', - title: 'Name', - required: true, - validate: settings => validateDNS1123SubdomainValue(settingsValue(settings, NAME_KEY)), - }, - [DESCRIPTION_KEY]: { - id: 'vm-description', - title: 'Description', - type: TEXT_AREA, - }, - [NAMESPACE_KEY]: namespaceDropdown, - [USER_TEMPLATE_KEY]: userTemplateDropdown, - [PROVISION_SOURCE_TYPE_KEY]: { - id: 'image-source-type-dropdown', - title: 'Provision Source', - type: DROPDOWN, - defaultValue: '--- Select Provision Source ---', - choices: imageSources, - required: true, - disabled: userTemplate !== undefined, - help: getProvisionSourceHelp(settingsValue(basicSettings, PROVISION_SOURCE_TYPE_KEY)), - }, - [CONTAINER_IMAGE_KEY]: { - id: 'provision-source-container', - title: 'Container Image', - required: true, - isVisible: basicVmSettings => isImageSourceType(basicVmSettings, PROVISION_SOURCE_CONTAINER), - disabled: userTemplate !== undefined, - validate: settings => validateContainer(settingsValue(settings, CONTAINER_IMAGE_KEY)), - }, - [IMAGE_URL_KEY]: { - id: 'provision-source-url', - title: 'URL', - required: true, - isVisible: basicVmSettings => isImageSourceType(basicVmSettings, PROVISION_SOURCE_URL), - disabled: userTemplate !== undefined, - validate: settings => validateURL(settingsValue(settings, IMAGE_URL_KEY)), - }, - ...providersSection, - [OPERATING_SYSTEM_KEY]: { - id: 'operating-system-dropdown', - title: 'Operating System', - type: DROPDOWN, - defaultValue: '--- Select Operating System ---', - choices: operatingSystems, - required: true, - disabled: userTemplate !== undefined, - help: HELP_OS, - }, - [FLAVOR_KEY]: { - id: 'flavor-dropdown', - title: 'Flavor', - type: DROPDOWN, - defaultValue: '--- Select Flavor ---', - choices: flavors, - required: true, - disabled: userTemplate !== undefined && flavors.length === 1, - help: HELP_FLAVOR, - }, - [MEMORY_KEY]: { - id: 'resources-memory', - title: 'Memory (GB)', - type: POSITIVE_NUMBER, - required: true, - isVisible: basicVmSettings => isFlavorType(basicVmSettings, CUSTOM_FLAVOR), - help: HELP_MEMORY, - }, - [CPU_KEY]: { - id: 'resources-cpu', - title: 'CPUs', - type: POSITIVE_NUMBER, - required: true, - isVisible: basicVmSettings => isFlavorType(basicVmSettings, CUSTOM_FLAVOR), - help: HELP_CPU, - }, - [WORKLOAD_PROFILE_KEY]: { - id: 'workload-profile-dropdown', - title: 'Workload Profile', - type: DROPDOWN, - defaultValue: '--- Select Workload Profile ---', - choices: workloadProfiles, - required: true, - help: HELP_WORKLOAD, - disabled: userTemplate !== undefined, - }, - [START_VM_KEY]: startVmCheckbox, - [CLOUD_INIT_KEY]: { - id: 'use-cloud-init', - title: 'Use cloud-init', - type: CHECKBOX, - }, - [USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY]: { - id: 'use-cloud-init-custom-script', - title: 'Use custom script', - type: CHECKBOX, - isVisible: basicVmSettings => settingsValue(basicVmSettings, CLOUD_INIT_KEY, false), - }, - [HOST_NAME_KEY]: { - id: 'cloud-init-hostname', - title: 'Hostname', - isVisible: basicVmSettings => - settingsValue(basicVmSettings, CLOUD_INIT_KEY, false) && - !settingsValue(basicVmSettings, USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY, false), - }, - [AUTHKEYS_KEY]: { - id: 'cloud-init-ssh', - title: 'Authenticated SSH Keys', - type: TEXT_AREA, - isVisible: basicVmSettings => - settingsValue(basicVmSettings, CLOUD_INIT_KEY, false) && - !settingsValue(basicVmSettings, USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY, false), - }, - [CLOUD_INIT_CUSTOM_SCRIPT_KEY]: { - id: 'cloud-init-custom-script', - title: 'Custom Script', - type: TEXT_AREA, - className: 'kubevirt-create-vm-wizard__custom-cloud-script-textarea', - isVisible: basicVmSettings => - settingsValue(basicVmSettings, CLOUD_INIT_KEY, false) && - settingsValue(basicVmSettings, USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY, false), - }, - }; -}; - -const asValueObject = (value, validation) => ({ - value, - validation, -}); - -// TODO: To avoid race conditions as the basicSettings tab is bound to the CreateVmWizard state at the render-time, it would -// be better to retrieve basicSettings at the time of its actual use - to align behavior with the setState(). -// The onChange() bellow should be called with just the diff, not whole copy of the stepData. -const publish = ({ basicSettings, templates, onChange, dataVolumes }, value, target, formValid, formFields) => { - let newBasicSettings; - - if (target === PROVISION_SOURCE_TYPE_KEY && value.value === PROVISION_SOURCE_IMPORT) { - basicSettings = { - ...basicSettings, - [START_VM_KEY]: asValueObject(false), - }; - } - - if (target === BATCH_CHANGES_KEY) { - // the "value" is an array of pairs { value, target } - const difference = value.value.reduce((map, obj) => { - map[obj.target] = { value: obj.value }; // validation will be set in a next step - return map; - }, {}); - newBasicSettings = { - ...basicSettings, - ...difference, - }; - value.value.forEach(pair => { - if (pair.target !== INTERMEDIARY_NETWORKS_TAB_KEY && pair.target !== INTERMEDIARY_STORAGE_TAB_KEY) { - // set field validation - newBasicSettings[pair.target].validation = - pair.validation || getFieldValidation(formFields[pair.target], pair.value, newBasicSettings); - } - }); - formValid = validateForm(formFields, newBasicSettings); - } else { - newBasicSettings = { - ...basicSettings, - [target]: value, - }; - - if (target === USER_TEMPLATE_KEY) { - if (value.value === NO_TEMPLATE) { - newBasicSettings[target] = asValueObject(undefined); - } else { - const allTemplates = getTemplate(templates, TEMPLATE_TYPE_VM); - if (allTemplates.length > 0) { - const userTemplate = allTemplates.find(template => template.metadata.name === value.value); - updateTemplateData(userTemplate, newBasicSettings, dataVolumes); - } - } - formValid = validateForm(formFields, newBasicSettings); - } - } - - onChange(newBasicSettings, formValid); -}; - -const updateTemplateData = (userTemplate, newBasicSettings, dataVolumes) => { - if (userTemplate) { - const vm = selectVm(userTemplate.objects); - - // update flavor - const [flavor] = getTemplateFlavors([userTemplate]); - newBasicSettings[FLAVOR_KEY] = asValueObject(flavor); - if (flavor === CUSTOM_FLAVOR) { - newBasicSettings.cpu = asValueObject(getCpu(vm)); - const memory = getMemory(vm); - newBasicSettings.memory = memory ? asValueObject(parseInt(memory, 10)) : undefined; - } - - // update os - const [os] = getTemplateOperatingSystems([userTemplate]); - newBasicSettings[OPERATING_SYSTEM_KEY] = asValueObject(os); - - // update workload profile - const [workload] = getTemplateWorkloadProfiles([userTemplate]); - newBasicSettings[WORKLOAD_PROFILE_KEY] = asValueObject(workload); - - // update cloud-init - const cloudInitUserData = getCloudInitUserData(vm); - if (cloudInitUserData) { - newBasicSettings[CLOUD_INIT_KEY] = asValueObject(true); - newBasicSettings[USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY] = asValueObject(true); - newBasicSettings[CLOUD_INIT_CUSTOM_SCRIPT_KEY] = asValueObject(cloudInitUserData || ''); - } else if (get(newBasicSettings[CLOUD_INIT_KEY], 'value')) { - newBasicSettings[CLOUD_INIT_KEY] = asValueObject(false); - newBasicSettings[USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY] = asValueObject(false); - } - - // update provision source - const provisionSource = getTemplateProvisionSource(userTemplate, dataVolumes); - if (provisionSource) { - newBasicSettings[PROVISION_SOURCE_TYPE_KEY] = asValueObject(provisionSource.type); - switch (provisionSource.type) { - case PROVISION_SOURCE_CONTAINER: - newBasicSettings[CONTAINER_IMAGE_KEY] = asValueObject(provisionSource.source); - break; - case PROVISION_SOURCE_URL: - newBasicSettings[IMAGE_URL_KEY] = asValueObject(provisionSource.source); - break; - case PROVISION_SOURCE_PXE: - break; - default: - // eslint-disable-next-line - console.warn(`Unknown provision source ${provisionSource.type}`); - break; - } - } else { - // eslint-disable-next-line - console.warn(`Cannot detect provision source for template ${getName(userTemplate)}`); - } - } - return newBasicSettings; -}; - -// Do clean-up -export const onCloseBasic = async (basicSettings, callerContext) => { - const v2vvmwareName = getV2VVmwareName(basicSettings); - if (v2vvmwareName) { - // This is a friendly help to keep things clean. - // If missed here (e.g. when the browser window is closed), the kubevirt-vmware controller's garbage - // collector will do following automatically after a delay. - const resource = { - metadata: { - name: v2vvmwareName, - // TODO: potential issue if the user changed the namespace after creation of the v2vvmware object - // to fix: either store the namespace along the v2vvmwareName or empty v2vvmwareName on namespace change - namespace: settingsValue(basicSettings, NAMESPACE_KEY), - }, - }; - try { - await callerContext.k8sKill(V2VVMwareModel, resource); - } catch (error) { - // eslint-disable-next-line no-console - console.log( - 'Failed to remove temporary V2VVMWare object. It is not an issue, it will be garbage collected later if still present, resource: ', - resource, - ', error: ', - error - ); - } - } -}; - -export class BasicSettingsTab extends React.Component { - constructor(props) { - super(props); - if (props.selectedNamespace) { - this.updateSelectedNamespace(props); - } - } - - componentDidUpdate(prevProps) { - const newNamespace = this.props.selectedNamespace; - const oldNamespace = prevProps.selectedNamespace; - - if (getName(newNamespace) !== getName(oldNamespace)) { - this.updateSelectedNamespace(this.props); - } - } - - updateSelectedNamespace = props => { - const { - basicSettings, - namespaces, - selectedNamespace, - templates, - createTemplate, - WithResources, - k8sCreate, - k8sGet, - k8sPatch, - } = props; - const formFields = getFormFields( - basicSettings, - namespaces, - templates, - selectedNamespace, - createTemplate, - WithResources, - k8sCreate, - k8sGet, - k8sPatch - ); - const valid = validateForm(formFields, basicSettings); - publish(props, asValueObject(getName(selectedNamespace)), NAMESPACE_KEY, valid, formFields); - }; - - render() { - const { - basicSettings, - namespaces, - templates, - selectedNamespace, - createTemplate, - WithResources, - k8sCreate, - k8sGet, - k8sPatch, - } = this.props; - const formFields = getFormFields( - basicSettings, - namespaces, - templates, - selectedNamespace, - createTemplate, - WithResources, - k8sCreate, - k8sGet, - k8sPatch - ); - - return ( - publish(this.props, newValue, target, formValid, formFields)} - /> - ); - } -} - -BasicSettingsTab.defaultProps = { - selectedNamespace: undefined, - createTemplate: false, -}; - -BasicSettingsTab.propTypes = { - WithResources: PropTypes.func.isRequired, - k8sCreate: PropTypes.func.isRequired, - k8sGet: PropTypes.func.isRequired, - k8sPatch: PropTypes.func.isRequired, - templates: PropTypes.array.isRequired, - namespaces: PropTypes.array.isRequired, - selectedNamespace: PropTypes.object, // used only in initialization - basicSettings: PropTypes.object.isRequired, - // eslint-disable-next-line react/no-unused-prop-types - onChange: PropTypes.func.isRequired, - createTemplate: PropTypes.bool, - // eslint-disable-next-line react/no-unused-prop-types - dataVolumes: PropTypes.array.isRequired, -}; diff --git a/src/components/Wizard/CreateVmWizard/CreateVmWizard.js b/src/components/Wizard/CreateVmWizard/CreateVmWizard.js index 3feee5160..82b68afab 100644 --- a/src/components/Wizard/CreateVmWizard/CreateVmWizard.js +++ b/src/components/Wizard/CreateVmWizard/CreateVmWizard.js @@ -1,428 +1,149 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { findIndex } from 'lodash'; import { Wizard } from 'patternfly-react'; -import { BasicSettingsTab, onCloseBasic } from './BasicSettingsTab'; +import { VmSettingsTab } from './VmSettingsTab'; +import { getInitialActiveStepIndex, getTabInitialState } from './initialState'; + +import { validateTabData } from './validations'; import { StorageTab } from './StorageTab'; import { ResultTab } from './ResultTab'; import { ResultTabRow } from './ResultTabRow'; import { NetworksTab } from './NetworksTab'; -import { loadingWizardTab } from '../loadingWizardTab'; -import { settingsValue } from '../../../k8s/selectors'; +import { LoadingTab } from '../LoadingTab'; import { createVm, createVmTemplate } from '../../../k8s/request'; import { - POD_NETWORK, - PROVISION_SOURCE_URL, - PROVISION_SOURCE_CONTAINER, - PROVISION_SOURCE_PXE, - PROVISION_SOURCE_IMPORT, - PROVISION_SOURCE_CLONED_DISK, -} from '../../../constants'; - -import { - PROVISION_SOURCE_TYPE_KEY, - USER_TEMPLATE_KEY, - BASIC_SETTINGS_TAB_KEY, + VM_SETTINGS_TAB_KEY, + NAMESPACE_KEY, NETWORKS_TAB_KEY, - STORAGE_TAB_KEY, + PROVISION_SOURCE_TYPE_KEY, RESULT_TAB_KEY, - STORAGE_TYPE_CONTAINER, - STORAGE_TYPE_DATAVOLUME, - NETWORK_TYPE_POD, - INTERMEDIARY_NETWORKS_TAB_KEY, - NAMESPACE_KEY, - PROVIDER_VMWARE_VM_KEY, - INTERMEDIARY_STORAGE_TAB_KEY, - NETWORK_BINDING_MASQUERADE, + STORAGE_TAB_KEY, } from './constants'; import { CREATE_VM, CREATE_VM_TEMPLATE, + NEXT, STEP_BASIC_SETTINGS, STEP_NETWORK, - STEP_STORAGE, STEP_RESULT, - NEXT, - ERROR, - CREATED, - CREATED_WITH_CLEANUP, - CREATED_WITH_FAILED_CLEANUP, - FAILED_TO_CREATE, - FAILED_TO_PATCH, + STEP_STORAGE, } from './strings'; -import { getFullResourceId } from '../../../utils'; -import { - getUserTemplate, - getTemplateStorages, - getTemplateInterfaces, - hasAutoAttachPodInterface, -} from '../../../utils/templates'; - -import { getName, getGeneratedName, getKind } from '../../../selectors'; import { EnhancedK8sMethods } from '../../../k8s/util/enhancedK8sMethods'; - -// left intentionally empty -const TEMPLATE_ROOT_STORAGE = {}; - -const LoadingBasicWizardTab = loadingWizardTab(BasicSettingsTab); -const LoadingStorageTab = loadingWizardTab(StorageTab); -const LoadingNetworksTab = loadingWizardTab(NetworksTab); - -const getBasicSettingsValue = (stepData, key) => settingsValue(stepData[BASIC_SETTINGS_TAB_KEY].value, key); - -const getInitialDisk = provisionSource => { - switch (provisionSource) { - case PROVISION_SOURCE_URL: - return rootDataVolumeDisk; - case PROVISION_SOURCE_CONTAINER: - return rootContainerDisk; - case PROVISION_SOURCE_PXE: - case PROVISION_SOURCE_CLONED_DISK: - case PROVISION_SOURCE_IMPORT: - return null; - default: - // eslint-disable-next-line - console.warn(`Unknown provision source ${provisionSource}`); - return null; - } -}; - -const onUserTemplateChangedInStorageTab = (stepData, newUserTemplate, dataVolumes) => { - const withoutDiscardedTemplateStorage = stepData[STORAGE_TAB_KEY].value.filter( - storage => !(storage.templateStorage || storage.rootStorage) +import { getUpdatedState } from './stateUpdate/stateUpdate'; +import { cleanupAndGetResults, errorsFirstSort, getResults } from '../../../k8s/util/k8sMethodsUtils'; +import { VMWareImportProvider } from './providers/VMwareImportProvider/VMWareImportProvider'; +import { ImportProvider } from './providers/ImportProvider/ImportProvider'; +import { isVmwareProvider } from './providers/VMwareImportProvider/selectors'; +import { getVmWareProviderRequestedResources } from './providers/VMwareImportProvider/requestedResources'; +import { getVmSettings, getVmSettingValue, onCloseVmSettings } from './utils/vmSettingsTabUtils'; + +const ALL_TAB_KEYS = [VM_SETTINGS_TAB_KEY, NETWORKS_TAB_KEY, STORAGE_TAB_KEY, RESULT_TAB_KEY]; + +const getUpdatedValidatedState = (prevProps, prevState, props, state, extra) => { + const newState = ALL_TAB_KEYS.reduce( + (intermediaryState, tabKey) => + getUpdatedState(tabKey, prevProps, prevState, props, intermediaryState, extra) || intermediaryState, + state ); - const rows = [...withoutDiscardedTemplateStorage]; - - if (newUserTemplate) { - const templateStorages = getTemplateStorages(newUserTemplate, dataVolumes).map(storage => ({ - templateStorage: storage, - rootStorage: storage.disk.bootOrder === 1 ? TEMPLATE_ROOT_STORAGE : undefined, - })); - rows.push(...templateStorages); - } else { - const basicSettings = stepData[BASIC_SETTINGS_TAB_KEY].value; - const storage = getInitialDisk(settingsValue(basicSettings, PROVISION_SOURCE_TYPE_KEY)); - if (storage) { - rows.push(storage); - } + if (state === newState) { + return state; // skip validation } - return { - ...stepData, - [STORAGE_TAB_KEY]: { - ...stepData[STORAGE_TAB_KEY], - value: rows, - }, - }; -}; - -const onUserTemplateChangedInNetworksTab = (stepData, newUserTemplate) => { - const withoutDiscardedTemplateNetworks = stepData[NETWORKS_TAB_KEY].value.filter(network => !network.templateNetwork); - - const rows = [...withoutDiscardedTemplateNetworks]; - if (!rows.find(row => row.rootNetwork)) { - rows.push(podNetwork); - } - - if (newUserTemplate) { - const templateInterfaces = getTemplateInterfaces(newUserTemplate); - const networks = templateInterfaces.map(i => ({ - templateNetwork: i, - })); - - // do not add root interface if there already is one or autoAttachPodInterface is set to false - if (networks.some(network => network.templateNetwork.network.pod) || !hasAutoAttachPodInterface(newUserTemplate)) { - const index = findIndex(rows, network => network.rootNetwork); - rows.splice(index, 1); - } - - rows.push(...networks); - } - - return { - ...stepData, - [NETWORKS_TAB_KEY]: { - ...stepData[NETWORKS_TAB_KEY], - value: rows, - }, - }; -}; - -const rootDisk = { - rootStorage: {}, - name: 'rootdisk', - isBootable: true, -}; - -export const rootContainerDisk = { - ...rootDisk, - storageType: STORAGE_TYPE_CONTAINER, -}; - -export const rootDataVolumeDisk = { - ...rootDisk, - storageType: STORAGE_TYPE_DATAVOLUME, - size: 10, -}; + const validatedStepData = ALL_TAB_KEYS.reduce((resultStepData, tabKey) => { + const { value, valid } = newState.stepData[tabKey]; + resultStepData[tabKey] = validateTabData(tabKey, value, valid); + return resultStepData; + }, {}); -const onImageSourceTypeChangedInStorageTab = stepData => { - const filteredStorage = stepData[STORAGE_TAB_KEY].value.filter( - storage => storage.templateStorage || !storage.rootStorage - ); - const rows = [...filteredStorage]; - const basicSettings = stepData[BASIC_SETTINGS_TAB_KEY].value; - if (!settingsValue(basicSettings, USER_TEMPLATE_KEY)) { - const storage = getInitialDisk(settingsValue(basicSettings, PROVISION_SOURCE_TYPE_KEY)); - if (storage) { - rows.push(storage); - } - } return { - ...stepData, - [STORAGE_TAB_KEY]: { - ...stepData[STORAGE_TAB_KEY], - value: rows, - }, + ...newState, + stepData: validatedStepData, }; }; -const onUserTemplateChanged = (props, stepData) => { - const userTemplateName = getBasicSettingsValue(stepData, USER_TEMPLATE_KEY); - const userTemplate = userTemplateName ? getUserTemplate(props.templates, userTemplateName) : undefined; - const newStepData = onUserTemplateChangedInStorageTab(stepData, userTemplate, props.dataVolumes); - return onUserTemplateChangedInNetworksTab(newStepData, userTemplate); -}; - -const onImageSourceTypeChanged = (props, stepData) => onImageSourceTypeChangedInStorageTab(stepData); - -export const onVmwareVmChanged = (props, stepData) => { - const sourceNetworks = getBasicSettingsValue(stepData, INTERMEDIARY_NETWORKS_TAB_KEY); - delete stepData[BASIC_SETTINGS_TAB_KEY].value[INTERMEDIARY_NETWORKS_TAB_KEY]; // not needed anymore, do not setState() that - - const sourceDisks = getBasicSettingsValue(stepData, INTERMEDIARY_STORAGE_TAB_KEY); - delete stepData[BASIC_SETTINGS_TAB_KEY].value[INTERMEDIARY_STORAGE_TAB_KEY]; - - if (sourceNetworks) { - const rows = sourceNetworks.map((src, index) => ({ - rootNetwork: {}, - id: index, - name: src.name, - mac: src.mac, - network: undefined, // Let the user select proper mapping - networkType: undefined, - editable: true, - edit: false, - importSourceId: src.id, // will be used for pairing when Conversion POD is executed - })); +export class CreateVmWizard extends React.Component { + constructor(props) { + super(props); - stepData = { - ...stepData, - [NETWORKS_TAB_KEY]: { - ...stepData[NETWORKS_TAB_KEY], - value: rows, + const initialState = ALL_TAB_KEYS.reduce( + (state, tabKey) => { + state.stepData[tabKey] = getTabInitialState(tabKey, props); + return state; }, - }; - } + { + activeStepIndex: getInitialActiveStepIndex(), + stepData: {}, + } + ); - if (sourceDisks) { - stepData = { - ...stepData, - [STORAGE_TAB_KEY]: { - ...stepData[STORAGE_TAB_KEY], - value: sourceDisks, - // the "valid" field will be set within StorageTab constructor processing based on the "row.error" - }, - }; + this.state = getUpdatedValidatedState(null, null, props, initialState, { + // eslint-disable-next-line no-console + safeSetState: () => console.warn('setState not supported when initializing'), + }); } - return stepData; -}; - -const podNetwork = { - rootNetwork: {}, - id: 0, - name: 'nic0', - mac: '', - network: POD_NETWORK, - editable: true, - edit: false, - networkType: NETWORK_TYPE_POD, - binding: NETWORK_BINDING_MASQUERADE, -}; - -const k8sObjectToResult = ({ obj, content, message, isExpanded, isError }) => ({ - title: [getKind(obj), getName(obj) || getGeneratedName(obj), message].filter(a => a).join(' '), - content, - isExpanded, - isError, -}); - -const cleanupAndGetResults = async (enhancedK8sMethods, { message, failedObject, failedPatches }) => { - const actualState = enhancedK8sMethods.getActualState(); // actual state will differ after cleanup - - let errors; - try { - await enhancedK8sMethods.rollback(); - } catch (e) { - // eslint-disable-next-line prefer-destructuring - errors = e.errors; + componentDidMount() { + this._unmounted = false; } - const failedObjectsMap = {}; - - if (errors) { - errors.forEach(error => { - failedObjectsMap[getFullResourceId(error.failedObject)] = error.failedObject; - }); + componentWillUnmount() { + this._unmounted = true; } - const cleanupArray = actualState - .map(resource => { - const failedToCleanup = !!failedObjectsMap[getFullResourceId(resource)]; - - return k8sObjectToResult({ - obj: resource, - content: resource, - message: failedToCleanup ? CREATED_WITH_FAILED_CLEANUP : CREATED_WITH_CLEANUP, - isExpanded: failedToCleanup, - isError: failedToCleanup, - }); - }) - .reverse(); - - const results = [ - k8sObjectToResult({ - content: message, - message: ERROR, - isExpanded: true, - isError: true, - }), - k8sObjectToResult({ - obj: failedObject, - content: failedPatches || failedObject, - message: failedPatches ? FAILED_TO_PATCH : FAILED_TO_CREATE, - isError: true, - }), - ...cleanupArray, - ]; - - return { - valid: false, - results, + safeSetState = state => { + if (!this._unmounted) { + this.setState(state); + } }; -}; -const getResults = enhancedK8sMethods => ({ - valid: true, - results: enhancedK8sMethods - .getActualState() - .map(obj => k8sObjectToResult({ obj, content: obj, message: CREATED })) - .reverse(), -}); + componentDidUpdate(prevProps, prevState, snapshot) { + const newState = getUpdatedValidatedState(prevProps, prevState, this.props, this.state, { + safeSetState: this.safeSetState, + }); -export class CreateVmWizard extends React.Component { - state = { - activeStepIndex: 0, - stepData: { - [BASIC_SETTINGS_TAB_KEY]: { - // Basic Settings - value: {}, - valid: false, - }, - [NETWORKS_TAB_KEY]: { - value: [podNetwork], - valid: true, - }, - [STORAGE_TAB_KEY]: { - value: [], // Storages - valid: true, // empty Storages are valid - }, - [RESULT_TAB_KEY]: { - value: [], - valid: null, // result of the request - }, - }, - }; + if (this.state !== newState) { + // eslint-disable-next-line react/no-did-update-set-state + this.safeSetState(newState); + } + } getLastStepIndex = () => this.wizardStepsNewVM.length - 1; lastStepReached = () => this.state.activeStepIndex === this.getLastStepIndex(); - callbacks = [ - { - field: USER_TEMPLATE_KEY, - callback: onUserTemplateChanged, - }, - { - field: PROVISION_SOURCE_TYPE_KEY, - callback: onImageSourceTypeChanged, - }, - { - field: INTERMEDIARY_NETWORKS_TAB_KEY, // can not use PROVIDER_VMWARE_VM_KEY to detect changes due to asynchronous load of details _after_ selection - callback: onVmwareVmChanged, - }, - ]; - - onStepDataChanged = (key, value, valid) => { - this.setState((state, props) => { - let stepData = { ...state.stepData }; - - stepData[key] = { - value, - valid, - }; - - if (key === BASIC_SETTINGS_TAB_KEY) { - this.callbacks.forEach(callback => { - const oldValue = getBasicSettingsValue(state.stepData, callback.field); - const newValue = getBasicSettingsValue(stepData, callback.field); - if (oldValue !== newValue) { - stepData = callback.callback(props, stepData); - } - }); - } - - return { stepData }; - }); + onStepDataChanged = (tabKey, value, valid) => { + const validatedTabData = validateTabData(tabKey, value, valid); + this.safeSetState(state => ({ + stepData: { + ...state.stepData, + [tabKey]: validatedTabData, + }, + })); }; finish() { const create = this.props.createTemplate ? createVmTemplate : createVm; - const basicSettings = this.state.stepData[BASIC_SETTINGS_TAB_KEY].value; + const vmSettings = this.state.stepData[VM_SETTINGS_TAB_KEY].value; const enhancedK8sMethods = new EnhancedK8sMethods(this.props); create( enhancedK8sMethods, this.props.templates, - basicSettings, + vmSettings, this.state.stepData[NETWORKS_TAB_KEY].value, this.state.stepData[STORAGE_TAB_KEY].value, this.props.persistentVolumeClaims ) .then(() => getResults(enhancedK8sMethods)) .catch(error => cleanupAndGetResults(enhancedK8sMethods, error)) - .then(({ results, valid }) => - this.onStepDataChanged( - RESULT_TAB_KEY, - results - .map((result, sortIndex) => ({ ...result, sortIndex })) - // move errors to the top - .sort((a, b) => { - if (a.isError === b.isError) { - return a.sortIndex - b.sortIndex; - } - return a.isError ? -1 : 1; - }), - valid - ) - ) + .then(({ results, valid }) => this.onStepDataChanged(RESULT_TAB_KEY, errorsFirstSort(results), valid)) // eslint-disable-next-line no-console .catch(e => console.error(e)); } @@ -436,7 +157,7 @@ export class CreateVmWizard extends React.Component { if (newActiveStepIndex === this.getLastStepIndex()) { this.finish(); } - this.setState({ activeStepIndex: newActiveStepIndex }); + this.safeSetState({ activeStepIndex: newActiveStepIndex }); } }; @@ -458,28 +179,32 @@ export class CreateVmWizard extends React.Component { wizardStepsNewVM = [ { title: STEP_BASIC_SETTINGS, - key: BASIC_SETTINGS_TAB_KEY, - onCloseWizard: onCloseBasic, + key: VM_SETTINGS_TAB_KEY, + onCloseWizard: onCloseVmSettings, render: () => { - const loadingData = { - namespaces: this.props.namespaces, - templates: this.props.templates, - dataVolumes: this.props.dataVolumes, - }; + const { namespaces, templates, dataVolumes, WithResources } = this.props; + + const loadingData = { namespaces, templates, dataVolumes }; + const vmSettings = getVmSettings(this.state); + return ( - this.onStepDataChanged(BASIC_SETTINGS_TAB_KEY, value, valid)} - loadingData={loadingData} - createTemplate={this.props.createTemplate} - WithResources={this.props.WithResources} - k8sCreate={this.props.k8sCreate} - k8sGet={this.props.k8sGet} - k8sPatch={this.props.k8sPatch} - k8sKill={this.props.k8sKill} - /> + + this.onStepDataChanged(VM_SETTINGS_TAB_KEY, value, valid)} + {...loadingData} + > + + + this.onStepDataChanged(VM_SETTINGS_TAB_KEY, value, valid)} + /> + + + + ); }, }, @@ -490,17 +215,20 @@ export class CreateVmWizard extends React.Component { const loadingData = { networkConfigs: this.props.networkConfigs, }; - const sourceType = getBasicSettingsValue(this.state.stepData, PROVISION_SOURCE_TYPE_KEY); + const sourceType = getVmSettingValue(this.state, PROVISION_SOURCE_TYPE_KEY); return ( - this.onStepDataChanged(NETWORKS_TAB_KEY, value, valid)} - networkConfigs={this.props.networkConfigs} - networks={this.state.stepData[NETWORKS_TAB_KEY].value || []} - sourceType={sourceType} - namespace={getBasicSettingsValue(this.state.stepData, NAMESPACE_KEY)} - loadingData={loadingData} - isCreateRemoveDisabled={!!getBasicSettingsValue(this.state.stepData, PROVIDER_VMWARE_VM_KEY)} - /> + + this.onStepDataChanged(NETWORKS_TAB_KEY, value, valid)} + networkConfigs={this.props.networkConfigs} + networks={this.state.stepData[NETWORKS_TAB_KEY].value || []} + sourceType={sourceType} + namespace={getVmSettingValue(this.state, NAMESPACE_KEY)} + isCreateRemoveDisabled={isVmwareProvider(this.state)} + {...loadingData} + /> + ); }, }, @@ -512,17 +240,20 @@ export class CreateVmWizard extends React.Component { storageClasses: this.props.storageClasses, persistentVolumeClaims: this.props.persistentVolumeClaims, }; - const sourceType = getBasicSettingsValue(this.state.stepData, PROVISION_SOURCE_TYPE_KEY); + const sourceType = getVmSettingValue(this.state, PROVISION_SOURCE_TYPE_KEY); return ( - this.onStepDataChanged(STORAGE_TAB_KEY, value, valid)} - units={this.props.units} - sourceType={sourceType} - namespace={getBasicSettingsValue(this.state.stepData, NAMESPACE_KEY)} - loadingData={loadingData} - isCreateRemoveDisabled={!!getBasicSettingsValue(this.state.stepData, PROVIDER_VMWARE_VM_KEY)} - /> + + this.onStepDataChanged(STORAGE_TAB_KEY, value, valid)} + units={this.props.units} + sourceType={sourceType} + namespace={getVmSettingValue(this.state, NAMESPACE_KEY)} + isCreateRemoveDisabled={isVmwareProvider(this.state)} + {...loadingData} + /> + ); }, }, @@ -532,7 +263,7 @@ export class CreateVmWizard extends React.Component { render: () => { const stepData = this.state.stepData[RESULT_TAB_KEY]; return ( - + {stepData.value.map((result, index) => ( ))} @@ -554,7 +285,7 @@ export class CreateVmWizard extends React.Component { onHide={this.onHideWrapper} steps={this.wizardStepsNewVM} activeStepIndex={this.state.activeStepIndex} - onStepChanged={index => this.onStepChanged(index)} + onStepChanged={this.onStepChanged} previousStepDisabled={lastStepReached} cancelButtonDisabled={lastStepReached} stepButtonsDisabled={lastStepReached} diff --git a/src/components/Wizard/CreateVmWizard/StorageTab.js b/src/components/Wizard/CreateVmWizard/StorageTab.js index 33aaff1ed..8fe57f65e 100644 --- a/src/components/Wizard/CreateVmWizard/StorageTab.js +++ b/src/components/Wizard/CreateVmWizard/StorageTab.js @@ -190,7 +190,7 @@ const resolveTemplateStorage = (storage, persistentVolumeClaims, storageClasses, } if (!templateStorage.storageClass) { templateStorage.storageClass = getName( - storageClasses.find(clazz => getName(clazz) === pvc.spec.storageClassName) + storageClasses.find(clazz => getName(clazz) === getPvcStorageClassName(pvc)) ); } templateStorage.storageType = STORAGE_TYPE_PVC; diff --git a/src/components/Wizard/CreateVmWizard/VmSettingsTab.js b/src/components/Wizard/CreateVmWizard/VmSettingsTab.js new file mode 100644 index 000000000..1c5f1f22c --- /dev/null +++ b/src/components/Wizard/CreateVmWizard/VmSettingsTab.js @@ -0,0 +1,187 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Form } from 'patternfly-react'; +import { get } from 'lodash'; + +import { Checkbox, Dropdown, Integer, Text, TextArea } from '../../Form'; +import { getName } from '../../../selectors'; +import { getTemplate } from '../../../utils/templates'; + +import { NO_TEMPLATE } from './strings'; + +import { TEMPLATE_TYPE_VM } from '../../../constants'; + +import { + AUTHKEYS_KEY, + CLOUD_INIT_CUSTOM_SCRIPT_KEY, + CONTAINER_IMAGE_KEY, + CPU_KEY, + DESCRIPTION_KEY, + FLAVOR_KEY, + HOST_NAME_KEY, + IMAGE_URL_KEY, + MEMORY_KEY, + NAME_KEY, + NAMESPACE_KEY, + OPERATING_SYSTEM_KEY, + PROVIDER_KEY, + PROVISION_SOURCE_TYPE_KEY, + START_VM_KEY, + USE_CLOUD_INIT_CUSTOM_SCRIPT_KEY, + USE_CLOUD_INIT_KEY, + USER_TEMPLATE_KEY, + WORKLOAD_PROFILE_KEY, +} from './constants'; +import { FormRow } from '../../Form/FormRow'; +import { isFieldDisabled, isFieldHidden, isFieldRequired } from './utils/vmSettingsTabUtils'; +import { getDefaultValue, getFieldHelp, getFieldId, getFieldTitle } from './initialState/vmSettingsTabInitialState'; +import { ImportProvider } from './providers/ImportProvider/ImportProvider'; +import { objectMerge } from '../../../utils'; + +export class VmSettingsTab extends React.Component { + onChange = (key, value) => { + this.props.onChange( + objectMerge({}, this.props.vmSettings, { + [key]: { value }, + }) + ); + }; + + onUserTemplateChange = userTemplate => { + this.onChange(USER_TEMPLATE_KEY, userTemplate === NO_TEMPLATE ? null : userTemplate); + }; + + // helpers + getField = key => get(this.props.vmSettings, key); + + getFieldAttribute = (key, attribute) => get(this.getField(key), attribute); + + getValue = key => this.getFieldAttribute(key, 'value'); + + getRowMetadata = key => ({ + key, + title: getFieldTitle(key), + help: getFieldHelp(key, this.getValue(key)), + validation: this.getFieldAttribute(key, 'validation'), + isHidden: isFieldHidden(this.getField(key)), + isRequired: isFieldRequired(this.getField(key)), + }); + + getFieldData = key => ({ + id: getFieldId(key), + disabled: isFieldDisabled(this.getField(key)), + value: this.getValue(key) || getDefaultValue(key), + onChange: value => this.onChange(key, value), + }); + + getCheckboxFieldData = key => ({ + id: getFieldId(key), + title: getFieldTitle(key), + disabled: isFieldDisabled(this.getField(key)), + checked: this.getValue(key), + onChange: value => this.onChange(key, value), + }); + + render() { + const { namespaces, templates, children } = this.props; + const namespaceNames = namespaces.map(getName); + const userTemplateNames = getTemplate(templates, TEMPLATE_TYPE_VM).map(getName); + userTemplateNames.push(NO_TEMPLATE); + + return ( +
+ + + + +