Sfoglia il codice sorgente

Implement source options support

Sergiu Miclea 7 anni fa
parent
commit
1c33b84e3f

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
     "env:prod": "cross-env NODE_ENV=production",
     "cypress": "cypress open",
     "test": "jest",
+    "testc": "jest --runInBand -t 'WizardOptions Component'",
     "storybook": "start-storybook -p 9001 -c private/storybook",
     "lint": "eslint src private webpack.config.js --ext js,jsx",
     "build:clean": "rimraf \"dist/!(.git*|Procfile)**\"",

+ 1 - 1
src/components/atoms/EndpointLogos/EndpointLogos.jsx

@@ -128,7 +128,7 @@ const widthHeights = [
 ]
 
 type Props = {
-  endpoint?: string,
+  endpoint?: ?string,
   height: number,
   disabled?: boolean,
   'data-test-id'?: string,

+ 2 - 1
src/components/molecules/ActionDropdown/ActionDropdown.jsx

@@ -66,6 +66,7 @@ type Props = {
   label: string,
   actions: Action[],
   style: any,
+  'data-test-id'?: string,
 }
 
 type State = {
@@ -190,7 +191,7 @@ class ActionDropdown extends React.Component<Props, State> {
 
   render() {
     return (
-      <Wrapper style={this.props.style}>
+      <Wrapper style={this.props.style} data-test-id={this.props['data-test-id']}>
         <DropdownButton
           secondary
           centered

+ 2 - 2
src/components/molecules/PropertiesTable/PropertiesTable.jsx

@@ -34,11 +34,11 @@ const Wrapper = styled.div`
   border-radius: ${StyleProps.borderRadius};
 `
 const Column = styled.div`
-  ${StyleProps.exactWidth('calc(50% - 16px)')}
+  ${StyleProps.exactWidth('calc(50% - 32px)')}
   height: 32px;
+  padding: 0 16px;
   display: flex;
   align-items: center;
-  padding-left: 16px;
   ${props => props.header ? css`
     color: ${Palette.grayscale[4]};
     background: ${Palette.grayscale[7]};

+ 3 - 1
src/components/molecules/WizardBreadcrumbs/WizardBreadcrumbs.jsx

@@ -45,13 +45,15 @@ type Props = {
   selected: { id: string },
   wizardType: 'migration' | 'replica',
   destinationProvider: ?string,
+  sourceProvider: ?string,
 }
 @observer
 class WizardBreadcrumbs extends React.Component<Props> {
   render() {
     let pages = wizardConfig.pages
       .filter(p => !p.excludeFrom || p.excludeFrom !== this.props.wizardType)
-      .filter(p => !p.filter || (this.props.destinationProvider && p.filter(this.props.destinationProvider)))
+      .filter(p => !p.targetFilter || (this.props.destinationProvider && p.targetFilter(this.props.destinationProvider)))
+      .filter(p => !p.sourceFilter || (this.props.sourceProvider && p.sourceFilter(this.props.sourceProvider)))
 
     return (
       <Wrapper>

+ 9 - 6
src/components/molecules/WizardBreadcrumbs/test.jsx

@@ -20,30 +20,33 @@ import WizardBreadcrumbs from '.'
 import TW from '../../../utils/TestWrapper'
 import { wizardConfig } from '../../../config'
 
-const wrap = props => new TW(shallow(<WizardBreadcrumbs destinationProvider="oci" {...props} />), 'wBreadCrumbs')
+const wrap = props => new TW(
+  shallow(<WizardBreadcrumbs destinationProvider="oci" sourceProvider="vmware_vsphere" {...props} />),
+  'wBreadCrumbs'
+)
 
 describe('WizardBreadcrumbs Component', () => {
   it('renders correct number of crumbs for replica', () => {
     let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'replica' })
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'replica')
-    expect(wrapper.find('name-', true).length).toBe(pages.length - 1)
+    expect(wrapper.find('name-', true).length).toBe(pages.length - 2)
   })
 
   it('renders correct number of crumbs for migration', () => {
     let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'migration' })
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
-    expect(wrapper.find('name-', true).length).toBe(pages.length - 1)
+    expect(wrapper.find('name-', true).length).toBe(pages.length - 2)
   })
 
   it('has correct page selected', () => {
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
-    let wrapper = wrap({ selected: pages[2], wizardType: 'migration' })
-    expect(wrapper.findText(`name-${pages[2].id}`)).toBe(pages[2].breadcrumb)
+    let wrapper = wrap({ selected: pages[1], wizardType: 'migration' })
+    expect(wrapper.findText(`name-${pages[1].id}`)).toBe(pages[1].breadcrumb)
   })
 
   it('renders correct number of crumbs for Openstack', () => {
     let wrapper = wrap({ selected: wizardConfig.pages[2], wizardType: 'migration', destinationProvider: 'openstack' })
     let pages = wizardConfig.pages.filter(p => !p.excludeFrom || p.excludeFrom !== 'migration')
-    expect(wrapper.find('name-', true).length).toBe(pages.length)
+    expect(wrapper.find('name-', true).length).toBe(pages.length - 1)
   })
 })

+ 1 - 12
src/components/organisms/DetailsContentHeader/DetailsContentHeader.jsx

@@ -146,20 +146,9 @@ class DetailsContentHeader extends React.Component<Props> {
       <ActionDropdown
         actions={this.props.dropdownActions}
         style={{ marginLeft: '32px' }}
+        data-test-id="dcHeader-actionButton"
       />
     )
-
-    // return (
-    //   <Button
-    //     secondary={!this.props.alertButton}
-    //     alert={this.props.alertButton}
-    //     hollow={this.props.hollowButton}
-    //     onClick={this.props.onActionButtonClick}
-    //     disabled={this.props.actionButtonDisabled}
-    //     style={{ marginLeft: '32px' }}
-    //     data-test-id="dcHeader-actionButton"
-    //   >{this.props.buttonLabel}</Button>
-    // )
   }
 
   renderDescription() {

+ 2 - 27
src/components/organisms/DetailsContentHeader/test.jsx

@@ -45,17 +45,9 @@ describe('DetailsContentHeader Component', () => {
     expect(wrapper.find('cancelButton').length).toBe(0)
   })
 
-  it('renders with action button, if there\'s action button handler', () => {
-    let wrapper = wrap({ item, buttonLabel: 'action button', onActionButtonClick: () => { } })
+  it('renders with action button, if there are dropdown actions', () => {
+    let wrapper = wrap({ item, dropdownActions: [] })
     expect(wrapper.find('actionButton').length).toBe(1)
-    expect(wrapper.find('actionButton').shallow.dive().dive().text()).toBe('action button')
-  })
-
-  it('dispatches action button click', () => {
-    let onActionButtonClick = sinon.spy()
-    let wrapper = wrap({ item, buttonLabel: 'action button', onActionButtonClick })
-    wrapper.find('actionButton').simulate('click')
-    expect(onActionButtonClick.calledOnce).toBe(true)
   })
 
   it('dispatches back button click', () => {
@@ -65,23 +57,6 @@ describe('DetailsContentHeader Component', () => {
     expect(onBackButonClick.called).toBe(true)
   })
 
-  it('renders cancel button if status is running', () => {
-    let wrapper = wrap({
-      item: { ...item, executions: [{ ...item.executions[0], status: 'RUNNING' }] },
-    })
-    expect(wrapper.find('cancelButton').length).toBe(1)
-  })
-
-  it('dispatches cancel click', () => {
-    let onCancelClick = sinon.spy()
-    let wrapper = wrap({
-      item: { ...item, executions: [{ ...item.executions[0], status: 'RUNNING' }] },
-      onCancelClick,
-    })
-    wrapper.find('cancelButton').simulate('click')
-    expect(onCancelClick.args[0][0].status).toBe('RUNNING')
-  })
-
   it('renders correct INFO pill', () => {
     let wrapper = wrap({ item, primaryInfoPill: true })
     expect(wrapper.find('infoPill').prop('primary')).toBe(true)

+ 12 - 13
src/components/organisms/EditReplica/EditReplica.jsx

@@ -64,7 +64,6 @@ const Buttons = styled.div`
   flex-shrink: 0;
   justify-content: space-between;
 `
-const Empty = styled.div``
 
 type Props = {
   isOpen: boolean,
@@ -101,7 +100,7 @@ class EditReplica extends React.Component<Props, State> {
     }
 
     providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, 'replica').then(() => {
-      return providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type)
+      return providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type, undefined, true)
     }).then(() => {
       this.loadEnvDestinationOptions()
     })
@@ -153,7 +152,7 @@ class EditReplica extends React.Component<Props, State> {
     })
 
     if (envData) {
-      providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type, envData)
+      providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type, envData, true)
     }
   }
 
@@ -206,8 +205,9 @@ class EditReplica extends React.Component<Props, State> {
   }
 
   handleNetworkChange(sourceNic: Nic, targetNetwork: Network) {
+    let networkMap = this.state.selectedNetworks.filter(n => n.sourceNic.network_name !== sourceNic.network_name)
     this.setState({
-      selectedNetworks: [...this.state.selectedNetworks, { sourceNic, targetNetwork }],
+      selectedNetworks: [...networkMap, { sourceNic, targetNetwork }],
     })
   }
 
@@ -237,7 +237,7 @@ class EditReplica extends React.Component<Props, State> {
 
     if (networkMap) {
       Object.keys(networkMap).forEach(sourceNetworkName => {
-        let network = this.props.networks.find(n => n.name === networkMap[sourceNetworkName])
+        let network = this.props.networks.find(n => n.name === networkMap[sourceNetworkName] || n.id === networkMap[sourceNetworkName])
         if (!network) {
           return
         }
@@ -292,12 +292,12 @@ class EditReplica extends React.Component<Props, State> {
 
   renderDestinationOptions() {
     if (providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading) {
-      return this.renderLoading('Loading destination options ...')
+      return this.renderLoading('Loading target options ...')
     }
 
     return (
       <WizardOptions
-        wizardType="dest-edit"
+        wizardType="replica-dest-options-edit"
         getFieldValue={(f, d) => this.getFieldValue(f, d)}
         fields={providerStore.destinationSchema.filter(f => !f.readOnly)}
         hasStorageMap={this.hasStorageMap()}
@@ -312,10 +312,6 @@ class EditReplica extends React.Component<Props, State> {
   }
 
   renderStorageMapping() {
-    if (!this.hasStorageMap()) {
-      return <Empty>The destination endpoint does not have storage listing.</Empty>
-    }
-
     if (this.props.instancesDetailsLoading) {
       return this.renderLoading('Loading instances details ...')
     }
@@ -394,11 +390,14 @@ class EditReplica extends React.Component<Props, State> {
 
   render() {
     const navigationItems: NavigationItem[] = [
-      { value: 'dest_options', label: 'Destination Options' },
+      { value: 'dest_options', label: 'Target Options' },
       { value: 'network_mapping', label: 'Network Mapping' },
-      { value: 'storage_mapping', label: 'Storage Mapping' },
     ]
 
+    if (this.hasStorageMap()) {
+      navigationItems.push({ value: 'storage_mapping', label: 'Storage Mapping' })
+    }
+
     return (
       <Modal
         isOpen={this.props.isOpen}

+ 1 - 1
src/components/organisms/WizardNetworks/WizardNetworks.jsx

@@ -77,7 +77,7 @@ const NetworkSubtitle = styled.div`
   margin-top: 1px;
 `
 const ArrowImage = styled.div`
-  ${StyleProps.exactWidth('32px')}
+  min-width: 32px;
   ${StyleProps.exactHeight('16px')}
   background: url('${arrowImage}') center no-repeat;
   flex-grow: 1;

+ 7 - 5
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -80,7 +80,7 @@ type Props = {
   onChange: (field: Field, value: any) => void,
   useAdvancedOptions?: boolean,
   hasStorageMap: boolean,
-  storageBackends: StorageBackend[],
+  storageBackends?: StorageBackend[],
   onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
   wizardType: string,
   loading?: boolean,
@@ -115,9 +115,11 @@ class WizardOptions extends React.Component<Props> {
   }
 
   getDefaultFieldsSchema() {
-    let fieldsSchema = [
-      { name: 'description', type: 'string' },
-    ]
+    let fieldsSchema = []
+
+    if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica') {
+      fieldsSchema.push({ name: 'description', type: 'string' })
+    }
 
     if (this.props.wizardType === 'migration') {
       fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'strict-boolean', default: false })
@@ -142,7 +144,7 @@ class WizardOptions extends React.Component<Props> {
       }
     }
 
-    if (this.props.hasStorageMap && this.props.useAdvancedOptions && this.props.storageBackends.length > 0) {
+    if (this.props.hasStorageMap && this.props.useAdvancedOptions && this.props.storageBackends && this.props.storageBackends.length > 0) {
       fieldsSchema.push({ name: 'default_storage', type: 'string', enum: this.props.storageBackends.map(s => s.name) })
     }
 

+ 5 - 5
src/components/organisms/WizardOptions/test.jsx

@@ -57,8 +57,8 @@ let fields = [
 
 describe('WizardOptions Component', () => {
   it('has description and required field in simple tab', () => {
-    let wrapper = wrap({ fields, selectedInstances: [] })
-    expect(wrapper.find('field-', true).length).toBe(2)
+    let wrapper = wrap({ fields, selectedInstances: [], wizardType: 'migration' })
+    expect(wrapper.find('field-', true).length).toBe(3)
     expect(wrapper.find('field-description').length).toBe(1)
     expect(wrapper.find('field-required_string_field').length).toBe(1)
   })
@@ -80,12 +80,12 @@ describe('WizardOptions Component', () => {
   })
 
   it('renders correct number of fields in advanced tab', () => {
-    let wrapper = wrap({ fields, selectedInstances: [], useAdvancedOptions: true })
-    expect(wrapper.find('field-', true).length).toBe(fields.length + 1)
+    let wrapper = wrap({ fields, selectedInstances: [], useAdvancedOptions: true, wizardType: 'migration' })
+    expect(wrapper.find('field-', true).length).toBe(fields.length + 2)
   })
 
   it('renders correct field info', () => {
-    let wrapper = wrap({ fields, selectedInstances: [], useAdvancedOptions: true })
+    let wrapper = wrap({ fields, selectedInstances: [], useAdvancedOptions: true, wizardType: 'migration' })
 
     expect(wrapper.find('field-description').prop('type')).toBe('string')
     expect(wrapper.find('field-required_string_field').prop('required')).toBe(true)

+ 24 - 9
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -151,7 +151,8 @@ type Props = {
   onInstancesReloadClick: () => void,
   onInstanceClick: (instance: Instance) => void,
   onInstancePageClick: (page: number) => void,
-  onOptionsChange: (field: Field, value: any) => void,
+  onDestOptionsChange: (field: Field, value: any) => void,
+  onSourceOptionsChange: (field: Field, value: any) => void,
   onNetworkChange: (nic: Nic, network: Network) => void,
   onStorageChange: (sourceStorage: Disk, targetStorage: StorageBackend, type: 'backend' | 'disk') => void,
   onAddScheduleClick: (schedule: ScheduleType) => void,
@@ -244,8 +245,8 @@ class WizardPageContent extends React.Component<Props, State> {
         return !this.props.wizardData.target
       case 'vms':
         return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
-      case 'options':
-        return !isOptionsPageValid(this.props.wizardData.options, this.props.providerStore.destinationSchema)
+      case 'dest-options':
+        return !isOptionsPageValid(this.props.wizardData.destOptions, this.props.providerStore.destinationSchema)
       case 'networks':
         return !this.isNetworksPageValid()
       default:
@@ -329,14 +330,27 @@ class WizardPageContent extends React.Component<Props, State> {
           />
         )
         break
-      case 'options':
+      case 'source-options':
+        body = (
+          <WizardOptions
+            loading={this.props.providerStore.sourceSchemaLoading}
+            fields={this.props.providerStore.sourceSchema}
+            onChange={this.props.onSourceOptionsChange}
+            data={this.props.wizardData.sourceOptions}
+            useAdvancedOptions
+            hasStorageMap={false}
+            wizardType={`${this.props.type}-source-options`}
+          />
+        )
+        break
+      case 'dest-options':
         body = (
           <WizardOptions
             loading={this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsLoading}
             selectedInstances={this.props.wizardData.selectedInstances}
             fields={this.props.providerStore.destinationSchema}
-            onChange={this.props.onOptionsChange}
-            data={this.props.wizardData.options}
+            onChange={this.props.onDestOptionsChange}
+            data={this.props.wizardData.destOptions}
             useAdvancedOptions={this.state.useAdvancedOptions}
             hasStorageMap={this.props.hasStorageMap}
             storageBackends={this.props.endpointStore.storageBackends}
@@ -363,7 +377,7 @@ class WizardPageContent extends React.Component<Props, State> {
             storageBackends={this.props.endpointStore.storageBackends}
             instancesDetails={this.props.instanceStore.instancesDetails}
             storageMap={this.props.storageMap}
-            defaultStorage={String(this.props.wizardData.options ? this.props.wizardData.options.default_storage : '')}
+            defaultStorage={String(this.props.wizardData.destOptions ? this.props.wizardData.destOptions.default_storage : '')}
             onChange={this.props.onStorageChange}
           />
         )
@@ -391,8 +405,8 @@ class WizardPageContent extends React.Component<Props, State> {
             instancesDetails={this.props.instanceStore.instancesDetails}
             defaultStorage={
               this.props.endpointStore.storageBackends.find(
-                s => this.props.wizardData.options ?
-                  s.name === this.props.wizardData.options.default_storage :
+                s => this.props.wizardData.destOptions ?
+                  s.name === this.props.wizardData.destOptions.default_storage :
                   false
               )
             }
@@ -447,6 +461,7 @@ class WizardPageContent extends React.Component<Props, State> {
             selected={this.props.page}
             wizardType={this.props.type}
             destinationProvider={this.props.wizardData.target ? this.props.wizardData.target.type : null}
+            sourceProvider={this.props.wizardData.source ? this.props.wizardData.source.type : null}
           />
         </Footer>
       </Wrapper>

+ 1 - 1
src/components/organisms/WizardStorage/WizardStorage.jsx

@@ -91,7 +91,7 @@ const StorageSubtitle = styled.div`
   margin-top: 1px;
 `
 const ArrowImage = styled.div`
-  ${StyleProps.exactWidth('32px')}
+  min-width: 32px;
   ${StyleProps.exactHeight('16px')}
   background: url('${arrowImage}') center no-repeat;
   flex-grow: 1;

+ 38 - 9
src/components/organisms/WizardSummary/WizardSummary.jsx

@@ -149,7 +149,7 @@ type Props = {
 @observer
 class WizardSummary extends React.Component<Props> {
   getDefaultOption(fieldName: string) {
-    if (this.props.data.options && this.props.data.options[fieldName] === false) {
+    if (this.props.data.destOptions && this.props.data.destOptions[fieldName] === false) {
       return false
     }
 
@@ -237,7 +237,36 @@ class WizardSummary extends React.Component<Props> {
     return value
   }
 
-  renderOptionsSection() {
+  renderSourceOptionsSection() {
+    let data = this.props.data
+    let type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
+
+    return (
+      <Section>
+        <SectionTitle>{type} Source Options</SectionTitle>
+        <OptionsList>
+          {data.sourceOptions ? Object.keys(data.sourceOptions).map(optionName => {
+            if (!data.sourceOptions || data.sourceOptions[optionName] == null) {
+              return null
+            }
+
+            return (
+              <Option key={optionName}>
+                <OptionLabel>
+                  {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
+                </OptionLabel>
+                <OptionValue>{
+                  this.renderOptionValue(data.sourceOptions && data.sourceOptions[optionName])
+                }</OptionValue>
+              </Option>
+            )
+          }) : null}
+        </OptionsList>
+      </Section>
+    )
+  }
+
+  renderTargetOptionsSection() {
     let data = this.props.data
     let type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
 
@@ -257,16 +286,16 @@ class WizardSummary extends React.Component<Props> {
 
     return (
       <Section>
-        <SectionTitle>{type} Options</SectionTitle>
+        <SectionTitle>{type} Target Options</SectionTitle>
         <OptionsList>
           {this.props.wizardType === 'replica' ? executeNowOption : null}
           {this.props.data.selectedInstances && this.props.data.selectedInstances.length > 1 ? separateVmOption : null}
-          {data.options ? Object.keys(data.options).map(optionName => {
+          {data.destOptions ? Object.keys(data.destOptions).map(optionName => {
             if (
               optionName === 'execute_now' ||
               optionName === 'separate_vm' ||
-              optionName === 'default_stoage' ||
-              !data.options || data.options[optionName] == null
+              optionName === 'default_storage' ||
+              !data.destOptions || data.destOptions[optionName] == null
             ) {
               return null
             }
@@ -277,8 +306,7 @@ class WizardSummary extends React.Component<Props> {
                   {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
                 </OptionLabel>
                 <OptionValue data-test-id={`wSummary-optionValue-${optionName}`}>{
-                  // $FlowIssue
-                  this.renderOptionValue(data.options[optionName])
+                  this.renderOptionValue(data.destOptions && data.destOptions[optionName])
                 }</OptionValue>
               </Option>
             )
@@ -432,7 +460,8 @@ class WizardSummary extends React.Component<Props> {
           {this.renderNetworksSection()}
         </Column>
         <Column>
-          {this.renderOptionsSection()}
+          {this.renderSourceOptionsSection()}
+          {this.renderTargetOptionsSection()}
           {this.renderStorageSection('backend')}
           {this.renderStorageSection('disk')}
           {this.renderScheduleSection()}

+ 1 - 1
src/components/organisms/WizardSummary/test.jsx

@@ -38,7 +38,7 @@ let schedules = [
 ]
 
 let data = {
-  options: {
+  destOptions: {
     description: 'A description',
     field_name: 'Field name value',
   },

+ 49 - 22
src/components/pages/WizardPage/WizardPage.jsx

@@ -80,7 +80,8 @@ class WizardPage extends React.Component<Props, State> {
   get pages() {
     return wizardConfig.pages
       .filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
-      .filter(p => !p.filter || (wizardStore.data.target && p.filter(wizardStore.data.target.type)))
+      .filter(p => !p.targetFilter || (wizardStore.data.target && p.targetFilter(wizardStore.data.target.type)))
+      .filter(p => !p.sourceFilter || (wizardStore.data.source && p.sourceFilter(wizardStore.data.source.type)))
   }
 
   componentWillMount() {
@@ -165,6 +166,16 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   handleTypeChange(isReplica: ?boolean) {
+    wizardStore.updateData({
+      target: null,
+      networks: null,
+      destOptions: null,
+      sourceOptions: null,
+      selectedInstances: null,
+      source: null,
+    })
+    wizardStore.clearStorageMap()
+    wizardStore.setPermalink(wizardStore.data)
     this.setState({ type: isReplica ? 'replica' : 'migration' })
   }
 
@@ -195,25 +206,29 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   handleSourceEndpointChange(source: ?EndpointType) {
-    wizardStore.updateData({ source, selectedInstances: null, networks: null })
+    wizardStore.updateData({ source, selectedInstances: null, networks: null, sourceOptions: null })
     wizardStore.clearStorageMap()
     wizardStore.setPermalink(wizardStore.data)
 
-    if (source) {
-      // Check if user has permission for this endpoint
-      endpointStore.getConnectionInfo(source).then(() => {
-        if (source) {
-          // Preload instances for 'vms' page
-          instanceStore.loadInstancesInChunks(source.id, this.instancesChunkSize)
-        }
-      }).catch(() => {
-        this.handleSourceEndpointChange(null)
-      })
+    if (!source) {
+      return
     }
+
+    // Check if user has permission for this endpoint
+    endpointStore.getConnectionInfo(source).then(() => {
+      if (source) {
+        // Preload instances for 'vms' page
+        instanceStore.loadInstancesInChunks(source.id, this.instancesChunkSize)
+      }
+    }).catch(() => {
+      this.handleSourceEndpointChange(null)
+    })
+
+    providerStore.loadSourceSchema(source.type, this.state.type === 'replica')
   }
 
   handleTargetEndpointChange(target: EndpointType) {
-    wizardStore.updateData({ target, networks: null, options: null })
+    wizardStore.updateData({ target, networks: null, destOptions: null })
     wizardStore.clearStorageMap()
     wizardStore.setPermalink(wizardStore.data)
     // Preload destination options schema
@@ -273,10 +288,10 @@ class WizardPage extends React.Component<Props, State> {
     instanceStore.updateChunkSize(chunkSize)
   }
 
-  handleOptionsChange(field: Field, value: any) {
+  handleDestOptionsChange(field: Field, value: any) {
     wizardStore.updateData({ networks: null })
     wizardStore.clearStorageMap()
-    wizardStore.updateOptions({ field, value })
+    wizardStore.updateDestOptions({ field, value })
     // If the field is a string and doesn't have an enum property,
     // we can't call destination options on "change" since too many calls will be made,
     // it also means a potential problem with the server not populating the "enum" prop.
@@ -286,6 +301,11 @@ class WizardPage extends React.Component<Props, State> {
     wizardStore.setPermalink(wizardStore.data)
   }
 
+  handleSourceOptionsChange(field: Field, value: any) {
+    wizardStore.updateSourceOptions({ field, value })
+    wizardStore.setPermalink(wizardStore.data)
+  }
+
   handleNetworkChange(sourceNic: Nic, targetNetwork: Network) {
     wizardStore.updateNetworks({ sourceNic, targetNetwork })
     wizardStore.setPermalink(wizardStore.data)
@@ -320,7 +340,7 @@ class WizardPage extends React.Component<Props, State> {
     let envData = getFieldChangeDestOptions({
       provider: wizardStore.data.target && wizardStore.data.target.type,
       destSchema: providerStore.destinationSchema,
-      data: wizardStore.data.options,
+      data: wizardStore.data.destOptions,
       field,
     })
 
@@ -336,7 +356,13 @@ class WizardPage extends React.Component<Props, State> {
         endpointStore.getEndpoints()
         // Preload instances if data is set from 'Permalink'
         let source = wizardStore.data.source
-        if (instanceStore.instances.length === 0 && source) {
+        if (!source) {
+          return
+        }
+
+        providerStore.loadSourceSchema(source.type, this.state.type === 'replica')
+
+        if (instanceStore.instances.length === 0) {
           // Check if user has permission for this endpoint
           endpointStore.getConnectionInfo(source).then(() => {
             // Preload instances for 'vms' page
@@ -372,7 +398,7 @@ class WizardPage extends React.Component<Props, State> {
         }
         if (wizardStore.data.target) {
           let id = wizardStore.data.target.id
-          networkStore.loadNetworks(id, wizardStore.data.options)
+          networkStore.loadNetworks(id, wizardStore.data.destOptions)
         }
         break
       default:
@@ -413,8 +439,8 @@ class WizardPage extends React.Component<Props, State> {
     let data = wizardStore.data
     let separateVms = true
 
-    if (data.options && data.options.separate_vm != null) {
-      separateVms = data.options.separate_vm
+    if (data.destOptions && data.destOptions.separate_vm != null) {
+      separateVms = data.destOptions.separate_vm
     }
 
     if (data.selectedInstances && data.selectedInstances.length === 1) {
@@ -442,7 +468,7 @@ class WizardPage extends React.Component<Props, State> {
   }
 
   executeCreatedReplica(replica: MainItem) {
-    let options = wizardStore.data.options
+    let options = wizardStore.data.destOptions
     let executeNow = true
     if (options && options.execute_now != null) {
       executeNow = options.execute_now
@@ -492,7 +518,8 @@ class WizardPage extends React.Component<Props, State> {
             onInstanceClick={instance => { this.handleInstanceClick(instance) }}
             onInstancePageClick={page => { this.handleInstancePageClick(page) }}
             onInstanceChunkSizeUpdate={chunkSize => { this.handleInstanceChunkSizeUpdate(chunkSize) }}
-            onOptionsChange={(field, value) => { this.handleOptionsChange(field, value) }}
+            onDestOptionsChange={(field, value) => { this.handleDestOptionsChange(field, value) }}
+            onSourceOptionsChange={(field, value) => { this.handleSourceOptionsChange(field, value) }}
             onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
             onStorageChange={(source, target, type) => { this.handleStorageChange(source, target, type) }}
             onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}

+ 14 - 2
src/config.js

@@ -51,6 +51,11 @@ export const navigationMenu = [
 
 export const requestPollTimeout = 5000
 
+// https://github.com/cloudbase/coriolis/blob/master/coriolis/constants.py
+// PROVIDER_TYPE_IMPORT = 1 // migration target schema
+// PROVIDER_TYPE_EXPORT = 2 // migration source schema
+// PROVIDER_TYPE_REPLICA_IMPORT = 4 // replica target schema
+// PROVIDER_TYPE_REPLICA_EXPORT = 8 // replica source schema
 export const providerTypes = {
   TARGET_MIGRATION: 1,
   SOURCE_MIGRATION: 2,
@@ -82,20 +87,27 @@ export const executionOptions = [
 ]
 
 export const storageProviders = ['openstack', 'azure']
+export const sourceOptionsProviders = ['aws']
 
 export const wizardConfig = {
   pages: [
     { id: 'type', title: 'New', breadcrumb: 'Type' },
     { id: 'source', title: 'Select your source cloud', breadcrumb: 'Source Cloud' },
+    {
+      id: 'source-options',
+      title: 'Source options',
+      breadcrumb: 'Source Options',
+      sourceFilter: (p: string) => sourceOptionsProviders.find(s => s === p),
+    },
     { id: 'vms', title: 'Select instances', breadcrumb: 'Select VMs' },
     { id: 'target', title: 'Select your target cloud', breadcrumb: 'Target Cloud' },
-    { id: 'options', title: 'Options', breadcrumb: 'Options' },
+    { id: 'dest-options', title: 'Target options', breadcrumb: 'Target Options' },
     { id: 'networks', title: 'Networks', breadcrumb: 'Networks' },
     {
       id: 'storage',
       title: 'Storage Mapping',
       breadcrumb: 'Storage',
-      filter: (p: string) => storageProviders.find(s => s === p),
+      targetFilter: (p: string) => storageProviders.find(s => s === p),
     },
     { id: 'schedule', title: 'Schedule', breadcrumb: 'Schedule', excludeFrom: 'migration' },
     { id: 'summary', title: 'Summary', breadcrumb: 'Summary' },

+ 7 - 6
src/plugins/endpoint/default/OptionsSchemaPlugin.js

@@ -94,13 +94,14 @@ export const defaultGetMigrationImageMap = (options: ?{ [string]: mixed }) => {
   let env = {}
   if (options) {
     migrationImageOsTypes.forEach(os => {
-      if (options && options[`${os}_os_image`]) {
-        if (!env.migr_image_map) {
-          env.migr_image_map = {}
-        }
-
-        env.migr_image_map[os] = options[`${os}_os_image`]
+      if (!options || !options[`${os}_os_image`]) {
+        return
       }
+      if (!env.migr_image_map) {
+        env.migr_image_map = {}
+      }
+
+      env.migr_image_map[os] = options[`${os}_os_image`]
     })
   }
 

+ 10 - 0
src/sources/ProviderSource.js

@@ -45,6 +45,16 @@ class ProviderSource {
     })
   }
 
+  static loadSourceSchema(providerName: string, isReplica: boolean): Promise<Field[]> {
+    let schemaTypeInt = isReplica ? providerTypes.SOURCE_REPLICA : providerTypes.SOURCE_MIGRATION
+
+    return Api.get(`${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`).then(response => {
+      let schema = { oneOf: [response.data.schemas.source_environment_schema] }
+      let fields = SchemaParser.optionsSchemaToFields(providerName, schema)
+      return fields
+    })
+  }
+
   static getDestinationOptions(endpointId: string, envData: ?{ [string]: mixed }): Promise<DestinationOption[]> {
     let envString = ''
     if (envData) {

+ 9 - 5
src/sources/WizardSource.js

@@ -30,15 +30,19 @@ class WizardSource {
     payload[type] = {
       origin_endpoint_id: data.source ? data.source.id : 'null',
       destination_endpoint_id: data.target ? data.target.id : 'null',
-      destination_environment: parser.getDestinationEnv(data.options),
+      destination_environment: parser.getDestinationEnv(data.destOptions),
       network_map: parser.getNetworkMap(data),
       instances: data.selectedInstances ? data.selectedInstances.map(i => i.instance_name) : 'null',
-      storage_mappings: parser.getStorageMap(data.options, storageMap),
-      notes: data.options ? data.options.description || '' : '',
+      storage_mappings: parser.getStorageMap(data.destOptions, storageMap),
+      notes: data.destOptions ? data.destOptions.description || '' : '',
     }
 
-    if (data.options && data.options.skip_os_morphing != null) {
-      payload[type].skip_os_morphing = data.options.skip_os_morphing
+    if (data.destOptions && data.destOptions.skip_os_morphing != null) {
+      payload[type].skip_os_morphing = data.destOptions.skip_os_morphing
+    }
+
+    if (data.sourceOptions) {
+      payload[type].source_environment = parser.getDestinationEnv(data.sourceOptions)
     }
 
     return Api.send({

+ 36 - 1
src/stores/ProviderStore.js

@@ -79,6 +79,8 @@ class ProviderStore {
   @observable destinationSchemaLoading: boolean = false
   @observable destinationOptions: DestinationOption[] = []
   @observable destinationOptionsLoading: boolean = false
+  @observable sourceSchema: Field[] = []
+  @observable sourceSchemaLoading: boolean = false
 
   lastDestinationSchemaType: string = ''
 
@@ -121,12 +123,37 @@ class ProviderStore {
     })
   }
 
-  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }): Promise<DestinationOption[]> {
+  @action loadSourceSchema(providerName: string, isReplica: boolean): Promise<void> {
+    this.sourceSchemaLoading = true
+
+    return ProviderSource.loadSourceSchema(providerName, isReplica).then((fields: Field[]) => {
+      this.sourceSchemaLoading = false
+      this.sourceSchema = fields
+    }).catch(() => { this.sourceSchemaLoading = false })
+  }
+
+  cache: { key: string, data: DestinationOption[] }[] = []
+
+  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }, useCache?: boolean): Promise<DestinationOption[]> {
     let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p === 'string' ? p === provider : p.name === provider)
     if (!providerWithExtraOptions) {
       return Promise.resolve([])
     }
 
+    if (useCache) {
+      let key = `${endpointId}-${provider}-${JSON.stringify(envData)}`
+      let cacheItem = this.cache.find(c => c.key === key)
+      if (cacheItem) {
+        this.destinationSchema.forEach(field => {
+          const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
+          parser.fillFieldValues(field, cacheItem.data)
+        })
+        this.destinationSchema = [...this.destinationSchema]
+        this.destinationOptions = cacheItem.data
+        return Promise.resolve(cacheItem.data)
+      }
+    }
+
     this.destinationOptionsLoading = true
     this.destinationOptions = []
     let destOptions = []
@@ -139,6 +166,14 @@ class ProviderStore {
       this.destinationOptions = options
       destOptions = options
       this.destinationOptionsLoading = false
+
+      if (useCache) {
+        let key = `${endpointId}-${provider}-${JSON.stringify(envData)}`
+        if (this.cache.length > 20) {
+          this.cache.splice(0)
+        }
+        this.cache.push({ key, data: options })
+      }
     }).catch(err => {
       console.error(err)
       if (envData) {

+ 23 - 16
src/stores/WizardStore.js

@@ -26,6 +26,21 @@ import type { Schedule } from '../types/Schedule'
 import { wizardConfig } from '../config'
 import Source from '../sources/WizardSource'
 
+const updateOptions = (oldOptions: ?{ [string]: mixed }, data: { field: Field, value: any }) => {
+  let options = { ...oldOptions }
+  if (data.field.type === 'array') {
+    let oldValues: string[] = options[data.field.name] || []
+    if (oldValues.find(v => v === data.value)) {
+      options[data.field.name] = oldValues.filter(v => v !== data.value)
+    } else {
+      options[data.field.name] = [...oldValues, data.value]
+    }
+  } else {
+    options[data.field.name] = data.value
+  }
+  return options
+}
+
 class WizardStore {
   @observable data: WizardData = {}
   @observable schedules: Schedule[] = []
@@ -64,22 +79,14 @@ class WizardStore {
     this.currentPage = page
   }
 
-  @action updateOptions(data: { field: Field, value: any }) {
-    this.data.options = {
-      ...this.data.options,
-    }
-    if (data.field.type === 'array') {
-      let oldValues: string[] = this.data.options[data.field.name] || []
-      if (oldValues.find(v => v === data.value)) {
-        // $FlowIssue
-        this.data.options[data.field.name] = oldValues.filter(v => v !== data.value)
-      } else {
-        // $FlowIssue
-        this.data.options[data.field.name] = [...oldValues, data.value]
-      }
-    } else {
-      this.data.options[data.field.name] = data.value
-    }
+  @action updateSourceOptions(data: { field: Field, value: any }) {
+    this.data = { ...this.data }
+    this.data.sourceOptions = updateOptions(this.data.sourceOptions, data)
+  }
+
+  @action updateDestOptions(data: { field: Field, value: any }) {
+    this.data = { ...this.data }
+    this.data.destOptions = updateOptions(this.data.destOptions, data)
   }
 
   @action updateNetworks(network: NetworkMap) {

+ 3 - 2
src/types/WizardData.js

@@ -19,11 +19,12 @@ import type { NetworkMap } from './Network'
 import type { Endpoint } from './Endpoint'
 
 export type WizardData = {
-  options?: ?{ [string]: mixed },
+  destOptions?: ?{ [string]: mixed },
+  sourceOptions?: ?{ [string]: mixed },
   selectedInstances?: ?Instance[],
   networks?: ?NetworkMap[],
   source?: ?Endpoint,
-  target?: Endpoint,
+  target?: ?Endpoint,
 }
 
 export type WizardPage = {