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

Add support for `object` type in wizard options

Sergiu Miclea 8 лет назад
Родитель
Сommit
6064b6ef59

+ 4 - 3
src/components/atoms/TextInput/index.jsx

@@ -15,7 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 // @flow
 
 import * as React from 'react'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 
@@ -43,9 +43,9 @@ const Input = styled.input`
   line-height: ${StyleProps.inputSizes.regular.height}px;
   border-radius: ${StyleProps.borderRadius};
   background-color: #FFF;
-  border: 1px solid ${props => borderColor(props)};
+  border: ${props => props.embedded ? 0 : css`1px solid ${props => borderColor(props)}`};
   color: ${Palette.black};
-  padding: 0 ${props => props.customRequired ? '29px' : '8px'} 0 16px;
+  padding: 0 ${props => props.customRequired ? '29px' : '8px'} 0 ${props => props.embedded ? 0 : '16px'};
   font-size: inherit;
   transition: all ${StyleProps.animations.swift};
   box-sizing: border-box;
@@ -97,6 +97,7 @@ type Props = {
   value?: string,
   showClose?: boolean,
   onCloseClick?: () => void,
+  embedded?: boolean,
 }
 const TextInput = (props: Props) => {
   const { _ref, required, value, onChange, showClose, onCloseClick } = props

+ 66 - 17
src/components/molecules/PropertiesTable/index.jsx

@@ -24,6 +24,8 @@ import TextInput from '../../atoms/TextInput'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
+import Dropdown from '../../molecules/Dropdown'
+import type { Field } from '../../../types/Field'
 
 const Wrapper = styled.div`
   display: flex;
