Просмотр исходного кода

Merge pull request #423 from smiclea/improve-source-options

Improve Replica / Migration source options support
Dorin Paslaru 6 лет назад
Родитель
Сommit
392dd9912e

+ 7 - 7
config.js

@@ -30,7 +30,7 @@ const conf: Config = {
   requestPollTimeout: 5000,
   requestPollTimeout: 5000,
 
 
   // The list of providers which offer source options
   // The list of providers which offer source options
-  sourceOptionsProviders: ['aws'],
+  sourceOptionsProviders: ['aws', 'azure', 'openstack'],
 
 
   // - Specifies the `limit` for each provider when listing all its VMs for pagination.
   // - Specifies the `limit` for each provider when listing all its VMs for pagination.
   // - If the provider is not in this list, the 'default' value will be used.
   // - If the provider is not in this list, the 'default' value will be used.
@@ -38,17 +38,17 @@ const conf: Config = {
   // - `Infinity` value means no `limit` will be used, i.e. all VMs will be listed.
   // - `Infinity` value means no `limit` will be used, i.e. all VMs will be listed.
   instancesListBackgroundLoading: { default: 10, ovm: Infinity, 'hyper-v': Infinity },
   instancesListBackgroundLoading: { default: 10, ovm: Infinity, 'hyper-v': Infinity },
 
 
-  // The providers for which an extra `source` or `destination options` call can be made with a set of field values
-  providersWithEnvOptions: [
+  // The providers for which an extra `source options` or `destination options` call can be made with a set of field values
+  extraOptionsApiCalls: [
     {
     {
       name: 'azure',
       name: 'azure',
-      type: 'destination',
-      envRequiredFields: ['location', 'resource_group'],
+      types: ['source', 'destination'],
+      requiredFields: ['location', 'resource_group'],
     },
     },
     {
     {
       name: 'oci',
       name: 'oci',
-      type: 'destination',
-      envRequiredFields: ['compartment', 'availability_domain', 'vcn_compartment'],
+      types: ['destination'],
+      requiredFields: ['compartment', 'availability_domain', 'vcn_compartment'],
     },
     },
   ],
   ],
 
 

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

@@ -48,6 +48,7 @@ const Required = styled.div`
   right: -16px;
   right: -16px;
   top: 12px;
   top: 12px;
   background: url('${requiredImage}') center no-repeat;
   background: url('${requiredImage}') center no-repeat;
+  ${props => props.disabledLoading ? StyleProps.animations.disabledLoading : ''}
 `
 `
 const List = styled.div`
 const List = styled.div`
   position: absolute;
   position: absolute;
@@ -483,7 +484,7 @@ class Dropdown extends React.Component<Props, State> {
           value={buttonValue()}
           value={buttonValue()}
           onClick={() => this.handleButtonClick()}
           onClick={() => this.handleButtonClick()}
         />
         />
-        {this.props.required ? <Required /> : null}
+        {this.props.required ? <Required disabledLoading={this.props.disabledLoading} /> : null}
         {this.renderList()}
         {this.renderList()}
       </Wrapper>
       </Wrapper>
     )
     )

+ 4 - 3
src/components/molecules/FieldInput/FieldInput.jsx

@@ -181,7 +181,7 @@ class FieldInput extends React.Component<Props> {
         placeholder={LabelDictionary.get(this.props.name)}
         placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
         disabled={this.props.disabled}
         disabledLoading={this.props.disabledLoading}
         disabledLoading={this.props.disabledLoading}
-        required={this.props.required}
+        required={this.props.layout === 'page' ? false : this.props.required}
       />
       />
     )
     )
   }
   }
@@ -208,6 +208,7 @@ class FieldInput extends React.Component<Props> {
     let selectedItem = items.find(i => i.value === this.props.value)
     let selectedItem = items.find(i => i.value === this.props.value)
     let commonProps = {
     let commonProps = {
       width: this.props.width,
       width: this.props.width,
+      required: this.props.layout === 'page' ? false : this.props.required,
       selectedItem,
       selectedItem,
       items,
       items,
       disabledLoading: this.props.disabledLoading,
       disabledLoading: this.props.disabledLoading,
@@ -283,7 +284,7 @@ class FieldInput extends React.Component<Props> {
         disabled={this.props.disabled}
         disabled={this.props.disabled}
         disabledLoading={this.props.disabledLoading}
         disabledLoading={this.props.disabledLoading}
         highlight={this.props.highlight}
         highlight={this.props.highlight}
-        required={this.props.required}
+        required={this.props.layout === 'page' ? false : this.props.required}
       />
       />
     )
     )
   }
   }
@@ -324,7 +325,7 @@ class FieldInput extends React.Component<Props> {
         highlight={this.props.highlight}
         highlight={this.props.highlight}
         disabled={this.props.disabled}
         disabled={this.props.disabled}
         disabledLoading={this.props.disabledLoading}
         disabledLoading={this.props.disabledLoading}
-        required={this.props.required}
+        required={this.props.layout === 'page' ? false : this.props.required}
       />
       />
     )
     )
   }
   }

+ 6 - 6
src/components/molecules/MainDetailsTable/MainDetailsTable.jsx

@@ -262,7 +262,7 @@ class MainDetailsTable extends React.Component<Props, State> {
       }
       }
 
 
       rows.push(this.renderRow(
       rows.push(this.renderRow(
-        `${instance.instance_name}-${sourceName}-${destinationName}`,
+        `${instance.instance_name || instance.name}-${sourceName}-${destinationName}`,
         'storage',
         'storage',
         sourceName,
         sourceName,
         destinationName,
         destinationName,
@@ -332,7 +332,7 @@ class MainDetailsTable extends React.Component<Props, State> {
         }
         }
 
 
         rows.push(this.renderRow(
         rows.push(this.renderRow(
-          `${instance.instance_name}-${nic.network_name}`,
+          `${instance.instance_name || instance.name}-${nic.network_name}`,
           'network',
           'network',
           nic.mac_address,
           nic.mac_address,
           destinationNetworkName,
           destinationNetworkName,
@@ -358,16 +358,16 @@ class MainDetailsTable extends React.Component<Props, State> {
     let destinationName: string = ''
     let destinationName: string = ''
     let transferResult = this.getTransferResult(instance)
     let transferResult = this.getTransferResult(instance)
     if (transferResult) {
     if (transferResult) {
-      destinationName = transferResult.instance_name
+      destinationName = transferResult.instance_name || transferResult.name
       destinationBody = getBody(transferResult)
       destinationBody = getBody(transferResult)
     } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
     } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
       destinationName = 'Waiting for migration to finish'
       destinationName = 'Waiting for migration to finish'
     }
     }
-
+    let instanceName = instance.instance_name || instance.name
     return this.renderRow(
     return this.renderRow(
-      instance.instance_name,
+      instanceName,
       'instance',
       'instance',
-      instance.instance_name,
+      instanceName,
       destinationName,
       destinationName,
       sourceBody,
       sourceBody,
       destinationBody
       destinationBody

+ 45 - 45
src/components/organisms/EditReplica/EditReplica.jsx

@@ -44,7 +44,6 @@ import configLoader from '../../../utils/Config'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 
 
 const PanelContent = styled.div`
 const PanelContent = styled.div`
-  padding: 32px;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   justify-content: space-between;
   justify-content: space-between;
@@ -62,7 +61,7 @@ const LoadingText = styled.div`
   margin-top: 32px;
   margin-top: 32px;
 `
 `
 const Buttons = styled.div`
 const Buttons = styled.div`
-  margin-top: 32px;
+  padding: 32px;
   display: flex;
   display: flex;
   flex-shrink: 0;
   flex-shrink: 0;
   justify-content: space-between;
   justify-content: space-between;
@@ -117,17 +116,15 @@ class EditReplica extends React.Component<Props, State> {
       endpointStore.loadStorage(this.props.destinationEndpoint.id, {})
       endpointStore.loadStorage(this.props.destinationEndpoint.id, {})
     }
     }
 
 
-    this.loadDestinationOptions(useCache)
-
-    if (!this.hasSourceOptions()) {
-      return
+    let loadAllOptions = async (type: 'source' | 'destination') => {
+      let endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
+      await this.loadOptions(endpoint, type, useCache)
+      this.loadExtraOptions(null, type)
     }
     }
-    this.loadOptions(this.props.sourceEndpoint, 'source', useCache)
-  }
-
-  async loadDestinationOptions(useCache: boolean) {
-    await this.loadOptions(this.props.destinationEndpoint, 'destination', useCache)
-    this.loadEnvDestinationOptions()
+    if (this.hasSourceOptions()) {
+      loadAllOptions('source')
+    }
+    loadAllOptions('destination')
   }
   }
 
 
   async loadOptions(endpoint: Endpoint, optionsType: 'source' | 'destination', useCache: boolean) {
   async loadOptions(endpoint: Endpoint, optionsType: 'source' | 'destination', useCache: boolean) {
@@ -145,6 +142,34 @@ class EditReplica extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
+  loadExtraOptions(field?: ?Field, type: 'source' | 'destination') {
+    let endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
+    let env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
+    let stateEnv = type === 'source' ? this.state.sourceData : this.state.destinationData
+
+    let envData = getFieldChangeOptions({
+      providerName: endpoint.type,
+      schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
+      data: {
+        ...this.parseReplicaData(env),
+        ...stateEnv,
+      },
+      field,
+      type,
+    })
+
+    if (!envData) {
+      return
+    }
+    providerStore.getOptionsValues({
+      optionsType: type,
+      endpointId: endpoint.id,
+      providerName: endpoint.type,
+      useCache: true,
+      envData,
+    })
+  }
+
   hasStorageMap(): boolean {
   hasStorageMap(): boolean {
     return providerStore.providers && providerStore.providers[this.props.destinationEndpoint.type] ?
     return providerStore.providers && providerStore.providers[this.props.destinationEndpoint.type] ?
       !!providerStore.providers[this.props.destinationEndpoint.type].types.find(t => t === providerTypes.STORAGE)
       !!providerStore.providers[this.props.destinationEndpoint.type].types.find(t => t === providerTypes.STORAGE)
@@ -187,29 +212,6 @@ class EditReplica extends React.Component<Props, State> {
     return data
     return data
   }
   }
 
 
-  loadEnvDestinationOptions(field?: Field) {
-    let envData = getFieldChangeOptions({
-      providerName: this.props.destinationEndpoint.type,
-      schema: providerStore.destinationSchema,
-      data: {
-        ...this.parseReplicaData(this.props.replica.destination_environment),
-        ...this.state.destinationData,
-      },
-      field,
-      type: 'destination',
-    })
-
-    if (envData) {
-      providerStore.getOptionsValues({
-        optionsType: 'destination',
-        endpointId: this.props.destinationEndpoint.id,
-        providerName: this.props.destinationEndpoint.type,
-        useCache: true,
-        envData,
-      })
-    }
-  }
-
   validateOptions(type: 'source' | 'destination') {
   validateOptions(type: 'source' | 'destination') {
     let env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
     let env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
     let data = type === 'source' ? this.state.sourceData : this.state.destinationData
     let data = type === 'source' ? this.state.sourceData : this.state.destinationData
@@ -244,18 +246,16 @@ class EditReplica extends React.Component<Props, State> {
       data[field.name] = value
       data[field.name] = value
     }
     }
 
 
+    let handleStateUpdate = () => {
+      if (field.type !== 'string' || field.enum) {
+        this.loadExtraOptions(field, type)
+      }
+      this.validateOptions(type)
+    }
     if (type === 'source') {
     if (type === 'source') {
-      this.setState({ sourceData: data }, () => {
-        this.validateOptions('source')
-      })
+      this.setState({ sourceData: data }, () => { handleStateUpdate() })
     } else {
     } else {
-      this.setState({ destinationData: data }, () => {
-        if (field.type !== 'string' || field.enum) {
-          this.loadEnvDestinationOptions(field)
-        }
-
-        this.validateOptions('destination')
-      })
+      this.setState({ destinationData: data }, () => { handleStateUpdate() })
     }
     }
   }
   }
 
 

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

@@ -155,7 +155,7 @@ class MainDetails extends React.Component<Props> {
         instanceDet.devices && instanceDet.devices.nics && instanceDet.devices.nics.find &&
         instanceDet.devices && instanceDet.devices.nics && instanceDet.devices.nics.find &&
         instanceDet.devices.nics.find(n => n.network_name === networkId)
         instanceDet.devices.nics.find(n => n.network_name === networkId)
       ) {
       ) {
-        vms.push(instanceDet.instance_name)
+        vms.push(instanceDet.instance_name || instanceDet.name)
       }
       }
     })
     })
 
 

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

