Procházet zdrojové kódy

Add 'source-options' API support

AWS supports source options schema and now it also supports
'source-options' API calls to populate the source options schema with
available values, similar to 'destination-options' API calls available
to other providers.

To add to the list of providers which support the 'source-options' API,
update the `sourceProvidersWithExtraOptions` property in `./config.js`
file.
Sergiu Miclea před 6 roky
rodič
revize
1b82a945bc

+ 8 - 1
config.js

@@ -37,10 +37,17 @@ const conf: Config = {
   // - `Infinity` value means no `limit` will be used, i.e. all VMs will be listed.
   instancesListBackgroundLoading: { default: 10, ovm: Infinity },
 
+  // 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',
+  ],
+
   // 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
   // If the item has `envRequiredFields`, an additional API call will be made once the specified fields are filled
-  providersWithExtraOptions: [
+  destinationProvidersWithExtraOptions: [
     'openstack',
     'oracle_vm',
     'aws',

+ 1 - 2
src/components/atoms/DropdownButton/DropdownButton.jsx

@@ -35,7 +35,7 @@ const getLabelColor = props => {
 }
 const Label = styled.div`
   color: ${props => getLabelColor(props)};
-  margin: 0 32px 0 16px;
+  margin: 0 32px 0 ${props => props.embedded ? 0 : 16}px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -119,7 +119,6 @@ const Wrapper = styled.div`
   ${props => props.embedded ? css`
     border: 0;
     width: calc(100% + 8px);
-    margin-left: -16px;
   ` : ''}
 
   #dropdown-arrow-image {stroke: ${props => getArrowColor(props)};}

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

@@ -34,9 +34,9 @@ const Wrapper = styled.div`
   border-radius: ${StyleProps.borderRadius};
 `
 const Column = styled.div`
-  ${StyleProps.exactWidth('calc(50% - 32px)')}
+  ${StyleProps.exactWidth('calc(50% - 24px)')}
   height: 32px;
-  padding: 0 16px;
+  padding: 0 8px 0 16px;
   display: flex;
   align-items: center;
   ${props => props.header ? css`

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

@@ -108,7 +108,12 @@ class EditReplica extends React.Component<Props, State> {
     })
 
     providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, this.props.type || 'replica').then(() => {
-      return providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type, undefined, true)
+      return providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: this.props.destinationEndpoint.id,
+        provider: this.props.destinationEndpoint.type,
+        useCache: true,
+      })
     }).then(() => {
       this.loadEnvDestinationOptions()
     })
@@ -167,7 +172,13 @@ class EditReplica extends React.Component<Props, State> {
     })
 
     if (envData) {
-      providerStore.getDestinationOptions(this.props.destinationEndpoint.id, this.props.destinationEndpoint.type, envData, true)
+      providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: this.props.destinationEndpoint.id,
+        provider: this.props.destinationEndpoint.type,
+        useCache: true,
+        envData,
+      })
     }
   }
 

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

