Browse Source

Merge pull request #234 from smiclea/options-schema-plugin

Use a plugin system for wizard options behaviour
Dorin Paslaru 8 năm trước cách đây
mục cha
commit
b99b6ae510

+ 37 - 6
src/components/molecules/Dropdown/index.jsx

@@ -16,7 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
 import ReactDOM from 'react-dom'
 
 import DropdownButton from '../../atoms/DropdownButton'
@@ -70,9 +70,10 @@ const Tip = styled.div`
 `
 const ListItem = styled.div`
   position: relative;
-  color: ${Palette.grayscale[4]};
+  color: ${props => props.selected ? 'white' : props.dim ? Palette.grayscale[3] : Palette.grayscale[4]};
+  ${props => props.selected ? css`background: ${Palette.primary};` : ''}
+  ${props => props.selected ? css`font-weight: ${StyleProps.fontWeights.medium};` : ''}
   padding: 8px 16px;
-  ${props => props.selected ? `font-weight: ${StyleProps.fontWeights.medium};` : ''}
   transition: all ${StyleProps.animations.swift};
 
   &:first-child {
@@ -99,6 +100,12 @@ const DuplicatedLabel = styled.div`
     overflow: hidden;
   }
 `
+const Separator = styled.div`
+  width: calc(100% - 32px);
+  height: 1px;
+  margin: 8px 16px;
+  background: ${Palette.grayscale[3]};
+`
 
 type Props = {
   selectedItem: any,
@@ -114,6 +121,7 @@ type Props = {
   'data-test-id'?: string,
   embedded?: boolean,
   required?: boolean,
+  dimFirstItem?: boolean,
 }
 type State = {
   showDropdownList: boolean,
@@ -127,6 +135,7 @@ class Dropdown extends React.Component<Props, State> {
 
   buttonRef: HTMLElement
   listRef: HTMLElement
+  listItemsRef: HTMLElement
   tipRef: HTMLElement
   scrollableParent: HTMLElement
   buttonRect: ClientRect
@@ -213,7 +222,9 @@ class Dropdown extends React.Component<Props, State> {
       return
     }
 
-    this.setState({ showDropdownList: !this.state.showDropdownList })
+    this.setState({ showDropdownList: !this.state.showDropdownList }, () => {
+      this.scrollIntoView()
+    })
   }
 
   handleItemClick(item: any) {
@@ -259,8 +270,23 @@ class Dropdown extends React.Component<Props, State> {
       scrollOffset = -parseInt(document.body && document.body.style.top, 10)
     }
 
+    let widthDiff = this.listRef.offsetWidth - this.buttonRef.offsetWidth
     this.listRef.style.top = `${listTop + (window.pageYOffset || scrollOffset)}px`
-    this.listRef.style.left = `${this.buttonRect.left + window.pageXOffset}px`
+    this.listRef.style.left = `${(this.buttonRect.left + window.pageXOffset) - widthDiff}px`
+  }
+
+  scrollIntoView() {
+    if (!this.listRef || !this.listItemsRef) {
+      return
+    }
+
+    let itemIndex = this.props.items.findIndex(i => this.getValue(i) === this.getValue(this.props.selectedItem))
+    if (itemIndex === -1 || !this.listItemsRef.children[itemIndex]) {
+      return
+    }
+
+    // $FlowIssue
+    this.listItemsRef.children[itemIndex].parentNode.scrollTop = this.listItemsRef.children[itemIndex].offsetTop - this.listItemsRef.children[itemIndex].parentNode.offsetTop - 32
   }
 
   renderList() {
@@ -283,8 +309,12 @@ class Dropdown extends React.Component<Props, State> {
     let list = ReactDOM.createPortal((
       <List {...this.props} innerRef={ref => { this.listRef = ref }}>
         <Tip innerRef={ref => { this.tipRef = ref }} primary={this.state.firstItemHover} />
-        <ListItems>
+        <ListItems innerRef={ref => { this.listItemsRef = ref }}>
           {this.props.items.map((item, i) => {
+            if (item.separator === true) {
+              return <Separator />
+            }
+
             let label = this.getLabel(item)
             let value = this.getValue(item)
             let duplicatedLabel = duplicatedLabels.find(l => l === label)
@@ -298,6 +328,7 @@ class Dropdown extends React.Component<Props, State> {
                 onMouseLeave={() => { this.handleItemMouseLeave(i) }}
                 onClick={() => { this.handleItemClick(item) }}
                 selected={value === selectedValue}
+                dim={this.props.dimFirstItem && i === 0}
               >
                 {label}
                 {duplicatedLabel ? <DuplicatedLabel> (<span>{value || ''}</span>)</DuplicatedLabel> : ''}

+ 4 - 0
src/components/molecules/Dropdown/story.jsx

@@ -54,6 +54,9 @@ storiesOf('Dropdown', module)
   .add('disabled', () => (
     <Wrapper disabled />
   ))
+  .add('required', () => (
+    <Wrapper required />
+  ))
   .add('long list', () => (
     <Wrapper
       items={[
@@ -61,6 +64,7 @@ storiesOf('Dropdown', module)
         { label: 'Item 2', value: 'item-2' },
         { label: 'Item 3', value: 'item-3' },
         { label: 'Item 4', value: 'item-4' },
+        { separator: true },
         { label: 'Item 1', value: 'item-1' },
         { label: 'Item 2', value: 'item-2' },
         { label: 'Item 3', value: 'item-3' },

+ 6 - 2
src/components/molecules/DropdownLink/story.jsx

@@ -12,11 +12,13 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+// @flow
+
 import React from 'react'
 import { storiesOf } from '@storybook/react'
 import DropdownLink from '.'
 
-class Wrapper extends React.Component {
+class Wrapper extends React.Component<any, any> {
   constructor() {
     super()
     this.state = {
@@ -52,5 +54,7 @@ storiesOf('DropdownLink', module)
     <Wrapper />
   ))
   .add('searchable', () => (
-    <Wrapper searchable />
+    <Wrapper
+      searchable
+    />
   ))

+ 3 - 1
src/components/molecules/PropertiesTable/index.jsx

@@ -110,6 +110,8 @@ class PropertiesTable extends React.Component<Props> {
           label: this.getName(e),
           value: e,
         }
+      } else if (e.separator === true) {
+        return e
       }
       return {
         label: e.name,
@@ -130,7 +132,7 @@ class PropertiesTable extends React.Component<Props> {
         data-test-id={`${baseId}-dropdown-${prop.name}`}
         width={320}
         noSelectionMessage="Choose a value"
-        selectedItem={selectedItem ? selectedItem.label : null}
+        selectedItem={selectedItem}
         items={items}
         onChange={item => this.props.onChange(prop, item.value)}
       />

+ 22 - 0
src/components/molecules/WizardOptionsField/images/asterisk.svg

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon-Info Copy 8</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
+        <g id="Wizard/05-Options/2-Search-Dropdown_Erorr" transform="translate(-621.000000, -586.000000)" stroke="#0044CA" stroke-width="1.5">
+            <g id="Forms/Items/Input-Copy" transform="translate(528.000000, 583.000000)">
+                <g id="Group">
+                    <g id="Icon/Question/16" transform="translate(91.000000, 1.000000)">
+                        <g id="Icon/Asterisk/Blue" transform="translate(2.000000, 2.000000)">
+                            <path d="M6,1 L6,11" id="Line"></path>
+                            <path d="M1.66987298,3.5 L10.330127,8.5" id="Line"></path>
+                            <path d="M1.66987298,3.5 L10.330127,8.5" id="Line" transform="translate(6.000000, 6.000000) scale(-1, 1) translate(-6.000000, -6.000000) "></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 20 - 10
src/components/molecules/WizardOptionsField/index.jsx

@@ -29,6 +29,8 @@ import LabelDictionary from '../../../utils/LabelDictionary'
 
 import type { Field } from '../../../types/Field'
 
+import asteriskImage from './images/asterisk.svg'
+
 const getDirection = props => {
   if (props.type === 'strict-boolean' || props.type === 'boolean') {
     return 'row'
@@ -48,6 +50,13 @@ const Label = styled.div`
 const LabelText = styled.span`
   margin-right: 24px;
 `
