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

Load destination options values in wizard

After the destination options schema is loaded, load the destination
options values which complement the schema. For example, if using the
schema we render a text input for a string field, after loading the
destination options, we may have a dropdown instead with available
values for that field and a default value selected.
Sergiu Miclea 8 лет назад
Родитель
Сommit
fdf07014ae

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

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import Switch from '../../atoms/Switch'
 import Switch from '../../atoms/Switch'
 import TextInput from '../../atoms/TextInput'
 import TextInput from '../../atoms/TextInput'
@@ -58,6 +59,7 @@ type Props = {
   enum: string[],
   enum: string[],
   required: boolean,
   required: boolean,
 }
 }
+@observer
 class WizardOptionsField extends React.Component<Props> {
 class WizardOptionsField extends React.Component<Props> {
   renderSwitch(propss: { triState: boolean }) {
   renderSwitch(propss: { triState: boolean }) {
     return (
     return (
@@ -102,8 +104,8 @@ class WizardOptionsField extends React.Component<Props> {
   renderEnumDropdown() {
   renderEnumDropdown() {
     let items = this.props.enum.map(e => {
     let items = this.props.enum.map(e => {
       return {
       return {
-        label: LabelDictionary.get(e),
-        value: e,
+        label: typeof e === 'string' ? LabelDictionary.get(e) : e.name,
+        value: typeof e === 'string' ? e : e.id,
       }
       }
     })
     })
 
 

+ 43 - 2
src/components/organisms/WizardOptions/index.jsx

@@ -16,16 +16,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { observer } from 'mobx-react'
 
 
 import Tooltip from '../../atoms/Tooltip'
 import Tooltip from '../../atoms/Tooltip'
 import ToggleButtonBar from '../../atoms/ToggleButtonBar'
 import ToggleButtonBar from '../../atoms/ToggleButtonBar'
 import WizardOptionsField from '../../molecules/WizardOptionsField'
 import WizardOptionsField from '../../molecules/WizardOptionsField'
+import StatusImage from '../../atoms/StatusImage'
 import type { Field } from '../../../types/Field'
 import type { Field } from '../../../types/Field'
 import type { Instance } from '../../../types/Instance'
 import type { Instance } from '../../../types/Instance'
 
 
 import { executionOptions } from '../../../config'
 import { executionOptions } from '../../../config'
 
 
 const Wrapper = styled.div``
 const Wrapper = styled.div``
+const Options = styled.div``
 const Fields = styled.div`
 const Fields = styled.div`
   margin-top: 46px;
   margin-top: 46px;
   display: flex;
   display: flex;
@@ -39,6 +42,16 @@ const WizardOptionsFieldStyled = styled(WizardOptionsField) `
   justify-content: space-between;
   justify-content: space-between;
   margin-bottom: 39px;
   margin-bottom: 39px;
 `
 `
+const LoadingWrapper = styled.div`
+  margin-top: 32px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`
+const LoadingText = styled.div`
+  margin-top: 38px;
+  font-size: 18px;
+`
 
 
 type Props = {
 type Props = {
   fields: Field[],
   fields: Field[],
@@ -48,7 +61,9 @@ type Props = {
   useAdvancedOptions: boolean,
   useAdvancedOptions: boolean,
   onAdvancedOptionsToggle: (showAdvanced: boolean) => void,
   onAdvancedOptionsToggle: (showAdvanced: boolean) => void,
   wizardType: string,
   wizardType: string,
+  loading: boolean,
 }
 }
+@observer
 class WizardOptions extends React.Component<Props> {
 class WizardOptions extends React.Component<Props> {
   getFieldValue(fieldName: string, defaultValue: any) {
   getFieldValue(fieldName: string, defaultValue: any) {
     if (!this.props.data || this.props.data[fieldName] === undefined) {
     if (!this.props.data || this.props.data[fieldName] === undefined) {
@@ -164,15 +179,41 @@ class WizardOptions extends React.Component<Props> {
     )
     )
   }
   }
 
 
-  render() {
+  renderLoading() {
+    if (!this.props.loading) {
+      return null
+    }
+
     return (
     return (
-      <Wrapper>
+      <LoadingWrapper>
+        <StatusImage loading />
+        <LoadingText>Loading options...</LoadingText>
+      </LoadingWrapper>
+    )
+  }
+
+  renderOptions() {
+    if (this.props.loading) {
+      return null
+    }
+
+    return (
+      <Options>
         <ToggleButtonBar
         <ToggleButtonBar
           items={[{ label: 'Simple', value: 'simple' }, { label: 'Advanced', value: 'advanced' }]}
           items={[{ label: 'Simple', value: 'simple' }, { label: 'Advanced', value: 'advanced' }]}
           selectedValue={this.props.useAdvancedOptions ? 'advanced' : 'simple'}
           selectedValue={this.props.useAdvancedOptions ? 'advanced' : 'simple'}
           onChange={item => { this.props.onAdvancedOptionsToggle(item.value === 'advanced') }}
           onChange={item => { this.props.onAdvancedOptionsToggle(item.value === 'advanced') }}
         />
         />
         {this.renderOptionsFields()}
         {this.renderOptionsFields()}
+      </Options>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderOptions()}
+        {this.renderLoading()}
       </Wrapper>
       </Wrapper>
     )
     )
   }
   }

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

@@ -200,7 +200,12 @@ class WizardPageContent extends React.Component<Props, State> {
       let required = schema.filter(f => f.required && f.type !== 'object')
       let required = schema.filter(f => f.required && f.type !== 'object')
       let validFieldsCount = 0
       let validFieldsCount = 0
       required.forEach(f => {
       required.forEach(f => {
-        if (this.props.wizardData.options && this.props.wizardData.options[f.name] !== null && this.props.wizardData.options[f.name] !== undefined) {
+        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)
+        ) {
           validFieldsCount += 1
           validFieldsCount += 1
         }
         }
       })
       })
@@ -313,6 +318,7 @@ class WizardPageContent extends React.Component<Props, State> {
       case 'options':
       case 'options':
         body = (
         body = (
           <WizardOptions
           <WizardOptions
+            loading={this.props.providerStore.optionsSchemaLoading || this.props.providerStore.destinationOptionsLoading}
             selectedInstances={this.props.wizardData.selectedInstances}
             selectedInstances={this.props.wizardData.selectedInstances}
             fields={this.props.providerStore.optionsSchema}
             fields={this.props.providerStore.optionsSchema}
             onChange={this.props.onOptionsChange}
             onChange={this.props.onOptionsChange}

+ 15 - 3
src/components/pages/WizardPage/index.jsx

@@ -181,6 +181,11 @@ class WizardPage extends React.Component<Props, State> {
   handleTargetEndpointChange(target: EndpointType) {
   handleTargetEndpointChange(target: EndpointType) {
     WizardStore.updateData({ target, networks: null, options: null })
     WizardStore.updateData({ target, networks: null, options: null })
     WizardStore.setPermalink(WizardStore.data)
     WizardStore.setPermalink(WizardStore.data)
+    // Preload destination options schema
+    ProviderStore.loadOptionsSchema(target.type, this.state.type).then(() => {
+      // Preload destination options values
+      return ProviderStore.getDestinationOptions(target.id, target.type)
+    })
   }
   }
 
 
   handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
   handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
@@ -269,11 +274,18 @@ class WizardPage extends React.Component<Props, State> {
         }
         }
         break
         break
       }
       }
-      case 'options':
-        if (WizardStore.data.target) {
-          ProviderStore.loadOptionsSchema(WizardStore.data.target.type, this.state.type)
+      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)
         }
         }
         break
         break
+      }
       case 'networks':
       case 'networks':
         if (WizardStore.data.source && WizardStore.data.selectedInstances) {
         if (WizardStore.data.source && WizardStore.data.selectedInstances) {
           InstanceStore.loadInstancesDetails(WizardStore.data.source.id, WizardStore.data.selectedInstances)
           InstanceStore.loadInstancesDetails(WizardStore.data.source.id, WizardStore.data.selectedInstances)

+ 3 - 0
src/config.js

@@ -72,4 +72,7 @@ export const wizardConfig = {
   instancesItemsPerPage: 6,
   instancesItemsPerPage: 6,
 }
 }
 
 
+// Providers for which `destination-options` API call will be made
+export const providersWithExtraOptions = ['azure']
+
 export const basename = process.env.PUBLIC_PATH
 export const basename = process.env.PUBLIC_PATH

+ 15 - 1
src/sources/ProviderSource.js

@@ -17,11 +17,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import cookie from 'js-cookie'
 import cookie from 'js-cookie'
 
 
 import Api from '../utils/ApiCaller'
 import Api from '../utils/ApiCaller'
-
 import { servicesUrl, providerTypes } from '../config'
 import { servicesUrl, providerTypes } from '../config'
 import { SchemaParser } from './Schemas'
 import { SchemaParser } from './Schemas'
 import type { Field } from '../types/Field'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
 import type { Providers } from '../types/Providers'
+import type { DestinationOption } from '../types/Endpoint'
 
 
 class ProviderSource {
 class ProviderSource {
   static getConnectionInfoSchema(providerName: string): Promise<Field[]> {
   static getConnectionInfoSchema(providerName: string): Promise<Field[]> {
@@ -67,6 +67,20 @@ class ProviderSource {
       }, reject).catch(reject)
       }, reject).catch(reject)
     })
     })
   }
   }
+
+  static getDestinationOptions(endpointId: string): Promise<DestinationOption[]> {
+    return new Promise((resolve, reject) => {
+      let projectId = cookie.get('projectId')
+
+      Api.sendAjaxRequest({
+        url: `${servicesUrl.coriolis}/${projectId || 'null'}/endpoints/${endpointId}/destination-options`,
+        method: 'GET',
+      }).then(response => {
+        let options = response.data.destination_options
+        resolve(options)
+      }).catch(() => { reject() })
+    })
+  }
 }
 }
 
 
 export default ProviderSource
 export default ProviderSource

+ 26 - 0
src/stores/ProviderStore.js

@@ -17,6 +17,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { observable, action } from 'mobx'
 import { observable, action } from 'mobx'
 
 
 import ProviderSource from '../sources/ProviderSource'
 import ProviderSource from '../sources/ProviderSource'
+import { providersWithExtraOptions } from '../config.js'
+import type { DestinationOption } from '../types/Endpoint'
 import type { Field } from '../types/Field'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
 import type { Providers } from '../types/Providers'
 
 
@@ -27,6 +29,8 @@ class ProviderStore {
   @observable providersLoading: boolean = false
   @observable providersLoading: boolean = false
   @observable optionsSchema: Field[] = []
   @observable optionsSchema: Field[] = []
   @observable optionsSchemaLoading: boolean = false
   @observable optionsSchemaLoading: boolean = false
+  @observable destinationOptions: DestinationOption[] = []
+  @observable destinationOptionsLoading: boolean = false
 
 
   @action getConnectionInfoSchema(providerName: string): Promise<void> {
   @action getConnectionInfoSchema(providerName: string): Promise<void> {
     this.connectionSchemaLoading = true
     this.connectionSchemaLoading = true
@@ -65,6 +69,28 @@ class ProviderStore {
       this.optionsSchemaLoading = false
       this.optionsSchemaLoading = false
     })
     })
   }
   }
+
+   @action getDestinationOptions(endpointId: string, provider: string): Promise<void> {
+    if (!providersWithExtraOptions.find(p => p === provider)) {
+      return Promise.resolve()
+    }
+
+    this.destinationOptionsLoading = true
+    return ProviderSource.getDestinationOptions(endpointId).then(options => {
+      this.optionsSchema.forEach(field => {
+        let fieldValues = options.find(f => f.name === field.name)
+        if (fieldValues) {
+          // $FlowIgnore
+          field.enum = [...fieldValues.values]
+          if (fieldValues.config_default) {
+            field.default = typeof fieldValues.config_default === 'string' ? fieldValues.config_default : fieldValues.config_default.id
+          }
+        }
+      })
+      this.destinationOptions = options
+      this.destinationOptionsLoading = false
+    }).catch(() => { this.destinationOptionsLoading = false })
+  }
 }
 }
 
 
 export default new ProviderStore()
 export default new ProviderStore()

+ 6 - 0
src/types/Endpoint.js

@@ -30,3 +30,9 @@ export type Endpoint = {
     [string]: mixed
     [string]: mixed
   },
   },
 }
 }
+
+export type DestinationOption = {
+  name: string,
+  values: string[] | {name: string, id: string}[],
+  config_default: string | {name: string, id: string},
+}

+ 1 - 1
src/types/Field.js

@@ -18,7 +18,7 @@ export type Field = {
   name: string,
   name: string,
   type?: string,
   type?: string,
   value?: any,
   value?: any,
-  enum?: string[],
+  enum?: string[] | {name: string, id: string}[],
   required?: boolean | (value: any) => boolean,
   required?: boolean | (value: any) => boolean,
   default?: any,
   default?: any,
   items?: Field[],
   items?: Field[],