@@ -335,7 +335,7 @@ class WizardPageContent extends React.Component<Props, State> {
       case 'source-options':
         body = (
           <WizardOptions
-            loading={this.props.providerStore.sourceSchemaLoading}
+            loading={this.props.providerStore.sourceSchemaLoading || this.props.providerStore.sourceOptionsLoading}
             fields={this.props.providerStore.sourceSchema}
             onChange={this.props.onSourceOptionsChange}
             data={this.props.wizardData.sourceOptions}

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

@@ -314,7 +314,11 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
 
   loadTargetOptions(): Promise<void> {
     let localData = this.getLocalData()
-    return providerStore.getDestinationOptions(localData.endpoint.id, localData.endpoint.type).then(options => {
+    return providerStore.getOptionsValues({
+      optionsType: 'destination',
+      endpointId: localData.endpoint.id,
+      provider: localData.endpoint.type,
+    }).then(options => {
       let locations = options.find(o => o.name === 'location')
       if (locations && locations.values) {
         let localDataFind = locations.values.find(l => l.id === localData.locationName)
@@ -338,9 +342,14 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   loadTargetVmSizes() {
     let localData = this.getLocalData()
     this.setState({ loadingTargetVmSizes: true })
-    providerStore.getDestinationOptions(localData.endpoint.id, localData.endpoint.type, {
-      location: localData.locationName,
-      resource_group: localData.resourceGroupName,
+    providerStore.getOptionsValues({
+      optionsType: 'destination',
+      endpointId: localData.endpoint.id,
+      provider: localData.endpoint.type,
+      envData: {
+        location: localData.locationName,
+        resource_group: localData.resourceGroupName,
+      },
     }).then(options => {
       let vmSizes = options.find(o => o.name === 'vm_size')
       if (vmSizes && vmSizes.values) {

+ 29 - 5
src/components/pages/WizardPage/WizardPage.jsx

@@ -232,7 +232,13 @@ class WizardPage extends React.Component<Props, State> {
       this.handleSourceEndpointChange(null)
     })
 
-    providerStore.loadSourceSchema(source.type, this.state.type === 'replica')
+    providerStore.loadSourceSchema(source.type, this.state.type).then(() => {
+      source && providerStore.getOptionsValues({
+        optionsType: 'source',
+        endpointId: source.id,
+        provider: source.type,
+      })
+    })
   }
 
   handleTargetEndpointChange(target: EndpointType) {
@@ -242,7 +248,7 @@ class WizardPage extends React.Component<Props, State> {
     // Preload destination options schema
     providerStore.loadDestinationSchema(target.type, this.state.type).then(() => {
       // Preload destination options values
-      return providerStore.getDestinationOptions(target.id, target.type)
+      providerStore.getOptionsValues({ optionsType: 'destination', endpointId: target.id, provider: target.type })
     })
     if (this.pages.find(p => p.id === 'storage')) {
       endpointStore.loadStorage(target.id, {})
@@ -299,6 +305,8 @@ class WizardPage extends React.Component<Props, State> {
     // 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.
+    // Otherwise, the field has enum property, which there potentially other destination options for the new
+    // chosen value from the enum
     if (field.type !== 'string' || field.enum) {
       this.loadEnvDestinationOptions(field)
     }
@@ -349,7 +357,12 @@ class WizardPage extends React.Component<Props, State> {
     })
 
     if (provider && envData && wizardStore.data.target) {
-      providerStore.getDestinationOptions(wizardStore.data.target.id, provider, envData)
+      providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: wizardStore.data.target.id,
+        provider,
+        envData,
+      })
     }
   }
 
@@ -364,7 +377,14 @@ class WizardPage extends React.Component<Props, State> {
           return
         }
 
-        providerStore.loadSourceSchema(source.type, this.state.type === 'replica')
+        if (providerStore.sourceSchema.length === 0 && source) {
+          providerStore.loadSourceSchema(source.type, this.state.type).then(() => {
+            // Preload source options if data is set from 'Permalink'
+            if (providerStore.sourceOptions.length === 0 && source) {
+              providerStore.getOptionsValues({ optionsType: 'source', endpointId: source.id, provider: source.type })
+            }
+          })
+        }
 
         if (instanceStore.instances.length === 0) {
           // Check if user has permission for this endpoint
@@ -388,7 +408,11 @@ class WizardPage extends React.Component<Props, State> {
           providerStore.loadDestinationSchema(target.type, this.state.type).then(() => {
             // Preload destination options if data is set from 'Permalink'
             if (providerStore.destinationOptions.length === 0 && target) {
-              providerStore.getDestinationOptions(target.id, target.type).then(() => {
+              providerStore.getOptionsValues({
+                optionsType: 'destination',
+                endpointId: target.id,
+                provider: target.type,
+              }).then(() => {
                 this.loadEnvDestinationOptions()
               })
             }

+ 4 - 4
src/plugins/endpoint/default/OptionsSchemaPlugin.js

@@ -15,13 +15,13 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 
 import type { Field } from '../../../types/Field'
-import type { DestinationOption, StorageMap } from '../../../types/Endpoint'
+import type { OptionValues, StorageMap } from '../../../types/Endpoint'
 import type { NetworkMap } from '../../../types/Network'
 import { executionOptions } from '../../../constants'
 
 const migrationImageOsTypes = ['windows', 'linux']
 
-export const defaultFillFieldValues = (field: Field, option: DestinationOption) => {
+export const defaultFillFieldValues = (field: Field, option: OptionValues) => {
   if (field.type === 'string') {
     field.enum = [...option.values]
     if (option.config_default) {
@@ -33,7 +33,7 @@ export const defaultFillFieldValues = (field: Field, option: DestinationOption)
   }
 }
 
-export const defaultFillMigrationImageMapValues = (field: Field, option: DestinationOption): boolean => {
+export const defaultFillMigrationImageMapValues = (field: Field, option: OptionValues): boolean => {
   if (field.name === 'migr_image_map') {
     field.properties = migrationImageOsTypes.map(os => {
       let values = option.values
@@ -109,7 +109,7 @@ export const defaultGetMigrationImageMap = (options: ?{ [string]: mixed }) => {
 }
 
 export default class OptionsSchemaParser {
-  static fillFieldValues(field: Field, options: DestinationOption[]) {
+  static fillFieldValues(field: Field, options: OptionValues[]) {
     let option = options.find(f => f.name === field.name)
     if (!option) {
       return

+ 8 - 6
src/sources/ProviderSource.js

@@ -19,7 +19,7 @@ import { servicesUrl, providerTypes } from '../constants'
 import { SchemaParser } from './Schemas'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
-import type { DestinationOption } from '../types/Endpoint'
+import type { OptionValues } from '../types/Endpoint'
 
 class ProviderSource {
   static getConnectionInfoSchema(providerName: string): Promise<Field[]> {
@@ -44,8 +44,8 @@ class ProviderSource {
     })
   }
 
-  static loadSourceSchema(providerName: string, isReplica: boolean): Promise<Field[]> {
-    let schemaTypeInt = isReplica ? providerTypes.SOURCE_REPLICA : providerTypes.SOURCE_MIGRATION
+  static loadSourceSchema(providerName: string, schemaType: string): Promise<Field[]> {
+    let schemaTypeInt = schemaType === 'replica' ? 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] }
@@ -54,14 +54,16 @@ class ProviderSource {
     })
   }
 
-  static getDestinationOptions(endpointId: string, envData: ?{ [string]: mixed }): Promise<DestinationOption[]> {
+  static getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }): Promise<OptionValues[]> {
     let envString = ''
     if (envData) {
       envString = `?env=${btoa(JSON.stringify(envData))}`
     }
+    let callName = optionsType === 'source' ? 'source-options' : 'destination-options'
+    let fieldName = optionsType === 'source' ? 'source_options' : 'destination_options'
 
-    return Api.get(`${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/destination-options${envString}`)
-      .then(response => response.data.destination_options)
+    return Api.get(`${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/${callName}${envString}`)
+      .then(response => response.data[fieldName])
   }
 }
 

+ 70 - 35
src/stores/ProviderStore.js

@@ -19,7 +19,7 @@ import { observable, action } from 'mobx'
 import ProviderSource from '../sources/ProviderSource'
 import configLoader from '../utils/Config'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
-import type { DestinationOption } from '../types/Endpoint'
+import type { OptionValues } from '../types/Endpoint'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
 
@@ -30,7 +30,7 @@ export const getFieldChangeDestOptions = (options: {
   field: ?Field,
 }) => {
   let { provider, destSchema, data, field } = options
-  let providerWithExtraOptions = configLoader.config.providersWithExtraOptions
+  let providerWithExtraOptions = configLoader.config.destinationProvidersWithExtraOptions
     .find(p => typeof p !== 'string' && p.name === provider)
   if (!provider || !providerWithExtraOptions || typeof providerWithExtraOptions === 'string' || !providerWithExtraOptions.envRequiredFields) {
     return null
@@ -78,12 +78,15 @@ class ProviderStore {
   @observable providersLoading: boolean = false
   @observable destinationSchema: Field[] = []
   @observable destinationSchemaLoading: boolean = false
-  @observable destinationOptions: DestinationOption[] = []
+  @observable destinationOptions: OptionValues[] = []
   @observable destinationOptionsLoading: boolean = false
+  @observable sourceOptions: OptionValues[] = []
+  @observable sourceOptionsLoading: boolean = false
   @observable sourceSchema: Field[] = []
   @observable sourceSchemaLoading: boolean = false
 
   lastDestinationSchemaType: string = ''
+  lastSourceSchemaType: string = ''
 
   @action getConnectionInfoSchema(providerName: string): Promise<void> {
     this.connectionSchemaLoading = true
@@ -125,53 +128,62 @@ class ProviderStore {
     })
   }
 
-  @action loadSourceSchema(providerName: string, isReplica: boolean): Promise<void> {
+  @action loadSourceSchema(providerName: string, schemaType: string): Promise<void> {
     this.sourceSchemaLoading = true
+    this.lastSourceSchemaType = schemaType
 
-    return ProviderSource.loadSourceSchema(providerName, isReplica).then((fields: Field[]) => {
+    return ProviderSource.loadSourceSchema(providerName, schemaType).then((fields: Field[]) => {
       this.sourceSchemaLoading = false
       this.sourceSchema = fields
-    }).catch(() => { this.sourceSchemaLoading = false })
+    }).catch(err => {
+      this.sourceSchemaLoading = false
+      throw err
+    })
   }
 
-  cache: { key: string, data: DestinationOption[] }[] = []
-
-  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }, useCache?: boolean): Promise<DestinationOption[]> {
-    let providerWithExtraOptions = configLoader.config.providersWithExtraOptions
-      .find(p => typeof p === 'string' ? p === provider : p.name === provider)
+  cache: { key: string, data: OptionValues[] }[] = []
+
+  @action getOptionsValues(config: {
+    optionsType: 'source' | 'destination',
+    endpointId: string,
+    provider: string,
+    envData?: { [string]: mixed },
+    useCache?: boolean,
+  }): Promise<OptionValues[]> {
+    let { provider, optionsType, endpointId, envData, useCache } = config
+    let providers = optionsType === 'source' ?
+      configLoader.config.sourceProvidersWithExtraOptions :
+      configLoader.config.destinationProvidersWithExtraOptions
+    let providerWithExtraOptions = providers.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 key = `${endpointId}-${provider}-${optionsType}-${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
+        this.getOptionsValuesSuccess(optionsType, provider, cacheItem.data)
+        this.getOptionsValuesDone(optionsType)
         return Promise.resolve(cacheItem.data)
       }
     }
 
-    this.destinationOptionsLoading = true
-    this.destinationOptions = []
-    let destOptions = []
+    if (optionsType === 'source') {
+      this.sourceOptionsLoading = true
+      this.sourceOptions = []
+    } else {
+      this.destinationOptionsLoading = true
+      this.destinationOptions = []
+    }
 
-    return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
-      this.destinationSchema.forEach(field => {
-        const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
-        parser.fillFieldValues(field, options)
-      })
-      this.destinationOptions = options
-      destOptions = options
-      this.destinationOptionsLoading = false
+    let optionsValues = []
 
+    return ProviderSource.getOptionsValues(optionsType, endpointId, envData).then(options => {
+      this.getOptionsValuesSuccess(optionsType, provider, options)
+      optionsValues = options
       if (useCache) {
-        let key = `${endpointId}-${provider}-${JSON.stringify(envData)}`
+        let key = `${endpointId}-${provider}-${optionsType}-${JSON.stringify(envData)}`
         if (this.cache.length > 20) {
           this.cache.splice(0)
         }
@@ -179,16 +191,39 @@ class ProviderStore {
       }
     }).catch(err => {
       console.error(err)
-      if (envData) {
-        return this.loadDestinationSchema(provider, this.lastDestinationSchemaType).then(() => {
-          return this.getDestinationOptions(endpointId, provider)
-        })
+      if (optionsType === 'source') {
+        return this.loadSourceSchema(provider, this.lastSourceSchemaType)
+          .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
       }
       return this.loadDestinationSchema(provider, this.lastDestinationSchemaType)
+        .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
     }).then(() => {
+      this.getOptionsValuesDone(optionsType)
+      return optionsValues
+    })
+  }
+
+  @action getOptionsValuesDone(optionsType: 'source' | 'destination') {
+    if (optionsType === 'source') {
+      this.sourceOptionsLoading = false
+    } else {
       this.destinationOptionsLoading = false
-      return destOptions
+    }
+  }
+
+  @action getOptionsValuesSuccess(optionsType: 'source' | 'destination', provider: string, options: OptionValues[]) {
+    let schema = optionsType === 'source' ? this.sourceSchema : this.destinationSchema
+    schema.forEach(field => {
+      const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
+      parser.fillFieldValues(field, options)
     })
+    if (optionsType === 'source') {
+      this.sourceSchema = [...schema]
+      this.sourceOptions = options
+    } else {
+      this.destinationSchema = [...schema]
+      this.destinationOptions = options
+    }
   }
 }
 

+ 2 - 1
src/types/Config.js

@@ -9,5 +9,6 @@ export type Config = {
   requestPollTimeout: number,
   sourceOptionsProviders: string[],
   instancesListBackgroundLoading: { default: number, [string]: number },
-  providersWithExtraOptions: Array<string | { name: string, envRequiredFields: string[] }>,
+  sourceProvidersWithExtraOptions: Array<string | { name: string, envRequiredFields: string[] }>,
+  destinationProvidersWithExtraOptions: Array<string | { name: string, envRequiredFields: string[] }>,
 }

+ 1 - 1
src/types/Endpoint.js

@@ -34,7 +34,7 @@ export type Endpoint = {
   },
 }
 
-export type DestinationOption = {
+export type OptionValues = {
   name: string,
   // $FlowIssue
   values: string[] | { name: string, id: string, [string]: mixed }[],