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

Update Openstack source and conn. info schemas

Includes support for an updated source schema containing replica export
mechanism options and its associated sub-options.

Includes support for an updated connection info schema containing Ceph
platform options.
Sergiu Miclea 6 лет назад
Родитель
Сommit
a99c7690eb

+ 5 - 0
config.js

@@ -40,6 +40,11 @@ const conf: Config = {
 
   // The providers for which an extra `source options` or `destination options` call can be made with a set of field values
   extraOptionsApiCalls: [
+    {
+      name: 'openstack',
+      types: ['source'],
+      requiredFields: ['replica_export_mechanism'],
+    },
     {
       name: 'azure',
       types: ['source', 'destination'],

+ 1 - 1
src/components/molecules/FieldInput/FieldInput.jsx

@@ -200,7 +200,7 @@ class FieldInput extends React.Component<Props> {
         style={{ width: '100%' }}
         highlight={this.props.highlight}
         value={this.props.value}
-        onChange={e => { console.log('changing', e); if (this.props.onChange) this.props.onChange(e.target.value) }}
+        onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
         placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
         disabledLoading={this.props.disabledLoading}

+ 9 - 0
src/components/organisms/EditReplica/EditReplica.jsx

@@ -286,6 +286,15 @@ class EditReplica extends React.Component<Props, State> {
       data[field.name] = value
     }
 
+    if (field.enum && field.subFields) {
+      field.subFields.forEach(subField => {
+        let subFieldKeys = Object.keys(data).filter(k => k.indexOf(`${subField.name}/`) > -1)
+        subFieldKeys.forEach(k => {
+          delete data[k]
+        })
+      })
+    }
+
     let handleStateUpdate = () => {
       if (field.type !== 'string' || field.enum) {
         this.loadExtraOptions(field, type)

+ 6 - 2
src/components/organisms/Endpoint/Endpoint.jsx

@@ -168,12 +168,13 @@ class Endpoint extends React.Component<Props, State> {
     }
 
     if (props.endpoint && endpointStore.connectionInfo) {
+      let plugin: any = ContentPlugin[props.endpoint.type] || ContentPlugin.default
       this.setState({
         isNew: this.props.isNewEndpoint ? (this.state.isNew === null || this.state.isNew) : this.state.isNew,
         endpoint: {
           ...this.state.endpoint,
-          ...ObjectUtils.flatten(props.endpoint || {}),
-          ...ObjectUtils.flatten(endpointStore.connectionInfo || {}),
+          ...ObjectUtils.flatten(props.endpoint || {}, plugin.REQUIRES_PARENT_OBJECT_PATH),
+          ...ObjectUtils.flatten(endpointStore.connectionInfo || {}, plugin.REQUIRES_PARENT_OBJECT_PATH),
         },
       })
     } else {
@@ -218,6 +219,9 @@ class Endpoint extends React.Component<Props, State> {
       return field.default
     }
 
+    if (field.type === 'integer') {
+      return null
+    }
     return ''
   }
 

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

@@ -197,14 +197,33 @@ class WizardOptions extends React.Component<Props> {
   }
 
   generateGroups(fields: FieldRender[]) {
+    let groups: Array<{ fields: FieldRender[], name?: string }> = [{ fields }]
+
     let workerFields = fields.filter(f => f.field.name.indexOf('migr_') === 0)
     if (workerFields.length > 1) {
-      return [
+      groups = [
         { fields: fields.filter(f => f.field.name.indexOf('migr_') === -1) },
         { name: 'Temporary Migration Worker Options', fields: workerFields.map((f, i) => ({ ...f, column: i % 2 })) },
       ]
     }
-    return [{ fields }]
+
+    fields.forEach(f => {
+      if (f.field.groupName) {
+        groups[0].fields = groups[0].fields ? groups[0].fields.filter(gf => gf.field.name !== f.field.name) : []
+
+        let group = groups.find(g => g.name && g.name === f.field.groupName)
+        if (!group) {
+          groups.push({
+            name: f.field.groupName,
+            fields: [f],
+          })
+        } else {
+          group.fields.push(f)
+        }
+      }
+    })
+
+    return groups
   }
 
   renderOptionsField(field: Field) {
@@ -255,6 +274,23 @@ class WizardOptions extends React.Component<Props> {
       fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => !f.required))
     }
 
+    // Add subfields for enums which have them
+    let subFields = []
+    fieldsSchema.forEach(f => {
+      if (!f.enum || !f.subFields) {
+        return
+      }
+      let value = this.getFieldValue(f.name, f.default)
+      if (!f.subFields) {
+        return
+      }
+      let subField = f.subFields.find(f => f.name === `${String(value)}_options`)
+      if (subField && subField.properties) {
+        subFields = [...subFields, ...subField.properties]
+      }
+    })
+    fieldsSchema = [...fieldsSchema, ...subFields]
+
     let executeNowColumn
     let fields: FieldRender[] = fieldsSchema.filter(f => shouldRenderField(f)).map((field, i) => {
       let column: number = i % 2

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

@@ -136,6 +136,14 @@ export const isOptionsPageValid = (data: ?any, schema: Field[]) => {
       if (f.type === 'object' && f.properties && f.properties.filter && f.properties.filter(p => isValid(p)).length > 0) {
         required = required.concat(f.properties.filter(p => p.required))
       }
+
+      if (f.enum && f.subFields) {
+        let value = data && data[f.name]
+        let subField = f.subFields.find(f => f.name === `${String(value)}_options`)
+        if (subField && subField.properties) {
+          required = required.concat(subField.properties.filter(p => p.required))
+        }
+      }
     })
 
     let validFieldsCount = 0

+ 7 - 3
src/components/organisms/WizardStorage/WizardStorage.jsx

@@ -177,11 +177,15 @@ class WizardStorage extends React.Component<Props> {
       if (!name) {
         return [null, false]
       }
-      let paths = name.split('/')
-      if (paths.length < 4) {
+      let slashPaths = name.split('/')
+      let dashPaths = name.split('-')
+      if (slashPaths.length < 4 && dashPaths.length < 4) {
         return [name, false]
       }
-      return [`.../${paths.filter((_, i) => i > paths.length - 4).join('/')}`, true]
+      if (slashPaths.length >= 4) {
+        return [`.../${slashPaths.filter((_, i) => i > slashPaths.length - 4).join('/')}`, true]
+      }
+      return [`${dashPaths[0]}-...-${dashPaths[1]}`, true]
     }
 
 

+ 16 - 13
src/components/organisms/WizardSummary/WizardSummary.jsx

@@ -143,14 +143,14 @@ const Option = styled.div`
 `
 const OptionLabel = styled.div`
   color: ${Palette.grayscale[4]};
-  flex-grow: 1;
+  ${StyleProps.exactWidth('50%')}
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 `
 const OptionValue = styled.div`
   text-align: right;
-  max-width: 50%;
+  ${StyleProps.exactWidth('50%')}
   text-overflow: ellipsis;
   overflow: hidden;
 `
@@ -257,15 +257,16 @@ class WizardSummary extends React.Component<Props> {
             if (!data.sourceOptions || data.sourceOptions[optionName] == null || data.sourceOptions[optionName] === '') {
               return null
             }
-
+            let optionLabel = optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')
+            let optionValue = fieldHelper.getValueAlias(optionName, data.sourceOptions && data.sourceOptions[optionName], this.props.sourceSchema)
             return (
               <Option key={optionName}>
-                <OptionLabel>
-                  {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
+                <OptionLabel title={optionLabel}>
+                  {optionLabel}
                 </OptionLabel>
-                <OptionValue>{
-                  fieldHelper.getValueAlias(optionName, data.sourceOptions && data.sourceOptions[optionName], this.props.sourceSchema)
-                }</OptionValue>
+                <OptionValue title={optionValue}>
+                  {optionValue}
+                </OptionValue>
               </Option>
             )
           }) : null}
@@ -324,14 +325,16 @@ class WizardSummary extends React.Component<Props> {
               return null
             }
 
+            let optionLabel = optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')
+            let optionValue = fieldHelper.getValueAlias(optionName, data.destOptions && data.destOptions[optionName], this.props.destinationSchema)
             return (
               <Option key={optionName}>
-                <OptionLabel data-test-id={`wSummary-optionLabel-${optionName}`}>
-                  {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
+                <OptionLabel data-test-id={`wSummary-optionLabel-${optionName}`} title={optionLabel}>
+                  {optionLabel}
                 </OptionLabel>
-                <OptionValue data-test-id={`wSummary-optionValue-${optionName}`}>{
-                  fieldHelper.getValueAlias(optionName, data.destOptions && data.destOptions[optionName], this.props.destinationSchema)
-                }</OptionValue>
+                <OptionValue data-test-id={`wSummary-optionValue-${optionName}`} title={optionValue}>
+                  {optionValue}
+                </OptionValue>
               </Option>
             )
           }) : null}

+ 14 - 0
src/plugins/endpoint/default/ConnectionSchemaPlugin.js

@@ -98,6 +98,20 @@ export const fieldsToPayload = (data: { [string]: mixed }, schema: SchemaPropert
   Object.keys(schema.properties).forEach(fieldName => {
     if (data[fieldName] && typeof data[fieldName] !== 'object') {
       info[fieldName] = Utils.trim(fieldName, data[fieldName])
+    } else if (typeof schema.properties[fieldName] === 'object') {
+      // $FlowIgnore
+      let properties = schema.properties[fieldName] && schema.properties[fieldName].properties
+      if (properties) {
+        Object.keys(properties).forEach(fn => {
+          let fullFn = `${fieldName}/${fn}`
+          if (data[fullFn] != null) {
+            if (!info[fieldName]) {
+              info[fieldName] = {}
+            }
+            info[fieldName][fn] = Utils.trim(fn, data[fullFn])
+          }
+        })
+      }
     } else if (
       !data[fieldName] &&
       schema.required && schema.required.find(f => f === fieldName) &&

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

@@ -123,8 +123,8 @@ export default class OptionsSchemaParser {
     return defaultSchemaToFields(schema, schemaDefinitions)
   }
 
-  static fillFieldValues(field: Field, options: OptionValues[]) {
-    let option = options.find(f => f.name === field.name)
+  static fillFieldValues(field: Field, options: OptionValues[], customFieldName?: string) {
+    let option = options.find(f => customFieldName ? f.name === customFieldName : f.name === field.name)
     if (!option) {
       return
     }

+ 2 - 0
src/plugins/endpoint/index.js

@@ -24,6 +24,7 @@ import OpenstackContentPlugin from './openstack/ContentPlugin'
 
 import DefaultOptionsSchemaPlugin from './default/OptionsSchemaPlugin'
 import OvmOptionsSchemaPlugin from './ovm/OptionsSchemaPlugin'
+import OpenstackOptionsSchemaPlugin from './openstack/OptionsSchemaPlugin'
 
 export const ConnectionSchemaPlugin = {
   default: DefaultConnectionSchemaPlugin,
@@ -35,6 +36,7 @@ export const ConnectionSchemaPlugin = {
 export const OptionsSchemaPlugin = {
   default: DefaultOptionsSchemaPlugin,
   oracle_vm: OvmOptionsSchemaPlugin,
+  openstack: OpenstackOptionsSchemaPlugin,
 }
 
 export const ContentPlugin = {

+ 163 - 10
src/plugins/endpoint/openstack/ContentPlugin.jsx

@@ -14,20 +14,52 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import React from 'react'
+import * as React from 'react'
 import styled from 'styled-components'
 
 import configLoader from '../../../utils/Config'
 
 import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
 import type { Field } from '../../../types/Field'
-import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
+import { Wrapper, FieldStyled, Row } from '../default/ContentPlugin'
 
 import StyleProps from '../../../components/styleUtils/StyleProps'
+import Palette from '../../../components/styleUtils/Palette'
 
 const ToggleButtonBarStyled = styled(ToggleButtonBar)`
   margin-top: 16px;
 `
+const Fields = styled.div`
+  margin-top: 32px;
+  padding: 0 32px;
+  display: flex;
+  flex-direction: column;
+  overflow: auto;
+`
+const Group = styled.div`
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+`
+const GroupName = styled.div`
+  display: flex;
+  align-items: center;
+  margin: 32px 0 24px 0;
+`
+const GroupNameText = styled.div`
+  margin: 0 32px;
+  font-size: 16px;
+`
+const GroupNameBar = styled.div`
+  flex-grow: 1;
+  background: ${Palette.grayscale[3]};
+  height: 1px;
+`
+const GroupFields = styled.div`
+  display: flex;
+  justify-content: space-between;
+  flex-direction: column;
+`
 
 type Props = {
   connectionInfoSchema: Field[],
@@ -35,6 +67,7 @@ type Props = {
   invalidFields: string[],
   getFieldValue: (field: ?Field) => any,
   handleFieldChange: (field: ?Field, value: any) => void,
+  handleFieldsChange: (items: { field: Field, value: any }[]) => void,
   disabled: boolean,
   cancelButtonText: string,
   validating: boolean,
@@ -44,10 +77,16 @@ type Props = {
 }
 type State = {
   useAdvancedOptions: boolean,
+  showCephOptions: boolean,
 }
 class ContentPlugin extends React.Component<Props, State> {
+  // This is a temporary hack, should be always true for all plugins, but momentaraly causes issues in Azure plugins
+  // Fix Azure plugin and remove this line
+  static REQUIRES_PARENT_OBJECT_PATH = true
+
   state = {
     useAdvancedOptions: false,
+    showCephOptions: false,
   }
 
   previouslySelectedChoices: string[] = []
@@ -56,6 +95,16 @@ class ContentPlugin extends React.Component<Props, State> {
     return Boolean(this.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === 'openstack_use_current_user')))
   }
 
+  get hasCephOptionsSet(): boolean {
+    console.log('schema', JSON.parse(JSON.stringify(this.props.connectionInfoSchema)))
+    let cephOptionsField = this.props.connectionInfoSchema.find(n => n.name === 'ceph_options')
+    if (!cephOptionsField || !cephOptionsField.properties) {
+      return false
+    }
+    let hasValues = cephOptionsField.properties.filter(f => this.getFieldValue(f))
+    return hasValues.length > 0
+  }
+
   componentDidMount() {
     this.props.onRef(this)
   }
@@ -103,10 +152,25 @@ class ContentPlugin extends React.Component<Props, State> {
     this.setState({ useAdvancedOptions })
   }
 
+  handleShowCepthOptionsChange(value: boolean) {
+    let cephOptions = this.props.connectionInfoSchema.find(f => f.name === 'ceph_options')
+    if (!cephOptions || !cephOptions.properties) {
+      return
+    }
+    let resetFields = cephOptions.properties.map(field => ({
+      field,
+      value: null,
+    }))
+
+    this.props.handleFieldsChange(resetFields)
+
+    this.setState({ showCephOptions: value })
+  }
+
   findInvalidFields = () => {
     let inputChoices = ['user_domain', 'project_domain']
 
-    const invalidFields = this.props.connectionInfoSchema.filter(field => {
+    let invalidFields = this.props.connectionInfoSchema.filter(field => {
       if (this.isFieldRequired(field)) {
         let value = this.getFieldValue(field)
         return !value
@@ -121,6 +185,13 @@ class ContentPlugin extends React.Component<Props, State> {
       return false
     }).map(f => f.name)
 
+    let cephOptions = this.props.connectionInfoSchema.find(f => f.name === 'ceph_options')
+    let cephOptionsProperties = cephOptions && cephOptions.properties
+    if (cephOptionsProperties && (this.state.showCephOptions || this.hasCephOptionsSet)) {
+      invalidFields = invalidFields.concat(
+        cephOptionsProperties.filter(f => f.required && !this.getFieldValue(f)).map(f => f.name)
+      )
+    }
     return invalidFields
   }
 
@@ -135,6 +206,10 @@ class ContentPlugin extends React.Component<Props, State> {
     }
 
     return this.props.connectionInfoSchema.filter(f => !ignoreFields.find(i => i === f.name)).filter(field => {
+      if (field.name === 'ceph_options') {
+        return this.state.useAdvancedOptions && (this.state.showCephOptions || this.hasCephOptionsSet)
+      }
+
       if (this.state.useAdvancedOptions) {
         return true
       }
@@ -158,17 +233,34 @@ class ContentPlugin extends React.Component<Props, State> {
 
   renderFields() {
     const rows = []
-    let lastField
     let fields = this.filterSimpleAdvanced()
+    if (this.state.useAdvancedOptions) {
+      let showCepthOptionsField = {
+        name: 'show_ceph_options',
+        label: 'Use Ceph for Replication',
+        type: 'boolean',
+        description: 'If performing Ceph-based Replicas from a source OpenStack, the Ceph configuration file and credentials for a user with read-only access to the Ceph pool used by Cinder backups/snapshots must be provided. Coriolis must be able to connect to the source OpenStack\'s Ceph RADOS cluster by being able to reach at least one Ceph- monitor host.For the easiest setup possible, simply using the same credentials used by the Cinder service(s) will work.',
+      }
+      fields.push(showCepthOptionsField)
+    }
 
-    fields.forEach((field, i) => {
+    const renderField = field => {
       let disabled = this.props.disabled
         || (this.useCurrentUser && field.name !== 'name' && field.name !== 'description' && field.name !== 'openstack_use_current_user')
       let required = this.isFieldRequired(field)
         || (this.getApiVersion() > 2 ? field.name === 'user_domain' || field.name === 'project_domain' : false)
       let isPassword = Boolean(configLoader.config.passwordFields.find(fn => field.name === fn))
         || field.name.indexOf('password') > -1
-      const currentField = (
+      let value = field.name === 'show_ceph_options' ? (this.state.showCephOptions || this.hasCephOptionsSet) : this.getFieldValue(field)
+      let onChange = value => {
+        if (field.name === 'show_ceph_options') {
+          this.handleShowCepthOptionsChange(value)
+        } else {
+          this.props.handleFieldChange(field, value)
+        }
+      }
+
+      return (
         <FieldStyled
           {...field}
           required={required}
@@ -176,12 +268,18 @@ class ContentPlugin extends React.Component<Props, State> {
           width={StyleProps.inputSizes.large.width}
           disabled={disabled}
           highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}
-          value={this.getFieldValue(field)}
-          onChange={value => { this.props.handleFieldChange(field, value) }}
+          value={value}
+          onChange={onChange}
           getFieldValue={fieldName => this.getFieldValue(this.props.connectionInfoSchema.find(n => n.name === fieldName))}
           onFieldChange={(fieldName, fieldValue) => { this.props.handleFieldChange(this.props.connectionInfoSchema.find(n => n.name === fieldName), fieldValue) }}
         />
       )
+    }
+
+    let lastField = null
+    let nonCephFields = fields.filter(f => f.name !== 'ceph_options')
+    nonCephFields.forEach((field, i) => {
+      const currentField = renderField(field)
       if (i % 2 !== 0) {
         rows.push((
           <Row key={field.name}>
@@ -189,7 +287,7 @@ class ContentPlugin extends React.Component<Props, State> {
             {currentField}
           </Row>
         ))
-      } else if (i === fields.length - 1) {
+      } else if (i === nonCephFields.length - 1) {
         rows.push((
           <Row key={field.name}>
             {currentField}
@@ -199,9 +297,64 @@ class ContentPlugin extends React.Component<Props, State> {
       lastField = currentField
     })
 
+    const cephOptionsRows = []
+    let cephOptionsField = fields.find(f => f.name === 'ceph_options')
+    let cephOptions = null
+    let properties = cephOptionsField && cephOptionsField.properties
+
+    if (properties) {
+      let i = 0
+      properties.forEach((field, fieldIndex) => {
+        if (field.name === 'ceph_options/ceph_conf_file' || field.name === 'ceph_options/ceph_keyring_file') {
+          field.useTextArea = true
+        }
+
+        const currentField = renderField(field)
+
+        const pushRow = (field1: React.Node, field2?: React.Node) => {
+          cephOptionsRows.push((
+            <Row key={field.name}>
+              {field1}
+              {field2}
+            </Row>
+          ))
+        }
+        if (field.useTextArea) {
+          pushRow(currentField)
+          i -= 1
+        } else if (i % 2 !== 0) {
+          pushRow(lastField, currentField)
+        } else if (fieldIndex === properties.length - 1) {
+          pushRow(currentField)
+          if (field.useTextArea) {
+            i -= 1
+          }
+        } else {
+          lastField = currentField
+        }
+        i += 1
+      })
+
+      cephOptions = (
+        <Group>
+          <GroupName>
+            <GroupNameBar />
+            <GroupNameText>Ceph Options</GroupNameText>
+            <GroupNameBar />
+          </GroupName>
+          <GroupFields>{cephOptionsRows}</GroupFields>
+        </Group>
+      )
+    }
+
     return (
       <Fields innerRef={ref => { this.props.scrollableRef(ref) }}>
-        {rows}
+        <Group>
+          <GroupFields>
+            {rows}
+          </GroupFields>
+        </Group>
+        {cephOptions}
       </Fields>
     )
   }

+ 72 - 0
src/plugins/endpoint/openstack/OptionsSchemaPlugin.js

@@ -0,0 +1,72 @@
+/*
+Copyright (C) 2019  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 DefaultOptionsSchemaPlugin from '../default/OptionsSchemaPlugin'
+import LabelDictionary from '../../../utils/LabelDictionary'
+
+import type { Field } from '../../../types/Field'
+import type { OptionValues, StorageMap } from '../../../types/Endpoint'
+import type { SchemaProperties, SchemaDefinitions } from '../../../types/Schema'
+import type { NetworkMap } from '../../../types/Network'
+
+export default class OptionsSchemaParser {
+  static parseSchemaToFields(schema: SchemaProperties, schemaDefinitions?: ?SchemaDefinitions) {
+    let fields = DefaultOptionsSchemaPlugin.parseSchemaToFields(schema, schemaDefinitions)
+    let exportMechField = fields.find(f => f.name === 'replica_export_mechanism')
+    if (exportMechField) {
+      exportMechField.subFields = []
+      exportMechField.enum.forEach(exportType => {
+        let exportTypeFieldIdx = fields.findIndex(f => f.name === `${exportType}_options`)
+        if (exportTypeFieldIdx > -1) {
+          let subField = fields[exportTypeFieldIdx]
+          if (subField.properties && subField.properties.length > 2) {
+            subField.properties = subField.properties.map(p => ({ ...p, groupName: LabelDictionary.get(subField.name) }))
+          }
+          exportMechField.subFields.push(subField)
+          fields.splice(exportTypeFieldIdx, 1)
+        }
+      })
+    }
+    return fields
+  }
+
+  static fillFieldValues(field: Field, options: OptionValues[]) {
+    if (field.name === 'replica_export_mechanism' && field.subFields) {
+      field.subFields.forEach(sf => {
+        if (sf.properties) {
+          sf.properties.forEach(f => {
+            DefaultOptionsSchemaPlugin.fillFieldValues(f, options, f.name.split('/')[1])
+          })
+        }
+      })
+    } else {
+      DefaultOptionsSchemaPlugin.fillFieldValues(field, options)
+    }
+  }
+
+  static getDestinationEnv(options: ?{ [string]: mixed }, oldOptions?: any) {
+    return DefaultOptionsSchemaPlugin.getDestinationEnv(options, oldOptions)
+  }
+
+  static getNetworkMap(networkMappings: ?NetworkMap[]) {
+    return DefaultOptionsSchemaPlugin.getNetworkMap(networkMappings)
+  }
+
+  static getStorageMap(defaultStorage: ?string, storageMap: ?StorageMap[], configDefault?: ?string) {
+    return DefaultOptionsSchemaPlugin.getStorageMap(defaultStorage, storageMap, configDefault)
+  }
+}
+

+ 10 - 0
src/stores/WizardStore.js

@@ -38,6 +38,16 @@ const updateOptions = (oldOptions: ?{ [string]: mixed }, data: { field: Field, v
   } else {
     options[data.field.name] = data.value
   }
+
+  if (data.field.enum && data.field.subFields) {
+    data.field.subFields.forEach(subField => {
+      let subFieldKeys = Object.keys(options).filter(k => k.indexOf(`${subField.name}/`) > -1)
+      subFieldKeys.forEach(k => {
+        delete options[k]
+      })
+    })
+  }
+
   return options
 }
 

+ 22 - 1
src/types/Field.js

@@ -35,6 +35,8 @@ export type Field = {
   readOnly?: boolean,
   title?: string,
   description?: string,
+  subFields?: Field[],
+  groupName?: string,
 }
 
 const migrationImageOsTypes = ['windows', 'linux']
@@ -47,7 +49,26 @@ class FieldHelper {
     if (value === false) {
       return 'No'
     }
-    let field = fields.find(f => f.name === name)
+    let findField = (f: Field[]) => f.find(f1 => f1.name === name)
+    let field = findField(fields)
+    if (!field) {
+      fields.forEach(f => {
+        if (f.properties && !field) {
+          field = findField(f.properties)
+        }
+
+        if (f.subFields && !field) {
+          field = findField(f.subFields)
+          if (f.subFields && !field) {
+            f.subFields.forEach(sf => {
+              if (!field && sf.properties) {
+                field = findField(sf.properties)
+              }
+            })
+          }
+        }
+      })
+    }
     let findInEnum = (v: any) => {
       let valueName = v
       if (field && field.enum) {

+ 1 - 1
src/types/Schema.js

@@ -34,7 +34,7 @@ export type SchemaProperties = {
     } | {
       type: string,
       enum?: string[],
-      default?: string,
+      default?: any,
     } | {
       $ref: string,
     },

+ 1 - 1
src/utils/LabelDictionary.js

@@ -60,7 +60,7 @@ const cache: { name: string, label: ?string, description: ?string }[] = []
 class LabelDictionary {
   // Fields which have enums for which dictionary labels should be used.
   // If a field has enums and is not in this array, their values will be used as labels
-  static enumFields = ['port_reuse_policy']
+  static enumFields = ['port_reuse_policy', 'replica_export_mechanism']
 
   static get(fieldName: ?string): string {
     if (!fieldName) {

+ 10 - 3
src/utils/ObjectUtils.js

@@ -17,16 +17,23 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import configLoader from './Config'
 
 class ObjectUtils {
-  static flatten(object: any): any {
+  static flatten(object: any, appendParentPath?: boolean, parent?: string): any {
     let result = {}
 
     Object.keys(object).forEach(k => {
       if (typeof object[k] === 'object') {
         if (object[k]) {
-          result = { ...result, ...this.flatten(object[k]) }
+          result = {
+            ...result,
+            ...this.flatten(object[k], appendParentPath, k),
+          }
         }
       } else {
-        result[k] = object[k]
+        let key = k
+        if (appendParentPath && parent) {
+          key = `${parent}/${k}`
+        }
+        result[key] = object[k]
       }
     })