@@ -314,7 +314,7 @@ class WizardInstances extends React.Component<Props, State> {
               <CheckboxStyled checked={selected} onChange={() => { }} />
               <CheckboxStyled checked={selected} onChange={() => { }} />
               <InstanceContent data-test-id="wInstances-instanceItem">
               <InstanceContent data-test-id="wInstances-instanceItem">
                 <Image />
                 <Image />
-                <Label data-test-id="wInstances-itemName">{instance.instance_name}</Label>
+                <Label data-test-id="wInstances-itemName">{instance.instance_name || instance.name}</Label>
                 <Details data-test-id="wInstances-itemDetails">{`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM${flavorName}`}</Details>
                 <Details data-test-id="wInstances-itemDetails">{`${instance.num_cpu} vCPU | ${instance.memory_mb} MB RAM${flavorName}`}</Details>
               </InstanceContent>
               </InstanceContent>
             </Instance>
             </Instance>

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

@@ -240,7 +240,7 @@ class WizardNetworks extends React.Component<Props> {
               return true
               return true
             }
             }
             return false
             return false
-          }).map(i => i.instance_name)
+          }).map(i => i.instance_name || i.name)
           let selectedNetwork = this.props.selectedNetworks && this.props.selectedNetworks.find(n => n.sourceNic.network_name === nic.network_name)
           let selectedNetwork = this.props.selectedNetworks && this.props.selectedNetworks.find(n => n.sourceNic.network_name === nic.network_name)
           return (
           return (
             <Nic key={nic.id} data-test-id="networkItem">
             <Nic key={nic.id} data-test-id="networkItem">

+ 3 - 2
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -41,6 +41,7 @@ const Options = styled.div`
   min-height: 0;
   min-height: 0;
 `
 `
 const Fields = styled.div`
 const Fields = styled.div`
+  ${props => props.padding ? `padding: ${props.padding}px;` : ''}
   display: flex;
   display: flex;
   overflow: auto;
   overflow: auto;
   justify-content: space-between;
   justify-content: space-between;
@@ -236,7 +237,7 @@ class WizardOptions extends React.Component<Props> {
 
 
     if (fields.length * 96 < availableHeight) {
     if (fields.length * 96 < availableHeight) {
       return (
       return (
-        <Fields>
+        <Fields padding={this.props.layout === 'page' ? null : 32}>
           <OneColumn style={this.props.oneColumnStyle}>
           <OneColumn style={this.props.oneColumnStyle}>
             {fields.map(f => f.component)}
             {fields.map(f => f.component)}
           </OneColumn>
           </OneColumn>
@@ -245,7 +246,7 @@ class WizardOptions extends React.Component<Props> {
     }
     }
 
 
     return (
     return (
-      <Fields innerRef={this.props.onScrollableRef}>
+      <Fields innerRef={this.props.onScrollableRef} padding={this.props.layout === 'page' ? null : 32}>
         <Column left style={this.props.columnStyle}>
         <Column left style={this.props.columnStyle}>
           {fields.map(f => f.column === 'left' && f.component)}
           {fields.map(f => f.column === 'left' && f.component)}
         </Column>
         </Column>

+ 2 - 0
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -246,6 +246,8 @@ class WizardPageContent extends React.Component<Props, State> {
         return !this.props.wizardData.target
         return !this.props.wizardData.target
       case 'vms':
       case 'vms':
         return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
         return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
+      case 'source-options':
+        return !isOptionsPageValid(this.props.wizardData.sourceOptions, this.props.providerStore.sourceSchema)
       case 'dest-options':
       case 'dest-options':
         return !isOptionsPageValid(this.props.wizardData.destOptions, this.props.providerStore.destinationSchema)
         return !isOptionsPageValid(this.props.wizardData.destOptions, this.props.providerStore.destinationSchema)
       case 'networks':
       case 'networks':

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

@@ -182,7 +182,7 @@ class WizardStorage extends React.Component<Props> {
                 return true
                 return true
               }
               }
               return false
               return false
-            }).map(i => i.instance_name)
+            }).map(i => i.instance_name || i.name)
             let selectedItem = storageMap && storageMap.find(s => s.type === type && String(s.source[diskFieldName]) === String(disk[diskFieldName]))
             let selectedItem = storageMap && storageMap.find(s => s.type === type && String(s.source[diskFieldName]) === String(disk[diskFieldName]))
             selectedItem = selectedItem ? selectedItem.target : null
             selectedItem = selectedItem ? selectedItem.target : null
             return (
             return (

+ 13 - 4
src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.jsx

@@ -398,10 +398,19 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   loadInstancesDetails() {
   loadInstancesDetails() {
-    let selectedVms = this.getLocalData().selectedVms
-    let instances = instanceStore.instances.filter(i => selectedVms.find(m => i.id === m))
+    let localData = this.getLocalData()
+    let selectedVms = localData.selectedVms
+    let instancesInfo = instanceStore.instances.filter(i => selectedVms.find(m => i.id === m))
     instanceStore.clearInstancesDetails()
     instanceStore.clearInstancesDetails()
-    instanceStore.loadInstancesDetails(this.getSourceEndpointId(), instances, true)
+    instanceStore.loadInstancesDetails({
+      endpointId: this.getSourceEndpointId(),
+      instancesInfo,
+      useLocalStorage: true,
+      env: {
+        location: localData.locationName,
+        resource_group: localData.resourceGroupName,
+      },
+    })
   }
   }
 
 
   handleMigrationExecute(fieldValues: { [string]: any }) {
   handleMigrationExecute(fieldValues: { [string]: any }) {
@@ -413,7 +422,7 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
       let vm = selectedVms.find(m => i.id === m)
       let vm = selectedVms.find(m => i.id === m)
       let selectedVmSize = localData.selectedVmSizes[i.id]
       let selectedVmSize = localData.selectedVmSizes[i.id]
       if (vm && azureStore.vmSizes.find(s => s === selectedVmSize)) {
       if (vm && azureStore.vmSizes.find(s => s === selectedVmSize)) {
-        vmSizes[i.instance_name] = selectedVmSize
+        vmSizes[i.instance_name || i.name] = selectedVmSize
       }
       }
     })
     })
 
 

+ 7 - 5
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.jsx

@@ -96,12 +96,14 @@ class MigrationDetailsPage extends React.Component<Props, State> {
       quietError: true,
       quietError: true,
       useLocalStorage: cache,
       useLocalStorage: cache,
     })
     })
-    instanceStore.loadInstancesDetails(
-      details.origin_endpoint_id,
+    instanceStore.loadInstancesDetails({
+      endpointId: details.origin_endpoint_id,
       // $FlowIgnore
       // $FlowIgnore
-      details.instances.map(n => { return { instance_name: n } }),
-      cache, false
-    )
+      instancesInfo: details.instances.map(n => ({ instance_name: n })),
+      useLocalStorage: cache,
+      quietError: false,
+      env: details.source_environment,
+    })
   }
   }
 
 
   handleUserItemClick(item: { value: string }) {
   handleUserItemClick(item: { value: string }) {

+ 7 - 5
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -136,12 +136,14 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       quietError: true,
       quietError: true,
       useLocalStorage: cache,
       useLocalStorage: cache,
     })
     })
-    instanceStore.loadInstancesDetails(
-      details.origin_endpoint_id,
+    instanceStore.loadInstancesDetails({
+      endpointId: details.origin_endpoint_id,
       // $FlowIgnore
       // $FlowIgnore
-      details.instances.map(n => { return { instance_name: n } }),
-      cache, false
-    )
+      instancesInfo: details.instances.map(n => ({ instance_name: n })),
+      useLocalStorage: cache,
+      quietError: false,
+      env: details.source_environment,
+    })
   }
   }
 
 
   getLastExecution() {
   getLastExecution() {

+ 71 - 66
src/components/pages/WizardPage/WizardPage.jsx

@@ -224,16 +224,11 @@ class WizardPage extends React.Component<Props, State> {
       // Check if user has permission for this endpoint
       // Check if user has permission for this endpoint
       try {
       try {
         await endpointStore.getConnectionInfo(source)
         await endpointStore.getConnectionInfo(source)
-        if (source) {
-          // Preload instances for 'vms' page
-          instanceStore.loadInstancesInChunks(source, this.instancesPerPage)
-        }
       } catch (err) {
       } catch (err) {
         this.handleSourceEndpointChange(null)
         this.handleSourceEndpointChange(null)
       }
       }
     }
     }
     getConnectionInfo()
     getConnectionInfo()
-
     if (!source) {
     if (!source) {
       return
       return
     }
     }
@@ -298,7 +293,7 @@ class WizardPage extends React.Component<Props, State> {
 
 
   handleInstancesReloadClick() {
   handleInstancesReloadClick() {
     if (wizardStore.data.source) {
     if (wizardStore.data.source) {
-      instanceStore.reloadInstances(wizardStore.data.source, this.instancesPerPage)
+      instanceStore.reloadInstances(wizardStore.data.source, this.instancesPerPage, wizardStore.data.sourceOptions)
     }
     }
   }
   }
 
 
@@ -323,13 +318,17 @@ class WizardPage extends React.Component<Props, State> {
     // Otherwise, the field has enum property, which there potentially other destination options for the new
     // Otherwise, the field has enum property, which there potentially other destination options for the new
     // chosen value from the enum
     // chosen value from the enum
     if (field.type !== 'string' || field.enum) {
     if (field.type !== 'string' || field.enum) {
-      this.loadEnvDestinationOptions(field)
+      this.loadExtraOptions(field, 'destination')
     }
     }
     wizardStore.setPermalink(wizardStore.data)
     wizardStore.setPermalink(wizardStore.data)
   }
   }
 
 
   handleSourceOptionsChange(field: Field, value: any) {
   handleSourceOptionsChange(field: Field, value: any) {
+    wizardStore.updateData({ selectedInstances: [] })
     wizardStore.updateSourceOptions({ field, value })
     wizardStore.updateSourceOptions({ field, value })
+    if (field.type !== 'string' || field.enum) {
+      this.loadExtraOptions(field, 'source')
+    }
     wizardStore.setPermalink(wizardStore.data)
     wizardStore.setPermalink(wizardStore.data)
   }
   }
 
 
@@ -362,27 +361,52 @@ class WizardPage extends React.Component<Props, State> {
     }
     }
   }
   }
 
 
-  loadEnvDestinationOptions(field?: Field) {
-    let providerName = wizardStore.data.target && wizardStore.data.target.type
+  loadExtraOptions(field?: Field, type: 'source' | 'destination') {
+    let endpoint = type === 'source' ? wizardStore.data.source : wizardStore.data.target
+    if (!endpoint) {
+      return
+    }
     let envData = getFieldChangeOptions({
     let envData = getFieldChangeOptions({
-      providerName: wizardStore.data.target && wizardStore.data.target.type,
-      schema: providerStore.destinationSchema,
-      data: wizardStore.data.destOptions,
+      providerName: endpoint.type,
+      schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
+      data: type === 'source' ? wizardStore.data.sourceOptions : wizardStore.data.destOptions,
       field,
       field,
-      type: 'destination',
+      type,
     })
     })
-
-    if (providerName && envData && wizardStore.data.target) {
-      providerStore.getOptionsValues({
-        optionsType: 'destination',
-        endpointId: wizardStore.data.target.id,
-        providerName,
-        envData,
-      })
+    if (!envData) {
+      return
     }
     }
+    providerStore.getOptionsValues({
+      optionsType: type,
+      endpointId: endpoint.id,
+      providerName: endpoint.type,
+      envData,
+    })
   }
   }
 
 
   async loadDataForPage(page: WizardPageType) {
   async loadDataForPage(page: WizardPageType) {
+    const loadOptions = async (endpoint: EndpointType, optionsType: 'source' | 'destination') => {
+      let schema = optionsType === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
+      if (schema.length > 0) {
+        return
+      }
+      await providerStore.loadOptionsSchema({
+        providerName: endpoint.type,
+        schemaType: this.state.type,
+        optionsType,
+      })
+
+      // Preload source options if data is set from 'Permalink'
+      if (providerStore.sourceOptions.length === 0) {
+        await providerStore.getOptionsValues({
+          optionsType,
+          endpointId: endpoint.id,
+          providerName: endpoint.type,
+        })
+        await this.loadExtraOptions(undefined, optionsType)
+      }
+    }
+
     switch (page.id) {
     switch (page.id) {
       case 'source': {
       case 'source': {
         providerStore.loadProviders()
         providerStore.loadProviders()
@@ -392,66 +416,47 @@ class WizardPage extends React.Component<Props, State> {
         if (!source) {
         if (!source) {
           return
           return
         }
         }
+        // Preload source options schema
+        loadOptions(source, 'source')
 
 
-        if (providerStore.sourceSchema.length === 0 && source) {
-          let loadOptionsSchema = async () => {
-            await providerStore.loadOptionsSchema({
-              providerName: source.type,
-              schemaType: this.state.type,
-              optionsType: 'source',
-            })
-            // Preload source options if data is set from 'Permalink'
-            if (providerStore.sourceOptions.length === 0 && source) {
-              providerStore.getOptionsValues({
-                optionsType: 'source',
-                endpointId: source.id,
-                providerName: source.type,
-              })
-            }
-          }
-          loadOptionsSchema()
+        if (instanceStore.instances.length > 0) {
+          return
         }
         }
-
-        if (instanceStore.instances.length === 0) {
-          try {
-            // Check if user has permission for this endpoint
-            await endpointStore.getConnectionInfo(source)
-            // Preload instances for 'vms' page
-            instanceStore.loadInstancesInChunks(source, this.instancesPerPage)
-          } catch (err) {
-            this.handleSourceEndpointChange(null)
-          }
+        try {
+          // Check if user has permission for this endpoint
+          await endpointStore.getConnectionInfo(source)
+        } catch (err) {
+          this.handleSourceEndpointChange(null)
+        }
+        break
+      }
+      case 'vms': {
+        if (!wizardStore.data.source) {
+          return
         }
         }
+        instanceStore.loadInstancesInChunks(wizardStore.data.source, this.instancesPerPage, false, wizardStore.data.sourceOptions)
         break
         break
       }
       }
       case 'target': {
       case 'target': {
         let target = wizardStore.data.target
         let target = wizardStore.data.target
+        if (!target) {
+          return
+        }
         // Preload Storage Mapping
         // Preload Storage Mapping
-        if (this.pages.find(p => p.id === 'storage') && target) {
+        if (this.pages.find(p => p.id === 'storage')) {
           endpointStore.loadStorage(target.id, {})
           endpointStore.loadStorage(target.id, {})
         }
         }
         // Preload destination options schema
         // Preload destination options schema
-        if (providerStore.destinationSchema.length === 0 && target) {
-          await providerStore.loadOptionsSchema({
-            providerName: target.type,
-            schemaType: this.state.type,
-            optionsType: 'destination',
-          })
-          // Preload destination options if data is set from 'Permalink'
-          if (providerStore.destinationOptions.length === 0 && target) {
-            await providerStore.getOptionsValues({
-              optionsType: 'destination',
-              endpointId: target.id,
-              providerName: target.type,
-            })
-            this.loadEnvDestinationOptions()
-          }
-        }
+        loadOptions(target, 'destination')
         break
         break
       }
       }
       case 'networks':
       case 'networks':
         if (wizardStore.data.source && wizardStore.data.selectedInstances) {
         if (wizardStore.data.source && wizardStore.data.selectedInstances) {
-          instanceStore.loadInstancesDetails(wizardStore.data.source.id, wizardStore.data.selectedInstances)
+          instanceStore.loadInstancesDetails({
+            endpointId: wizardStore.data.source.id,
+            instancesInfo: wizardStore.data.selectedInstances,
+            env: wizardStore.data.sourceOptions,
+          })
         }
         }
         if (wizardStore.data.target) {
         if (wizardStore.data.target) {
           let id = wizardStore.data.target.id
           let id = wizardStore.data.target.id

+ 21 - 3
src/sources/InstanceSource.js

@@ -25,7 +25,8 @@ class InstanceSource {
     chunkSize: number,
     chunkSize: number,
     lastInstanceId?: string,
     lastInstanceId?: string,
     cancelId?: string,
     cancelId?: string,
-    searchText?: string
+    searchText?: string,
+    env?: any,
   ): Promise<Instance[]> {
   ): Promise<Instance[]> {
     let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances`
     let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances`
     let queryParams: { [string]: string | number } = {}
     let queryParams: { [string]: string | number } = {}
@@ -50,6 +51,13 @@ class InstanceSource {
       }
       }
     }
     }
 
 
+    if (env) {
+      queryParams = {
+        ...queryParams,
+        env: btoa(JSON.stringify(env)),
+      }
+    }
+
     let keys = Object.keys(queryParams)
     let keys = Object.keys(queryParams)
     url = `${url}${keys.length > 0 ? '?' : ''}${keys.map(p => `${p}=${queryParams[p]}`).join('&')}`
     url = `${url}${keys.length > 0 ? '?' : ''}${keys.map(p => `${p}=${queryParams[p]}`).join('&')}`
 
 
@@ -64,9 +72,19 @@ class InstanceSource {
     return response.data.instances
     return response.data.instances
   }
   }
 
 
-  async loadInstanceDetails(endpointId: string, instanceName: string, reqId: number, quietError?: boolean): Promise<{ instance: Instance, reqId: number }> {
+  async loadInstanceDetails(
+    endpointId: string,
+    instanceName: string,
+    reqId: number,
+    quietError?: boolean,
+    env?: any
+  ): Promise<{ instance: Instance, reqId: number }> {
+    let url = `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances/${btoa(instanceName)}`
+    if (env) {
+      url += `?env=${btoa(JSON.stringify(env))}`
+    }
     let response = await Api.send({
     let response = await Api.send({
-      url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/instances/${btoa(instanceName)}`,
+      url,
       cancelId: `instanceDetail-${reqId}`,
       cancelId: `instanceDetail-${reqId}`,
       quietError,
       quietError,
     })
     })