@@ -32,7 +34,7 @@ const Wrapper = styled.div`
   border-radius: ${StyleProps.borderRadius};
 `
 const Column = styled.div`
-  width: 50%;
+  ${StyleProps.exactWidth('calc(50% - 16px)')}
   height: 32px;
   display: flex;
   align-items: center;
@@ -47,7 +49,7 @@ const Row = styled.div`
   align-items: center;
   border-bottom: 1px solid ${Palette.grayscale[2]};
   &:last-child {
-    border-bottom-color: transparent;
+    border-bottom: 0;
   }
   &:first-child ${Column} {
     border-top-left-radius: ${StyleProps.borderRadius};
@@ -57,21 +59,25 @@ const Row = styled.div`
   }
 `
 
-type PropertyType = {
-  name: string,
-  type: string,
-}
 type Props = {
-  properties: PropertyType[],
-  onChange: (property: PropertyType, value: any) => void,
-  valueCallback: (property: PropertyType) => any,
-}
+  properties: Field[],
+  onChange: (property: Field, value: any) => void,
+  valueCallback: (property: Field) => any,
+ }
 @observer
 class PropertiesTable extends React.Component<Props> {
-  renderSwitch(prop: PropertyType) {
+  getName(propName: string): string {
+    if (propName.indexOf('/') > -1) {
+      return LabelDictionary.get(propName.substr(propName.lastIndexOf('/') + 1))
+    }
+    return LabelDictionary.get(propName)
+  }
+
+  renderSwitch(prop: Field, opts: { triState: boolean }) {
     return (
       <Switch
         secondary
+        triState={opts.triState}
         height={16}
         checked={this.props.valueCallback(prop)}
         onChange={checked => { this.props.onChange(prop, checked) }}
@@ -79,20 +85,63 @@ class PropertiesTable extends React.Component<Props> {
     )
   }
 
-  renderTextInput() {
+  renderTextInput(prop: Field) {
+    return (
+      <TextInput
+        width="100%"
+        embedded
+        value={this.props.valueCallback(prop)}
+        onChange={e => { this.props.onChange(prop, e.target.value) }}
+        placeholder={this.getName(prop.name)}
+        required={typeof prop.required === 'boolean' ? prop.required : false}
+      />
+    )
+  }
+
+  renderEnumDropdown(prop: Field) {
+    if (!prop.enum) {
+      return null
+    }
+    let items = prop.enum.map(e => {
+      return {
+        label: this.getName(e),
+        value: e,
+      }
+    })
+
+    items = [
+      { label: 'Choose a value', value: null },
+      ...items,
+    ]
+
+    let selectedItem = items.find(i => i.value === this.props.valueCallback(prop))
+
     return (
-      <TextInput />
+      <Dropdown
+        width={320}
+        noSelectionMessage="Choose a value"
+        selectedItem={selectedItem ? selectedItem.label : null}
+        items={items}
+        onChange={item => this.props.onChange(prop, item.value)}
+      />
     )
   }
 
-  renderInput(prop: PropertyType) {
+  renderInput(prop: Field) {
     let input = null
     switch (prop.type) {
       case 'boolean':
-        input = this.renderSwitch(prop)
+        input = this.renderSwitch(prop, { triState: true })
+        break
+      case 'strict-boolean':
+        input = this.renderSwitch(prop, { triState: false })
         break
       case 'string':
-        input = this.renderTextInput()
+        if (prop.enum) {
+          input = this.renderEnumDropdown(prop)
+        } else {
+          input = this.renderTextInput(prop)
+        }
         break
       default:
     }
@@ -106,7 +155,7 @@ class PropertiesTable extends React.Component<Props> {
         {this.props.properties.map(prop => {
           return (
             <Row key={prop.name}>
-              <Column header>{LabelDictionary.get(prop.name)}</Column>
+              <Column header>{this.getName(prop.name)}</Column>
               <Column input>{this.renderInput(prop)}</Column>
             </Row>
           )

+ 6 - 4
src/components/molecules/PropertiesTable/story.jsx

@@ -17,8 +17,11 @@ import { storiesOf } from '@storybook/react'
 import PropertiesTable from '.'
 
 let properties = [
-  { type: 'boolean', name: 'prop-1', label: 'Property 1' },
-  { type: 'boolean', name: 'prop-2', label: 'Property 2' },
+  { type: 'boolean', name: 'prop-1', label: 'Boolean' },
+  { type: 'strict-boolean', name: 'prop-2', label: 'Strict Boolean' },
+  { type: 'string', name: 'prop-3', label: 'String' },
+  { type: 'string', name: 'prop-3a', label: 'String', required: true },
+  { type: 'string', enum: ['a', 'b', 'c'], name: 'prop-4', label: 'String enum' },
 ]
 
 class Wrapper extends React.Component {
@@ -39,9 +42,8 @@ class Wrapper extends React.Component {
 
   render() {
     return (
-      <div style={{ width: '200px' }}>
+      <div style={{ width: '320px', background: 'white', padding: '50px' }}>
         <PropertiesTable
-          {...this.props}
           properties={properties}
           valueCallback={prop => this.valueCallback(prop)}
           onChange={(prop, value) => { this.handleChange(prop, value) }}

+ 4 - 3
src/components/molecules/WizardOptionsField/index.jsx

@@ -27,6 +27,8 @@ import PropertiesTable from '../../molecules/PropertiesTable'
 import StyleProps from '../../styleUtils/StyleProps'
 import LabelDictionary from '../../../utils/LabelDictionary'
 
+import type { Field } from '../../../types/Field'
+
 const getDirection = props => {
   if (props.type === 'strict-boolean' || props.type === 'boolean') {
     return 'row'
@@ -47,15 +49,14 @@ const LabelText = styled.span`
   margin-right: 24px;
 `
 
-type PropertyType = { name: string, type: string }
 type Props = {
   type: 'replica' | 'migration',
   name: string,
   value: any,
   onChange: (value: any) => void,
-  valueCallback: (prop: PropertyType, value: any) => void,
+  valueCallback: (prop: Field, value: any) => void,
   className?: string,
-  properties: PropertyType[],
+  properties: Field[],
   enum: string[],
   required: boolean,
 }

+ 22 - 5
src/components/organisms/MainDetails/index.jsx

@@ -273,7 +273,7 @@ class MainDetails extends React.Component<Props> {
   }
 
   renderPropertiesTable(propertyNames: string[]) {
-    let renderValue = (value: any) => {
+    let getValue = (value: any) => {
       if (value === true) {
         return 'Yes'
       }
@@ -283,13 +283,30 @@ class MainDetails extends React.Component<Props> {
       return value.toString()
     }
 
+    let properties = []
+    propertyNames.forEach(pn => {
+      let value = this.props.item ? this.props.item.destination_environment[pn] : ''
+      let label = LabelDictionary.get(pn)
+
+      if (value && typeof value === 'object') {
+        properties = properties.concat(Object.keys(value).map(p => {
+          return {
+            label: `${label} - ${LabelDictionary.get(p)}`,
+            value: getValue(value[p]),
+          }
+        }))
+      } else {
+        properties.push({ label, value: getValue(value) })
+      }
+    })
+
     return (
       <PropertiesTable>
-        {propertyNames.map(propName => {
+        {properties.map(prop => {
           return (
-            <PropertyRow key={propName}>
-              <PropertyName>{LabelDictionary.get(propName)}</PropertyName>
-              <PropertyValue>{renderValue(this.props.item ? this.props.item.destination_environment[propName] : '')}</PropertyValue>
+            <PropertyRow key={prop.label}>
+              <PropertyName>{prop.label}</PropertyName>
+              <PropertyValue>{prop.value}</PropertyValue>
             </PropertyRow>
           )
         })}

+ 12 - 12
src/components/organisms/WizardOptions/index.jsx

@@ -106,17 +106,18 @@ class WizardOptions extends React.Component<Props> {
   }
 
   renderOptionsField(field: Field) {
+    let additionalProps
     if (field.type === 'object' && field.properties) {
-      return (
-        <WizardOptionsFieldStyled
-          key={field.name}
-          name={field.name}
-          type={field.type}
-          valueCallback={f => this.getFieldValue(f.name, f.default)}
-          properties={field.properties}
-          onChange={(f, value) => { this.props.onChange(f, value) }}
-        />
-      )
+      additionalProps = {
+        valueCallback: f => this.getFieldValue(f.name, f.default),
+        onChange: (f, value) => { this.props.onChange(f, value) },
+        properties: field.properties,
+      }
+    } else {
+      additionalProps = {
+        value: this.getFieldValue(field.name, field.default),
+        onChange: value => { this.props.onChange(field, value) },
+      }
     }
     return (
       <WizardOptionsFieldStyled
@@ -125,8 +126,7 @@ class WizardOptions extends React.Component<Props> {
         type={field.type}
         enum={field.enum}
         required={field.required}
-        value={this.getFieldValue(field.name, field.default)}
-        onChange={value => { this.props.onChange(field, value) }}
+        {...additionalProps}
       />
     )
   }

+ 15 - 6
src/components/organisms/WizardPageContent/index.jsx

@@ -195,17 +195,26 @@ class WizardPageContent extends React.Component<Props, State> {
   }
 
   isOptionsPageValid() {
+    const isValid = (field: Field): boolean => {
+      return (this.props.wizardData.options &&
+          this.props.wizardData.options[field.name] !== null &&
+          this.props.wizardData.options[field.name] !== undefined &&
+          this.props.wizardData.options[field.name] !== ''
+      ) || (field.default !== null && field.default !== undefined)
+    }
+
     let schema = this.props.providerStore.optionsSchema
     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) {
+          required = required.concat(f.properties.filter(p => p.required))
+        }
+      })
+
       let validFieldsCount = 0
       required.forEach(f => {
-        if (
-          (this.props.wizardData.options &&
-          this.props.wizardData.options[f.name] !== null &&
-          this.props.wizardData.options[f.name] !== undefined) ||
-          (f.default !== null && f.default !== undefined)
-        ) {
+        if (isValid(f)) {
           validFieldsCount += 1
         }
       })

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

@@ -256,7 +256,7 @@ class WizardSummary extends React.Component<Props> {
 
             return (
               <Option key={optionName}>
-                <OptionLabel>{LabelDictionary.get(optionName)}</OptionLabel>
+                <OptionLabel>{optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}</OptionLabel>
                 <OptionValue>{
                   // $FlowIssue
                   this.renderOptionValue(data.options[optionName])

+ 1 - 1
src/config.js

@@ -63,7 +63,7 @@ export const env = {
 export const executionOptions = [
   {
     name: 'shutdown_instances',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
 ]
 

+ 19 - 8
src/plugins/endpoint/default/SchemaPlugin.js

@@ -14,23 +14,34 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import type { Schema } from '../../../types/Schema'
+import type { Schema, SchemaProperties, SchemaDefinitions } from '../../../types/Schema'
 import type { Field } from '../../../types/Field'
 
-export const defaultSchemaToFields = (schema: Schema) => {
+export const defaultSchemaToFields = (schema: SchemaProperties, schemaDefinitions?: ?SchemaDefinitions, parent?: string): any[] => {
   let fields = Object.keys(schema.properties).map(fieldName => {
-    let field = {
-      ...schema.properties[fieldName],
-      name: fieldName,
+    let properties = schema.properties[fieldName]
+
+    if (typeof schema.properties[fieldName].$ref === 'string' && schemaDefinitions) {
+      const definitionName = schema.properties[fieldName].$ref.substr(schema.properties[fieldName].$ref.lastIndexOf('/') + 1)
+      properties = schemaDefinitions[definitionName]
+      return {
+        name: fieldName,
+        type: properties.type ? properties.type : '',
+        properties: properties.properties ? defaultSchemaToFields(properties, null, fieldName) : [],
+      }
+    }
+
+    return {
+      ...properties,
+      name: parent ? `${parent}/${fieldName}` : fieldName,
       required: schema.required && schema.required.find(k => k === fieldName) ? true : fieldName === 'username' || fieldName === 'password',
     }
-    return field
   })
 
   return fields
 }
 
-export const connectionSchemaToFields = (schema: Schema) => {
+export const connectionSchemaToFields = (schema: SchemaProperties) => {
   let fields = defaultSchemaToFields(schema)
 
   let sortPriority = { username: 1, password: 2 }
@@ -66,7 +77,7 @@ export const generateField = (name: string, label: string, required: boolean = f
   return field
 }
 
-export const fieldsToPayload = (data: { [string]: mixed }, schema: Schema) => {
+export const fieldsToPayload = (data: { [string]: mixed }, schema: SchemaProperties) => {
   let info = {}
 
   Object.keys(schema.properties).forEach(fieldName => {

+ 4 - 3
src/sources/Schemas.js

@@ -16,11 +16,12 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import { SchemaPlugin } from '../plugins/endpoint'
 import { defaultSchemaToFields } from '../plugins/endpoint/default/SchemaPlugin'
+import type { Schema } from '../types/Schema'
 
 class SchemaParser {
   static storedConnectionsSchemas = {}
 
-  static connectionSchemaToFields(provider: string, schema: { [string]: any }) {
+  static connectionSchemaToFields(provider: string, schema: Schema) {
     if (!this.storedConnectionsSchemas[provider]) {
       this.storedConnectionsSchemas[provider] = schema
     }
@@ -31,8 +32,8 @@ class SchemaParser {
     return fields
   }
 
-  static optionsSchemaToFields(provider: string, schema: { [string]: any }) {
-    let fields = defaultSchemaToFields(schema.oneOf[0])
+  static optionsSchemaToFields(provider: string, schema: Schema) {
+    let fields = defaultSchemaToFields(schema.oneOf[0], schema.definitions)
     fields.sort((a, b) => {
       if (a.required && !b.required) {
         return -1

+ 9 - 1
src/sources/WizardSource.js

@@ -34,7 +34,15 @@ class WizardSourceUtils {
           || data.options[optionName] === null || data.options[optionName] === undefined) {
           return
         }
-        env[optionName] = data.options[optionName]
+        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
+        }
       })
     }
 

+ 3 - 1
src/types/Field.js

@@ -18,11 +18,13 @@ export type Field = {
   name: string,
   type?: string,
   value?: any,
-  enum?: string[] | {name: string, id: string}[],
+  enum?: string[],
   required?: boolean | (value: any) => boolean,
   default?: any,
   items?: Field[],
   fields?: Field[],
   minimum?: number,
   maximum?: number,
+  parent?: string,
+  properties?: Field[],
 }

+ 30 - 4
src/types/Schema.js

@@ -14,12 +14,38 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-export type Schema = {
+// export type Schema = {
+//   properties: {
+//     [string]: {
+//       name: string,
+//     }
+//   },
+//   required: string[],
+//   oneOf: Schema[],
+// }
+
+export type SchemaProperties = {
   properties: {
     [string]: {
-      name: string,
-    }
+      type: 'array',
+      items: {
+        type: string,
+      },
+    } | {
+      type: string,
+      enum?: string[],
+    } | {
+      $ref: string,
+    },
   },
   required: string[],
-  oneOf: Schema[],
+}
+
+export type SchemaDefinitions = {
+  [string]: SchemaProperties,
+}
+
+export type Schema = {
+  oneOf: SchemaProperties[],
+  definitions?: SchemaDefinitions,
 }

+ 2 - 0
src/utils/LabelDictionary.js

@@ -97,6 +97,8 @@ class LabelDictionary {
     vmware_vsphere: 'VMware',
     separate_vm: 'Separate Migration/VM?',
     use_replica: 'Use replica',
+    windows_migr_image: { label: 'Windows Migration Image', description: 'The Windows Migration Image information found on the Azure page' },
+    linux_migr_image: { label: 'Linux Migration Image', description: 'The Linux Migration Image information found on the Azure page' },
   }
 
   static get(fieldName: ?string): string {