+const Asterisk = styled.div`
+  ${StyleProps.exactSize('16px')}
+  display: inline-block;
+  background: url('${asteriskImage}') center no-repeat;
+  margin-bottom: -3px;
+  margin-left: ${props => props.marginLeft || '0px'};
+`
 
 type Props = {
   type: 'replica' | 'migration',
@@ -83,7 +92,6 @@ class WizardOptionsField extends React.Component<Props> {
     return (
       <TextInput
         width={`${StyleProps.inputSizes.wizard.width}px`}
-        required={this.props.required}
         value={this.props.value}
         onChange={e => { this.props.onChange(e.target.value) }}
         placeholder={LabelDictionary.get(this.props.name)}
@@ -93,7 +101,7 @@ class WizardOptionsField extends React.Component<Props> {
   }
 
   renderObjectTable() {
-    if (!this.props.properties) {
+    if (!this.props.properties || !this.props.properties.length) {
       return null
     }
 
@@ -109,6 +117,10 @@ class WizardOptionsField extends React.Component<Props> {
 
   renderEnumDropdown() {
     let items = this.props.enum.map(e => {
+      if (typeof e !== 'string' && e.separator === true) {
+        return e
+      }
+
       return {
         label: typeof e === 'string' ? e : e.name,
         value: typeof e === 'string' ? e : e.id,
@@ -131,8 +143,8 @@ class WizardOptionsField extends React.Component<Props> {
         noSelectionMessage="Choose a value"
         selectedItem={selectedItem}
         items={items}
+        dimFirstItem
         onChange={item => this.props.onChange(item.value)}
-        required={this.props.required}
       />
     )
   }
@@ -164,15 +176,13 @@ class WizardOptionsField extends React.Component<Props> {
 
   renderLabel() {
     let description = LabelDictionary.getDescription(this.props.name)
-    let infoIcon = null
-    if (description) {
-      infoIcon = <InfoIcon text={description} marginLeft={-20} />
-    }
-
     return (
       <Label>
-        <LabelText data-test-id="wOptionsField-label">{LabelDictionary.get(this.props.name)}</LabelText>
-        {infoIcon}
+        <LabelText data-test-id="wOptionsField-label">
+          {LabelDictionary.get(this.props.name)}
+        </LabelText>
+        {description ? <InfoIcon text={description} marginLeft={-20} /> : null}
+        {this.props.required ? <Asterisk marginLeft={description ? '4px' : '-16px'} /> : null}
       </Label>
     )
   }

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

@@ -207,7 +207,7 @@ class WizardPageContent extends React.Component<Props, State> {
     if (schema && schema.length > 0) {
       let required = schema.filter(f => f.required && f.type !== 'object')
       schema.forEach(f => {
-        if (f.type === 'object' && f.properties && f.properties.filter(p => isValid(p)).length > 0) {
+        if (f.type === 'object' && f.properties && f.properties.length && f.properties.filter(p => isValid(p)).length > 0) {
           required = required.concat(f.properties.filter(p => p.required))
         }
       })

+ 40 - 20
src/components/pages/WizardPage/index.jsx

@@ -239,21 +239,7 @@ class WizardPage extends React.Component<Props, State> {
   handleOptionsChange(field: Field, value: any) {
     wizardStore.updateData({ networks: null })
     wizardStore.updateOptions({ field, value })
-
-    let provider = wizardStore.data.target && wizardStore.data.target.type
-    let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p !== 'string' && p.name === provider)
-    if (provider && providerWithExtraOptions && typeof providerWithExtraOptions !== 'string' && providerWithExtraOptions.envRequestFields) {
-      let validFields = providerWithExtraOptions.envRequestFields.filter(fn => wizardStore.data.options ? O.isValid(wizardStore.data.options[fn]) : false)
-      if (
-        validFields.length === providerWithExtraOptions.envRequestFields.length &&
-        wizardStore.data.options &&
-        wizardStore.data.target &&
-        validFields.find(fn => fn === field.name)
-      ) {
-        providerStore.getDestinationOptions(wizardStore.data.target.id, provider, wizardStore.data.options)
-      }
-    }
-
+    this.loadEnvDestinationOptions(field)
     wizardStore.setPermalink(wizardStore.data)
   }
 
@@ -277,6 +263,37 @@ class WizardPage extends React.Component<Props, State> {
     wizardStore.setPermalink(wizardStore.data)
   }
 
+  loadEnvDestinationOptions(field?: Field) {
+    let provider = wizardStore.data.target && wizardStore.data.target.type
+    let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p !== 'string' && p.name === provider)
+    if (provider && providerWithExtraOptions && typeof providerWithExtraOptions !== 'string' && providerWithExtraOptions.envRequiredFields) {
+      let findFieldInSchema = (name: string) => providerStore.optionsSchema.find(f => f.name === name)
+      let validFields = providerWithExtraOptions.envRequiredFields.filter(fn => {
+        let schemaField = findFieldInSchema(fn)
+        return wizardStore.data.options ? O.isValid(wizardStore.data.options[fn]) || (schemaField && schemaField.default) : false
+      })
+      let currentFieldValied = field ? validFields.find(fn => field ? fn === field.name : false) : true
+      if (
+        validFields.length === providerWithExtraOptions.envRequiredFields.length &&
+        currentFieldValied
+      ) {
+        let envData = {}
+        validFields.forEach(fn => {
+          envData[fn] = wizardStore.data.options ? wizardStore.data.options[fn] : null
+          if (!O.isValid(envData[fn])) {
+            let schemaField = findFieldInSchema(fn)
+            if (schemaField && schemaField.default) {
+              envData[fn] = schemaField.default
+            }
+          }
+        })
+        if (wizardStore.data.target) {
+          providerStore.getDestinationOptions(wizardStore.data.target.id, provider, envData)
+        }
+      }
+    }
+  }
+
   loadDataForPage(page: WizardPageType) {
     switch (page.id) {
       case 'source': {
@@ -290,14 +307,17 @@ class WizardPage extends React.Component<Props, State> {
         break
       }
       case 'target': {
-        // Preload destination options if data is set from 'Permalink'
         let target = wizardStore.data.target
-        if (providerStore.destinationOptions.length === 0 && target) {
-          providerStore.getDestinationOptions(target.id, target.type)
-        }
         // Preload destination options schema
         if (providerStore.optionsSchema.length === 0 && target) {
-          providerStore.loadOptionsSchema(target.type, this.state.type)
+          providerStore.loadOptionsSchema(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(() => {
+                this.loadEnvDestinationOptions()
+              })
+            }
+          })
         }
         break
       }

+ 8 - 4
src/config.js

@@ -81,14 +81,18 @@ export const wizardConfig = {
   instancesItemsPerPage: 6,
 }
 
-// Providers for which `destination-options` API call(s) will be made
-// If item is an object and has `envRequestFields` array,
-// subsequent requests to `destination-options` will be made with those fields, if they are set
+// A list of providers for which `destination-options` API call(s) will be made in the Wizard
+// 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
 export const providersWithExtraOptions = [
   'openstack',
+  {
+    name: 'azure',
+    envRequiredFields: ['location', 'resource_group'],
+  },
   {
     name: 'oci',
-    envRequestFields: ['compartment', 'availability_domain'],
+    envRequiredFields: ['compartment', 'availability_domain'],
   },
 ]
 

+ 1 - 1
src/plugins/endpoint/azure/SchemaPlugin.js → src/plugins/endpoint/azure/ConnectionSchemaPlugin.js

@@ -17,7 +17,7 @@ import {
   defaultSchemaToFields,
   generateField,
   fieldsToPayload,
-} from '../default/SchemaPlugin'
+} from '../default/ConnectionSchemaPlugin'
 
 const fieldsToPayloadUseDefaults = (data, schema) => {
   let info = {}

+ 0 - 0
src/plugins/endpoint/default/SchemaPlugin.js → src/plugins/endpoint/default/ConnectionSchemaPlugin.js


+ 135 - 0
src/plugins/endpoint/default/OptionsSchemaPlugin.js

@@ -0,0 +1,135 @@
+/*
+Copyright (C) 2017  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// @flow
+
+import type { Field } from '../../../types/Field'
+import type { DestinationOption } from '../../../types/Endpoint'
+import type { WizardData } from '../../../types/WizardData'
+import { executionOptions } from '../../../config'
+
+const migrationImageOsTypes = ['windows', 'linux']
+
+export const defaultFillFieldValues = (field: Field, option: DestinationOption) => {
+  if (field.type === 'string') {
+    // $FlowIgnore
+    field.enum = [...option.values]
+    if (option.config_default) {
+      field.default = typeof option.config_default === 'string' ? option.config_default : option.config_default.id
+    }
+  }
+}
+
+export const defaultFillMigrationImageMapValues = (field: Field, option: DestinationOption): boolean => {
+  if (field.name === 'migr_image_map') {
+    field.properties = migrationImageOsTypes.map(os => {
+      let values = option.values
+        .filter(v => v.os_type === os || v.os_type === 'unknown')
+        .sort((v1, v2) => {
+          if (v1.os_type === 'unknown' && v2.os_type !== 'unknown') {
+            return 1
+          } else if (v1.os_type !== 'unknown' && v2.os_type === 'unknown') {
+            return -1
+          }
+          return 0
+        })
+      let unknownIndex = values.findIndex(v => v.os_type === 'unknown')
+      if (unknownIndex > -1 && values.filter(v => v.os_type === 'unknown').length < values.length) {
+        values.splice(unknownIndex, 0, { separator: true })
+      }
+
+      return {
+        name: `${os}_os_image`,
+        type: 'string',
+        enum: values,
+      }
+    })
+    return true
+  }
+  return false
+}
+
+export const defaultGetDestinationEnv = (data: WizardData): any => {
+  let env = {}
+  let specialOptions = ['execute_now', 'separate_vm', 'skip_os_morphing']
+    .concat(executionOptions.map(o => o.name))
+    .concat(migrationImageOsTypes.map(o => `${o}_os_image`))
+
+
+  if (data.options) {
+    Object.keys(data.options).forEach(optionName => {
+      if (specialOptions.find(o => o === optionName)
+        // $FlowIssue
+        || data.options[optionName] === null || data.options[optionName] === undefined) {
+        return
+      }
+      if (optionName.indexOf('/') > 0) {
+        let parentName = optionName.substr(0, optionName.lastIndexOf('/'))
+        if (!env[parentName]) {
+          env[parentName] = {}
+        }
+        env[parentName][optionName.substr(optionName.lastIndexOf('/') + 1)] = data.options ? data.options[optionName] : null
+      } else {
+        env[optionName] = data.options ? data.options[optionName] : null
+      }
+    })
+  }
+}
+
+export const defaultGetNetworkMap = (data: WizardData) => {
+  let env = {}
+  env.network_map = {}
+  if (data.networks && data.networks.length) {
+    data.networks.forEach(mapping => {
+      env.network_map[mapping.sourceNic.network_name] = mapping.targetNetwork.id
+    })
+  }
+  return env
+}
+
+export const defaultGetMigrationImageMap = (data: WizardData) => {
+  let env = {}
+  env.migr_image_map = {}
+  if (data.options) {
+    migrationImageOsTypes.forEach(os => {
+      if (data.options && data.options[`${os}_os_image`]) {
+        env.migr_image_map[os] = data.options[`${os}_os_image`]
+      }
+    })
+  }
+
+  return env
+}
+
+export default class OptionsSchemaParser {
+  static fillFieldValues(field: Field, options: DestinationOption[]) {
+    let option = options.find(f => f.name === field.name)
+    if (!option) {
+      return
+    }
+    if (!defaultFillMigrationImageMapValues(field, option)) {
+      defaultFillFieldValues(field, option)
+    }
+  }
+
+  static getDestinationEnv(data: WizardData) {
+    let env = {
+      ...defaultGetDestinationEnv(data),
+      ...defaultGetNetworkMap(data),
+      ...defaultGetMigrationImageMap(data),
+    }
+    return env
+  }
+}
+

+ 15 - 9
src/plugins/endpoint/index.js

@@ -14,19 +14,25 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import DefaultSchemaPlugin from './default/SchemaPlugin'
-import AzureSchemaPlugin from './azure/SchemaPlugin'
+import DefaultConnectionSchemaPlugin from './default/ConnectionSchemaPlugin'
+import AzureConnectionSchemaPlugin from './azure/ConnectionSchemaPlugin'
+import OpenstackConnectionSchemaPlugin from './openstack/ConnectionSchemaPlugin'
+import OciConnectionSchemaPlugin from './oci/ConnectionSchemaPlugin'
 import DefaultContentPlugin from './default/ContentPlugin'
 import AzureContentPlugin from './azure/ContentPlugin'
 import OpenstackContentPlugin from './openstack/ContentPlugin'
-import OpenstackSchemaPlugin from './openstack/SchemaPlugin'
-import OciSchemaPlugin from './oci/SchemaPlugin'
 
-export const SchemaPlugin = {
-  default: DefaultSchemaPlugin,
-  azure: AzureSchemaPlugin,
-  openstack: OpenstackSchemaPlugin,
-  oci: OciSchemaPlugin,
+import DefaultOptionsSchemaPlugin from './default/OptionsSchemaPlugin'
+
+export const ConnectionSchemaPlugin = {
+  default: DefaultConnectionSchemaPlugin,
+  azure: AzureConnectionSchemaPlugin,
+  openstack: OpenstackConnectionSchemaPlugin,
+  oci: OciConnectionSchemaPlugin,
+}
+
+export const OptionsSchemaPlugin = {
+  default: DefaultOptionsSchemaPlugin,
 }
 
 export const ContentPlugin = {

+ 1 - 1
src/plugins/endpoint/oci/SchemaPlugin.js → src/plugins/endpoint/oci/ConnectionSchemaPlugin.js

@@ -17,7 +17,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import type { Schema } from '../../../types/Schema'
 import type { Field } from '../../../types/Field'
 
-import DefaultConnectionSchemaParser from '../default/SchemaPlugin'
+import DefaultConnectionSchemaParser from '../default/ConnectionSchemaPlugin'
 
 export default class ConnectionSchemaParser {
   static parseSchemaToFields(schema: Schema): Field[] {

+ 1 - 1
src/plugins/endpoint/openstack/SchemaPlugin.js → src/plugins/endpoint/openstack/ConnectionSchemaPlugin.js

@@ -17,7 +17,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import type { Schema } from '../../../types/Schema'
 import type { Field } from '../../../types/Field'
 
-import DefaultConnectionSchemaParser from '../default/SchemaPlugin'
+import DefaultConnectionSchemaParser from '../default/ConnectionSchemaPlugin'
 
 const customSort = (fields: Field[]) => {
   const sortPriority = {

+ 4 - 4
src/sources/Schemas.js

@@ -14,8 +14,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import { SchemaPlugin } from '../plugins/endpoint'
-import { defaultSchemaToFields } from '../plugins/endpoint/default/SchemaPlugin'
+import { ConnectionSchemaPlugin } from '../plugins/endpoint'
+import { defaultSchemaToFields } from '../plugins/endpoint/default/ConnectionSchemaPlugin'
 import type { Schema } from '../types/Schema'
 
 class SchemaParser {
@@ -26,7 +26,7 @@ class SchemaParser {
       this.storedConnectionsSchemas[provider] = schema
     }
 
-    let parsers = SchemaPlugin[provider] || SchemaPlugin.default
+    let parsers = ConnectionSchemaPlugin[provider] || ConnectionSchemaPlugin.default
     let fields = parsers.parseSchemaToFields(schema)
 
     return fields
@@ -50,7 +50,7 @@ class SchemaParser {
 
   static fieldsToPayload(data: { [string]: any }) {
     let storedSchema = this.storedConnectionsSchemas[data.type] || this.storedConnectionsSchemas.general
-    let parsers = SchemaPlugin[data.type] || SchemaPlugin.default
+    let parsers = ConnectionSchemaPlugin[data.type] || ConnectionSchemaPlugin.default
     let payload = parsers.parseFieldsToPayload(data, storedSchema)
 
     return payload

+ 5 - 44
src/sources/WizardSource.js

@@ -18,62 +18,23 @@ import cookie from 'js-cookie'
 
 import Api from '../utils/ApiCaller'
 import notificationStore from '../stores/NotificationStore'
-import { servicesUrl, executionOptions } from '../config'
+import { OptionsSchemaPlugin } from '../plugins/endpoint'
+
+import { servicesUrl } from '../config'
 import type { WizardData } from '../types/WizardData'
 import type { MainItem } from '../types/MainItem'
 
-class WizardSourceUtils {
-  static getDestinationEnv(data) {
-    let env = {}
-    let specialOptions = ['execute_now', 'separate_vm', 'skip_os_morphing', 'windows_image', 'linux_image'].concat(executionOptions.map(o => o.name))
-
-    if (data.options) {
-      Object.keys(data.options).forEach(optionName => {
-        if (specialOptions.find(o => o === optionName)
-          // $FlowIssue
-          || data.options[optionName] === null || data.options[optionName] === undefined) {
-          return
-        }
-        if (optionName.indexOf('/') > 0) {
-          let parentName = optionName.substr(0, optionName.lastIndexOf('/'))
-          if (!env[parentName]) {
-            env[parentName] = {}
-          }
-          env[parentName][optionName.substr(optionName.lastIndexOf('/') + 1)] = data.options ? data.options[optionName] : null
-        } else {
-          env[optionName] = data.options ? data.options[optionName] : null
-        }
-      })
-    }
-
-    env.network_map = {}
-    if (data.networks && data.networks.length) {
-      data.networks.forEach(mapping => {
-        env.network_map[mapping.sourceNic.network_name] = mapping.targetNetwork.id
-      })
-    }
-    env.migr_image_map = {}
-    if (data.options && data.options.windows_image) {
-      env.migr_image_map.windows = data.options.windows_image
-    }
-    if (data.options && data.options.linux_image) {
-      env.migr_image_map.linux = data.options.linux_image
-    }
-
-    return env
-  }
-}
-
 class WizardSource {
   static create(type: string, data: WizardData): Promise<MainItem> {
     return new Promise((resolve, reject) => {
       let projectId = cookie.get('projectId')
 
+      const parser = data.target ? OptionsSchemaPlugin[data.target.type] || OptionsSchemaPlugin.default : OptionsSchemaPlugin.default
       let payload = {}
       payload[type] = {
         origin_endpoint_id: data.source ? data.source.id : 'null',
         destination_endpoint_id: data.target ? data.target.id : 'null',
-        destination_environment: WizardSourceUtils.getDestinationEnv(data),
+        destination_environment: parser.getDestinationEnv(data),
         instances: data.selectedInstances ? data.selectedInstances.map(i => i.instance_name) : 'null',
         notes: '',
         security_groups: ['testgroup'],

+ 5 - 25
src/stores/ProviderStore.js

@@ -18,6 +18,7 @@ import { observable, action } from 'mobx'
 
 import ProviderSource from '../sources/ProviderSource'
 import { providersWithExtraOptions } from '../config.js'
+import { OptionsSchemaPlugin } from '../plugins/endpoint'
 import type { DestinationOption } from '../types/Endpoint'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
@@ -82,34 +83,13 @@ class ProviderStore {
     this.destinationOptionsLoading = true
     return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
       this.optionsSchema.forEach(field => {
-        let fieldValues = options.find(f => f.name === field.name)
-        if (fieldValues) {
-          if (field.type === 'string') {
-            // $FlowIgnore
-            field.enum = [...fieldValues.values]
-            if (fieldValues.config_default) {
-              field.default = typeof fieldValues.config_default === 'string' ? fieldValues.config_default : fieldValues.config_default.id
-            }
-            // the `migr_image_map` field is special since it needs to group the values by OS type
-          } else if (field.name === 'migr_image_map') {
-            field.properties = [
-              {
-                name: 'windows_image',
-                type: 'string',
-                enum: fieldValues.values.filter(v => typeof v !== 'string' && v.os_type === 'windows'),
-              },
-              {
-                name: 'linux_image',
-                type: 'string',
-                enum: fieldValues.values.filter(v => typeof v !== 'string' && v.os_type === 'linux'),
-              },
-            ]
-          }
-        }
+        const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
+        parser.fillFieldValues(field, options)
       })
       this.destinationOptions = options
       this.destinationOptionsLoading = false
-    }).catch(() => {
+    }).catch(err => {
+      console.error(err)
       if (envData) {
         return this.loadOptionsSchema(provider, this.lastOptionsSchemaType).then(() => {
           return this.getDestinationOptions(endpointId, provider)