+ 1 - 2
src/sources/ProviderSource.js

@@ -40,8 +40,7 @@ class ProviderSource {
       optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
       optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
 
 
     let response = await Api.get(`${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`)
     let response = await Api.get(`${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`)
-    let schema = optionsType === 'source' ?
-      { oneOf: [response.data.schemas.source_environment_schema] } : response.data.schemas.destination_environment_schema
+    let schema = optionsType === 'source' ? response.data.schemas.source_environment_schema : response.data.schemas.destination_environment_schema
     let fields = SchemaParser.optionsSchemaToFields(providerName, schema)
     let fields = SchemaParser.optionsSchemaToFields(providerName, schema)
     return fields
     return fields
   }
   }

+ 1 - 1
src/sources/WizardSource.js

@@ -34,7 +34,7 @@ class WizardSource {
       destination_endpoint_id: data.target ? data.target.id : 'null',
       destination_endpoint_id: data.target ? data.target.id : 'null',
       destination_environment: destParser.getDestinationEnv(data.destOptions),
       destination_environment: destParser.getDestinationEnv(data.destOptions),
       network_map: destParser.getNetworkMap(data.networks),
       network_map: destParser.getNetworkMap(data.networks),
-      instances: data.selectedInstances ? data.selectedInstances.map(i => i.instance_name) : 'null',
+      instances: data.selectedInstances ? data.selectedInstances.map(i => i.instance_name || i.name) : 'null',
       storage_mappings: destParser.getStorageMap(defaultStorage, storageMap),
       storage_mappings: destParser.getStorageMap(defaultStorage, storageMap),
       notes: data.destOptions ? data.destOptions.description || '' : '',
       notes: data.destOptions ? data.destOptions.description || '' : '',
     }
     }

