ソースを参照

Merge pull request #385 from smiclea/source-options

Add Edit Replica source options
Dorin Paslaru 6 年 前
コミット
ba920da792

+ 1 - 3
config.js

@@ -40,9 +40,7 @@ const conf: Config = {
   // A list of providers for which `source-options` API call(s) will be made
   // If the item is just a string with the provider name, only one API call will be made
   // If the item has `envRequiredFields`, an additional API call will be made once the specified fields are filled
-  sourceProvidersWithExtraOptions: [
-    'aws',
-  ],
+  sourceProvidersWithExtraOptions: ['aws'],
 
   // A list of providers for which `destination-options` API call(s) will be made
   // If the item is just a string with the provider name, only one API call will be made

+ 1 - 0
src/components/molecules/MainDetailsTable/test.jsx

@@ -65,6 +65,7 @@ const defaultItem: MainItem = {
     },
   },
   destination_environment: { option1: 'value1' },
+  source_environment: { option1: 'value1' },
   transfer_result: {
     instance1: defaultInstance,
   },

+ 3 - 3
src/components/molecules/Panel/Panel.jsx

@@ -70,7 +70,7 @@ export type NavigationItem = {
 export type Props = {
   navigationItems: NavigationItem[],
   content: React.Node,
-  selectedValue: string,
+  selectedValue: ?string,
   onChange: (item: NavigationItem) => void,
   style?: any,
   onReloadClick: () => void,
@@ -90,10 +90,10 @@ class Panel extends React.Component<Props> {
     return (
       <Wrapper style={this.props.style}>
         <Navigation>
-          {this.props.navigationItems.map(item => (
+          {this.props.navigationItems.map((item, i) => (
             <NavigationItemDiv
               key={item.value}
-              selected={this.props.selectedValue === item.value}
+              selected={this.props.selectedValue ? this.props.selectedValue === item.value : i === 0}
               onClick={() => { this.handleItemClick(item) }}
               data-test-id={`${TEST_ID}-navItem-${item.value}`}
             >{item.label}</NavigationItemDiv>

+ 99 - 52
src/components/organisms/EditReplica/EditReplica.jsx

@@ -38,8 +38,9 @@ import type { Endpoint, StorageBackend, StorageMap } from '../../../types/Endpoi
 import type { Field } from '../../../types/Field'
 import type { Instance, Nic, Disk } from '../../../types/Instance'
 import type { Network, NetworkMap } from '../../../types/Network'
-import { providerTypes } from '../../../constants'
 
+import { providerTypes } from '../../../constants'
+import configLoader from '../../../utils/Config'
 import StyleProps from '../../styleUtils/StyleProps'
 
 const PanelContent = styled.div`
@@ -82,8 +83,9 @@ type Props = {
   onReloadClick: () => void,
 }
 type State = {
-  selectedPanel: string,
+  selectedPanel: ?string,
   destinationData: any,
+  sourceData: any,
   updateDisabled: boolean,
   selectedNetworks: NetworkMap[],
   storageMap: StorageMap[],
@@ -92,8 +94,9 @@ type State = {
 @observer
 class EditReplica extends React.Component<Props, State> {
   state = {
-    selectedPanel: 'dest_options',
+    selectedPanel: null,
     destinationData: {},
+    sourceData: {},
     updateDisabled: false,
     selectedNetworks: [],
     storageMap: [],
@@ -103,6 +106,8 @@ class EditReplica extends React.Component<Props, State> {
 
   componentWillMount() {
     this.loadData(true)
+
+    this.setState({ selectedPanel: this.hasSourceOptions() ? 'source_options' : 'dest_options' })
   }
 
   loadData(useCache: boolean) {
@@ -112,16 +117,26 @@ class EditReplica extends React.Component<Props, State> {
       }
     })
 
-    providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, this.props.type || 'replica', useCache).then(() => {
-      return providerStore.getOptionsValues({
+    providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, this.props.type || 'replica', useCache)
+      .then(() => providerStore.getOptionsValues({
         optionsType: 'destination',
         endpointId: this.props.destinationEndpoint.id,
         provider: this.props.destinationEndpoint.type,
         useCache,
+      })).then(() => {
+        this.loadEnvDestinationOptions()
       })
-    }).then(() => {
-      this.loadEnvDestinationOptions()
-    })
+
+    if (!this.hasSourceOptions()) {
+      return
+    }
+    providerStore.loadSourceSchema(this.props.sourceEndpoint.type, this.props.type || 'replica', useCache)
+      .then(() => providerStore.getOptionsValues({
+        optionsType: 'source',
+        endpointId: this.props.sourceEndpoint.id,
+        provider: this.props.sourceEndpoint.type,
+        useCache,
+      }))
   }
 
   hasStorageMap(): boolean {
@@ -135,31 +150,37 @@ class EditReplica extends React.Component<Props, State> {
       : false
   }
 
+  hasSourceOptions(): boolean {
+    return Boolean(configLoader.config.sourceOptionsProviders.find(p => p === this.props.sourceEndpoint.type))
+  }
+
   isUpdateDisabled() {
     let isLoadingDestOptions = this.state.selectedPanel === 'dest_options'
       && (providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading)
+    let isLoadingSourceOptions = this.state.selectedPanel === 'source_options'
+      && (providerStore.sourceSchemaLoading || providerStore.sourceOptionsLoading)
     let isLoadingNetwork = this.state.selectedPanel === 'network_mapping' && this.props.instancesDetailsLoading
     let isLoadingStorage = this.state.selectedPanel === 'storage_mapping'
       && (this.props.instancesDetailsLoading || endpointStore.storageLoading)
-    return this.state.updateDisabled || isLoadingDestOptions || isLoadingNetwork || isLoadingStorage
+    return this.state.updateDisabled || isLoadingSourceOptions || isLoadingDestOptions || isLoadingNetwork || isLoadingStorage
   }
 
-  parseReplicaData() {
+  parseReplicaData(environment: ?{ [string]: mixed }) {
     let data = {}
-    let destEnv = this.props.replica.destination_environment
-    if (!destEnv) {
+    let env = environment
+    if (!env) {
       return data
     }
-    Object.keys(destEnv).forEach(key => {
-      if (destEnv[key] && typeof destEnv[key] === 'object') {
-        Object.keys(destEnv[key]).forEach(subkey => {
-          let destParent: any = destEnv[key]
+    Object.keys(env).forEach(key => {
+      if (env[key] && typeof env[key] === 'object') {
+        Object.keys(env[key]).forEach(subkey => {
+          let destParent: any = env[key]
           if (destParent[subkey]) {
             data[`${key}/${subkey}`] = destParent[subkey]
           }
         })
       } else {
-        data[key] = destEnv[key]
+        data[key] = env[key]
       }
     })
     return data
@@ -170,7 +191,7 @@ class EditReplica extends React.Component<Props, State> {
       provider: this.props.destinationEndpoint.type,
       destSchema: providerStore.destinationSchema,
       data: {
-        ...this.parseReplicaData(),
+        ...this.parseReplicaData(this.props.replica.destination_environment),
         ...this.state.destinationData,
       },
       field,
@@ -187,11 +208,14 @@ class EditReplica extends React.Component<Props, State> {
     }
   }
 
-  validateDestinationOptions() {
+  validateOptions(type: 'source' | 'destination') {
+    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 schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
     let isValid = isOptionsPageValid({
-      ...this.parseReplicaData(),
-      ...this.state.destinationData,
-    }, providerStore.destinationSchema)
+      ...this.parseReplicaData(env),
+      ...data,
+    }, schema)
 
     this.setState({ updateDisabled: !isValid })
   }
@@ -205,32 +229,39 @@ class EditReplica extends React.Component<Props, State> {
     this.loadData(false)
   }
 
-  handleDestinationFieldChange(field: Field, value: any) {
-    let destinationData = { ...this.state.destinationData }
+  handleFieldChange(type: 'source' | 'destination', field: Field, value: any) {
+    let data = type === 'source' ? { ...this.state.sourceData } : { ...this.state.destinationData }
     if (field.type === 'array') {
-      let oldValues: string[] = destinationData[field.name] || []
+      let oldValues: string[] = data[field.name] || []
       if (oldValues.find(v => v === value)) {
-        destinationData[field.name] = oldValues.filter(v => v !== value)
+        data[field.name] = oldValues.filter(v => v !== value)
       } else {
-        destinationData[field.name] = [...oldValues, value]
+        data[field.name] = [...oldValues, value]
       }
     } else {
-      destinationData[field.name] = value
+      data[field.name] = value
     }
 
-    this.setState({ destinationData }, () => {
-      if (field.type !== 'string' || field.enum) {
-        this.loadEnvDestinationOptions(field)
-      }
+    if (type === 'source') {
+      this.setState({ sourceData: data }, () => {
+        this.validateOptions('source')
+      })
+    } else {
+      this.setState({ destinationData: data }, () => {
+        if (field.type !== 'string' || field.enum) {
+          this.loadEnvDestinationOptions(field)
+        }
 
-      this.validateDestinationOptions()
-    })
+        this.validateOptions('destination')
+      })
+    }
   }
 
   handleUpdateClick() {
     this.setState({ updateDisabled: true })
 
     let updateData: UpdateData = {
+      source: this.state.sourceData,
       destination: this.state.destinationData,
       network: this.state.selectedNetworks.length > 0 ? this.getSelectedNetworks() : [],
       storage: this.state.destinationData.default_storage || this.state.storageMap.length > 0 ? this.getStorageMap() : [],
@@ -266,15 +297,22 @@ class EditReplica extends React.Component<Props, State> {
     this.setState({ storageMap })
   }
 
-  getFieldValue(fieldName: string, defaultValue: any) {
-    if (this.state.destinationData[fieldName] === undefined) {
-      let replicaData = this.parseReplicaData()
+  getFieldValue(type: 'source' | 'destination', fieldName: string, defaultValue: any) {
+    let currentData = type === 'source' ? this.state.sourceData : this.state.destinationData
+    if (currentData[fieldName] === undefined) {
+      let replicaData = this.parseReplicaData(type === 'source' ? this.props.replica.source_environment
+        : this.props.replica.destination_environment)
       if (replicaData[fieldName] !== undefined) {
         return replicaData[fieldName]
       }
+      let osMapping = /^(windows|linux)_os_image$/.exec(fieldName)
+      if (osMapping) {
+        let osData = replicaData[`migr_image_map/${osMapping[1]}`]
+        return osData
+      }
       return defaultValue
     }
-    return this.state.destinationData[fieldName]
+    return currentData[fieldName]
   }
 
   getSelectedNetworks(): NetworkMap[] {
@@ -335,26 +373,28 @@ class EditReplica extends React.Component<Props, State> {
     return storageMap
   }
 
-  renderDestinationOptions() {
-    if (providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading) {
-      return this.renderLoading('Loading target options ...')
+  renderOptions(type: 'source' | 'destination') {
+    let loading = type === 'source' ? (providerStore.sourceSchemaLoading || providerStore.sourceOptionsLoading) :
+      providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading
+    if (loading) {
+      return this.renderLoading(`Loading ${type === 'source' ? 'source' : 'target'} options ...`)
     }
-    let fields = this.props.type === 'replica' ? providerStore.destinationSchema.filter(f => !f.readOnly) :
-      providerStore.destinationSchema
+    let schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
+    let fields = this.props.type === 'replica' ? schema.filter(f => !f.readOnly) : schema
 
     return (
       <WizardOptions
-        wizardType="replica-dest-options-edit"
-        getFieldValue={(f, d) => this.getFieldValue(f, d)}
+        wizardType={`replica-${type}-options-edit`}
+        getFieldValue={(f, d) => this.getFieldValue(type, f, d)}
         fields={fields}
-        hasStorageMap={this.hasStorageMap()}
-        onChange={(f, v) => { this.handleDestinationFieldChange(f, v) }}
-        storageBackends={endpointStore.storageBackends}
-        useAdvancedOptions
+        hasStorageMap={type === 'source' ? false : this.hasStorageMap()}
+        onChange={(f, v) => { this.handleFieldChange(type, f, v) }}
+        oneColumnStyle={{ marginTop: '-16px', display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }}
         columnStyle={{ marginRight: 0 }}
         fieldWidth={StyleProps.inputSizes.large.width}
         onScrollableRef={ref => { this.scrollableRef = ref }}
         availableHeight={384}
+        useAdvancedOptions
       />
     )
   }
@@ -372,7 +412,7 @@ class EditReplica extends React.Component<Props, State> {
         storageBackends={endpointStore.storageBackends}
         instancesDetails={this.props.instancesDetails}
         storageMap={this.getStorageMap()}
-        defaultStorage={this.getFieldValue('default_storage')}
+        defaultStorage={this.getFieldValue('destination', 'default_storage')}
         onChange={(s, t, type) => { this.handleStorageChange(s, t, type) }}
       />
     )
@@ -394,8 +434,11 @@ class EditReplica extends React.Component<Props, State> {
   renderContent() {
     let content = null
     switch (this.state.selectedPanel) {
+      case 'source_options':
+        content = this.renderOptions('source')
+        break
       case 'dest_options':
-        content = this.renderDestinationOptions()
+        content = this.renderOptions('destination')
         break
       case 'network_mapping':
         content = this.renderNetworkMapping()
@@ -439,7 +482,7 @@ class EditReplica extends React.Component<Props, State> {
   }
 
   render() {
-    const navigationItems: NavigationItem[] = [
+    let navigationItems: NavigationItem[] = [
       { value: 'dest_options', label: 'Target Options' },
       { value: 'network_mapping', label: 'Network Mapping' },
     ]
@@ -448,6 +491,10 @@ class EditReplica extends React.Component<Props, State> {
       navigationItems.push({ value: 'storage_mapping', label: 'Storage Mapping' })
     }
 
+    if (this.hasSourceOptions()) {
+      navigationItems.splice(0, 0, { value: 'source_options', label: 'Source Options' })
+    }
+
     return (
       <Modal
         isOpen={this.props.isOpen}

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

@@ -85,6 +85,7 @@ type Props = {
   wizardType: string,
   loading?: boolean,
   columnStyle?: { [string]: mixed },
+  oneColumnStyle?: { [string]: mixed },
   fieldWidth?: number,
   onScrollableRef?: (ref: HTMLElement) => void,
   availableHeight?: number,
@@ -214,7 +215,7 @@ class WizardOptions extends React.Component<Props> {
     if (fields.length * 96 < availableHeight) {
       return (
         <Fields>
-          <OneColumn>
+          <OneColumn style={this.props.oneColumnStyle}>
             {fields.map(f => f.component)}
           </OneColumn>
         </Fields>

+ 4 - 0
src/sources/ReplicaSource.js

@@ -176,6 +176,10 @@ class ReplicaSource {
       payload.replica.destination_environment = parser.getDestinationEnv(updateData.destination, replica.destination_environment)
     }
 
+    if (Object.keys(updateData.source).length > 0) {
+      payload.replica.source_environment = parser.getDestinationEnv(updateData.source, replica.source_environment)
+    }
+
     if (updateData.storage.length > 0) {
       payload.replica.storage_mappings = parser.getStorageMap(updateData.destination, updateData.storage)
     }

+ 2 - 0
src/stores/MigrationStore.js

@@ -56,6 +56,8 @@ class MigrationStore {
       sourceEndpoint,
       destEndpoint,
       instanceNames: migration.instances,
+      sourceEnv: migration.source_environment,
+      updatedSourceEnv: updateData.source,
       destEnv: migration.destination_environment,
       updatedDestEnv: updateData.destination,
       storageMappings: migration.storage_mappings,

+ 12 - 2
src/stores/ProviderStore.js

@@ -157,13 +157,23 @@ class ProviderStore {
     })
   }
 
-  @action loadSourceSchema(providerName: string, schemaType: string): Promise<void> {
-    this.sourceSchemaLoading = true
+  sourceSchemaCache: { [string]: Field[] } = {}
+  @action loadSourceSchema(providerName: string, schemaType: string, useCache?: boolean): Promise<void> {
     this.lastSourceSchemaType = schemaType
 
+    let cacheKey = `${providerName}-${schemaType}`
+    let cacheData = this.sourceSchemaCache[cacheKey]
+    if (useCache && cacheData) {
+      this.sourceSchema = [...cacheData]
+      return Promise.resolve()
+    }
+
+    this.sourceSchemaLoading = true
+
     return ProviderSource.loadSourceSchema(providerName, schemaType).then((fields: Field[]) => {
       this.sourceSchemaLoading = false
       this.sourceSchema = fields
+      this.sourceSchemaCache[cacheKey] = fields
     }).catch(err => {
       this.sourceSchemaLoading = false
       throw err

+ 2 - 0
src/types/MainItem.js

@@ -32,6 +32,7 @@ export type MainItemInfo = {
 
 export type UpdateData = {
   destination: any,
+  source: any,
   network: NetworkMap[],
   storage: StorageMap[],
 }
@@ -52,6 +53,7 @@ export type MainItem = {
   type: string,
   info: { [string]: MainItemInfo },
   destination_environment: { [string]: mixed },
+  source_environment: { [string]: mixed },
   transfer_result: ?{ [string]: Instance },
   storage_mappings?: ?{
     backend_mappings: ?{