+ 18 - 10
src/stores/InstanceStore.js

@@ -117,7 +117,7 @@ class InstanceStore {
   lastEndpointId: string
   lastEndpointId: string
   reqId: number
   reqId: number
 
 
-  @action async loadInstancesInChunks(endpoint: Endpoint, vmsPerPage?: number = 6, reload?: boolean) {
+  @action async loadInstancesInChunks(endpoint: Endpoint, vmsPerPage?: number = 6, reload?: boolean, env?: any) {
     ApiCaller.cancelRequests(`${endpoint.id}-chunk`)
     ApiCaller.cancelRequests(`${endpoint.id}-chunk`)
 
 
     this.backgroundInstances = []
     this.backgroundInstances = []
@@ -133,7 +133,7 @@ class InstanceStore {
 
 
     let loadNextChunk = async (lastEndpointId?: string) => {
     let loadNextChunk = async (lastEndpointId?: string) => {
       let currentEndpointId = endpoint.id
       let currentEndpointId = endpoint.id
-      let instances = await InstanceSource.loadInstancesChunk(currentEndpointId, chunkCount, lastEndpointId, `${endpoint.id}-chunk`)
+      let instances = await InstanceSource.loadInstancesChunk(currentEndpointId, chunkCount, lastEndpointId, `${endpoint.id}-chunk`, undefined, env)
       if (currentEndpointId !== this.lastEndpointId) {
       if (currentEndpointId !== this.lastEndpointId) {
         return
         return
       }
       }
@@ -205,7 +205,7 @@ class InstanceStore {
 
 
     if (!this.backgroundChunksLoading) {
     if (!this.backgroundChunksLoading) {
       this.searchedInstances = this.backgroundInstances
       this.searchedInstances = this.backgroundInstances
-        .filter(i => i.instance_name.toLowerCase().indexOf(searchText.toLowerCase()) > -1)
+        .filter(i => (i.instance_name || i.name).toLowerCase().indexOf(searchText.toLowerCase()) > -1)
       this.searchNotFound = Boolean(this.searchedInstances.length === 0)
       this.searchNotFound = Boolean(this.searchedInstances.length === 0)
       this.currentPage = 1
       this.currentPage = 1
       return
       return
@@ -250,11 +250,11 @@ class InstanceStore {
     return true
     return true
   }
   }
 
 
-  @action reloadInstances(endpoint: Endpoint, chunkSize?: number) {
+  @action reloadInstances(endpoint: Endpoint, chunkSize?: number, env?: any) {
     this.searchNotFound = false
     this.searchNotFound = false
     this.searchText = ''
     this.searchText = ''
     this.currentPage = 1
     this.currentPage = 1
-    this.loadInstancesInChunks(endpoint, chunkSize, true)
+    this.loadInstancesInChunks(endpoint, chunkSize, true, env)
   }
   }
 
 
   @action cancelIntancesChunksLoading() {
   @action cancelIntancesChunksLoading() {
@@ -274,13 +274,20 @@ class InstanceStore {
     this.instancesPerPage = instancesPerPage
     this.instancesPerPage = instancesPerPage
   }
   }
 
 
-  @action async loadInstancesDetails(endpointId: string, instancesInfo: Instance[], useLocalStorage?: boolean, quietError?: boolean): Promise<void> {
+  @action async loadInstancesDetails(opts: {
+    endpointId: string,
+    instancesInfo: Instance[],
+    useLocalStorage?: boolean,
+    quietError?: boolean,
+    env?: any,
+  }): Promise<void> {
+    let { endpointId, instancesInfo, useLocalStorage, quietError, env } = opts
     // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
     // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
     this.reqId = !this.reqId ? 1 : this.reqId + 1
     this.reqId = !this.reqId ? 1 : this.reqId + 1
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
 
 
-    instancesInfo.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
-    let hash = i => `${i.instance_name}-${i.id || endpointId}`
+    instancesInfo.sort((a, b) => (a.instance_name || a.name).localeCompare(b.instance_name || b.name))
+    let hash = i => `${i.instance_name || i.name}-${i.id || endpointId}`
     if (useLocalStorage && this.instancesDetails.map(hash).join('_') === instancesInfo.map(hash).join('_')) {
     if (useLocalStorage && this.instancesDetails.map(hash).join('_') === instancesInfo.map(hash).join('_')) {
       return
       return
     }
     }
@@ -306,7 +313,8 @@ class InstanceStore {
       Promise.all(instancesInfo.map(async instanceInfo => {
       Promise.all(instancesInfo.map(async instanceInfo => {
         try {
         try {
           let resp: { instance: Instance, reqId: number } =
           let resp: { instance: Instance, reqId: number } =
-            await InstanceSource.loadInstanceDetails(endpointId, instanceInfo.instance_name, this.reqId, quietError)
+            await InstanceSource.loadInstanceDetails(endpointId, instanceInfo.instance_name || instanceInfo.name,
+              this.reqId, quietError, env)
           if (resp.reqId !== this.reqId) {
           if (resp.reqId !== this.reqId) {
             return
             return
           }
           }
@@ -327,7 +335,7 @@ class InstanceStore {
               ...this.instancesDetails,
               ...this.instancesDetails,
               resp.instance,
               resp.instance,
             ]
             ]
-            this.instancesDetails.sort((a, b) => a.instance_name.localeCompare(b.instance_name))
+            this.instancesDetails.sort((a, b) => (a.instance_name || a.name).localeCompare((b.instance_name || b.name)))
           })
           })
           if (this.instancesDetailsRemaining === 0) {
           if (this.instancesDetailsRemaining === 0) {
             resolve()
             resolve()

+ 5 - 4
src/stores/ProviderStore.js

@@ -32,16 +32,17 @@ export const getFieldChangeOptions = (config: {
   type: 'source' | 'destination',
   type: 'source' | 'destination',
 }) => {
 }) => {
   let { providerName, schema, data, field, type } = config
   let { providerName, schema, data, field, type } = config
-  let providerWithEnvOptions = configLoader.config.providersWithEnvOptions.find(p => p.name === providerName && p.type === type)
+  let providerWithEnvOptions = configLoader.config.extraOptionsApiCalls
+    .find(p => p.name === providerName && p.types.find(t => t === type))
 
 
   if (!providerName || !providerWithEnvOptions) {
   if (!providerName || !providerWithEnvOptions) {
     return null
     return null
   }
   }
-  let envRequiredFields = providerWithEnvOptions.envRequiredFields
+  let requiredFields = providerWithEnvOptions.requiredFields
 
 
   let findFieldInSchema = (name: string) => schema.find(f => f.name === name)
   let findFieldInSchema = (name: string) => schema.find(f => f.name === name)
 
 
-  let validFields = envRequiredFields.filter(fn => {
+  let validFields = requiredFields.filter(fn => {
     let schemaField = findFieldInSchema(fn)
     let schemaField = findFieldInSchema(fn)
     if (data) {
     if (data) {
       if (data[fn] === null) {
       if (data[fn] === null) {
@@ -56,7 +57,7 @@ export const getFieldChangeOptions = (config: {
   })
   })
 
 
   let isCurrentFieldValid = field ? validFields.find(fn => field ? fn === field.name : false) : true
   let isCurrentFieldValid = field ? validFields.find(fn => field ? fn === field.name : false) : true
-  if (validFields.length !== envRequiredFields.length || !isCurrentFieldValid) {
+  if (validFields.length !== requiredFields.length || !isCurrentFieldValid) {
     return null
     return null
   }
   }
 
 

+ 3 - 1
src/types/Config.js

@@ -1,5 +1,7 @@
 // @flow
 // @flow
 
 
+type Type = 'source' | 'destination'
+
 export type Config = {
 export type Config = {
   disabledPages: string[],
   disabledPages: string[],
   showUserDomainInput: boolean,
   showUserDomainInput: boolean,
@@ -9,7 +11,7 @@ export type Config = {
   requestPollTimeout: number,
   requestPollTimeout: number,
   sourceOptionsProviders: string[],
   sourceOptionsProviders: string[],
   instancesListBackgroundLoading: { default: number, [string]: number },
   instancesListBackgroundLoading: { default: number, [string]: number },
-  providersWithEnvOptions: Array<{ name: string, type: 'source' | 'destination', envRequiredFields: string[] }>,
+  extraOptionsApiCalls: Array<{ name: string, types: Type[], requiredFields: string[] }>,
   providerSortPriority: { [providerName: string]: number },
   providerSortPriority: { [providerName: string]: number },
   hiddenUsers: string[],
   hiddenUsers: string[],
 }
 }

+ 1 - 1
src/types/Instance.js

@@ -35,7 +35,7 @@ export type Instance = {
   id: string,
   id: string,
   name: string,
   name: string,
   flavor_name: string,
   flavor_name: string,
-  instance_name: string,
+  instance_name?: ?string,
   num_cpu: number,
   num_cpu: number,
   memory_mb: number,
   memory_mb: number,
   os_type: string,
   os_type: string,