Procházet zdrojové kódy

Merge pull request #434 from smiclea/improve-options-loading

Improve options loading UX
Dorin Paslaru před 6 roky
rodič
revize
6fcecbdbac

+ 15 - 5
src/components/organisms/EditReplica/EditReplica.jsx

@@ -182,9 +182,9 @@ class EditReplica extends React.Component<Props, State> {
 
   isUpdateDisabled() {
     let isLoadingDestOptions = this.state.selectedPanel === 'dest_options'
-      && (providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading)
+      && (providerStore.destinationSchemaLoading || providerStore.destinationOptionsPrimaryLoading)
     let isLoadingSourceOptions = this.state.selectedPanel === 'source_options'
-      && (providerStore.sourceSchemaLoading || providerStore.sourceOptionsLoading)
+      && (providerStore.sourceSchemaLoading || providerStore.sourceOptionsPrimaryLoading)
     let isLoadingNetwork = this.state.selectedPanel === 'network_mapping' && this.props.instancesDetailsLoading
     let isLoadingStorage = this.state.selectedPanel === 'storage_mapping'
       && (this.props.instancesDetailsLoading || endpointStore.storageLoading)
@@ -395,14 +395,23 @@ class EditReplica extends React.Component<Props, State> {
   }
 
   renderOptions(type: 'source' | 'destination') {
-    let loading = type === 'source' ? providerStore.sourceSchemaLoading : providerStore.destinationSchemaLoading
+    let loading = type === 'source' ? (providerStore.sourceSchemaLoading || providerStore.sourceOptionsPrimaryLoading)
+      : (providerStore.destinationSchemaLoading || providerStore.destinationOptionsPrimaryLoading)
     if (loading) {
       return this.renderLoading(`Loading ${type === 'source' ? 'source' : 'target'} options ...`)
     }
-    let optionsLoading = type === 'source' ? providerStore.sourceOptionsLoading : providerStore.destinationOptionsLoading
+    let optionsLoading = type === 'source' ? providerStore.sourceOptionsSecondaryLoading
+      : providerStore.destinationOptionsSecondaryLoading
     let schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
     let fields = this.props.type === 'replica' ? schema.filter(f => !f.readOnly) : schema
-
+    let extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => {
+      let provider = type === 'source' ? this.props.sourceEndpoint.type : this.props.destinationEndpoint.type
+      return o.name === provider && o.types.find(t => t === type)
+    })
+    let optionsLoadingSkipFields = []
+    if (extraOptionsConfig) {
+      optionsLoadingSkipFields = extraOptionsConfig.requiredFields
+    }
     return (
       <WizardOptions
         wizardType={`replica-${type}-options-edit`}
@@ -420,6 +429,7 @@ class EditReplica extends React.Component<Props, State> {
         useAdvancedOptions
         layout="modal"
         optionsLoading={optionsLoading}
+        optionsLoadingSkipFields={[...optionsLoadingSkipFields, 'description', 'execute_now', 'execute_now_options', 'default_storage']}
       />
     )
   }

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

@@ -86,14 +86,15 @@ type Props = {
   storageConfigDefault?: string,
   onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
   wizardType: string,
-  loading?: boolean,
   columnStyle?: { [string]: mixed },
   oneColumnStyle?: { [string]: mixed },
   fieldWidth?: number,
   onScrollableRef?: (ref: HTMLElement) => void,
   availableHeight?: number,
   layout?: 'page' | 'modal',
+  loading?: boolean,
   optionsLoading?: boolean,
+  optionsLoadingSkipFields?: string[],
 }
 @observer
 class WizardOptions extends React.Component<Props> {
@@ -184,6 +185,7 @@ class WizardOptions extends React.Component<Props> {
         onChange: value => { this.props.onChange(field, value) },
       }
     }
+    let optionsLoadingReqFields = this.props.optionsLoadingSkipFields || []
     return (
       <FieldInputStyled
         layout={this.props.layout || 'page'}
@@ -198,7 +200,7 @@ class WizardOptions extends React.Component<Props> {
         width={this.props.fieldWidth || StyleProps.inputSizes.wizard.width}
         label={field.label}
         nullableBoolean={field.nullableBoolean}
-        disabledLoading={this.props.optionsLoading}
+        disabledLoading={this.props.optionsLoading && !optionsLoadingReqFields.find(fn => fn === field.name)}
         {...additionalProps}
       />
     )

+ 21 - 4
src/components/organisms/WizardPageContent/WizardPageContent.jsx

@@ -33,6 +33,8 @@ import WizardSummary from '../WizardSummary'
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 import { providerTypes, wizardPages } from '../../../constants'
+import configLoader from '../../../utils/Config'
+
 import type { WizardData, WizardPage } from '../../../types/WizardData'
 import type { Endpoint, StorageBackend, StorageMap } from '../../../types/Endpoint'
 import type { Instance, Nic, Disk } from '../../../types/Instance'
@@ -279,6 +281,19 @@ class WizardPageContent extends React.Component<Props, State> {
   renderBody() {
     let body = null
 
+    let getOptionsLoadingSkipFields = (type: 'source' | 'destination') => {
+      let extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => {
+        let provider = type === 'source' ? this.props.wizardData.source && this.props.wizardData.source.type
+          : this.props.wizardData.target && this.props.wizardData.target.type
+        return o.name === provider && o.types.find(t => t === type)
+      })
+      let optionsLoadingRequiredFields = []
+      if (extraOptionsConfig) {
+        optionsLoadingRequiredFields = extraOptionsConfig.requiredFields
+      }
+      return optionsLoadingRequiredFields
+    }
+
     switch (this.props.page.id) {
       case 'type':
         body = (
@@ -338,8 +353,9 @@ class WizardPageContent extends React.Component<Props, State> {
       case 'source-options':
         body = (
           <WizardOptions
-            loading={this.props.providerStore.sourceSchemaLoading}
-            optionsLoading={this.props.providerStore.sourceOptionsLoading}
+            loading={this.props.providerStore.sourceSchemaLoading || this.props.providerStore.sourceOptionsPrimaryLoading}
+            optionsLoading={this.props.providerStore.sourceOptionsSecondaryLoading}
+            optionsLoadingSkipFields={getOptionsLoadingSkipFields('source')}
             fields={this.props.providerStore.sourceSchema}
             onChange={this.props.onSourceOptionsChange}
             data={this.props.wizardData.sourceOptions}
@@ -352,8 +368,9 @@ class WizardPageContent extends React.Component<Props, State> {
       case 'dest-options':
         body = (
           <WizardOptions
-            loading={this.props.providerStore.destinationSchemaLoading}
-            optionsLoading={this.props.providerStore.destinationOptionsLoading}
+            loading={this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsPrimaryLoading}
+            optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading}
+            optionsLoadingSkipFields={[...getOptionsLoadingSkipFields('destination'), 'description', 'execute_now', 'execute_now_options', 'default_storage']}
             selectedInstances={this.props.wizardData.selectedInstances}
             fields={this.props.providerStore.destinationSchema}
             onChange={this.props.onDestOptionsChange}

+ 1 - 1
src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.jsx

@@ -491,7 +491,7 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
               targetEndpointsLoading={endpointStore.loading}
               loadingVmSizes={this.state.loadingTargetVmSizes}
               sourceEndpointsLoading={endpointsLoading}
-              targetOptionsLoading={providerStore.destinationOptionsLoading}
+              targetOptionsLoading={providerStore.destinationOptionsPrimaryLoading || providerStore.destinationOptionsSecondaryLoading}
               targetEndpoints={this.getTargetEndpoints()}
               targetEndpoint={localData.endpoint}
               onTargetEndpointChange={endpoint => { this.handleTargetEndpointChange(endpoint) }}

+ 1 - 0
src/sources/ProviderSource.js

@@ -59,6 +59,7 @@ class ProviderSource {
     let response = await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/${callName}${envString}`,
       cache,
+      cancelId: endpointId,
     })
     return response.data[fieldName]
   }

+ 40 - 12
src/stores/ProviderStore.js

@@ -17,6 +17,8 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { observable, action, computed, runInAction } from 'mobx'
 
 import ProviderSource from '../sources/ProviderSource'
+import apiCaller from '../utils/ApiCaller'
+
 import configLoader from '../utils/Config'
 import { providerTypes } from '../constants'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
@@ -83,9 +85,15 @@ class ProviderStore {
   @observable destinationSchema: Field[] = []
   @observable destinationSchemaLoading: boolean = false
   @observable destinationOptions: OptionValues[] = []
-  @observable destinationOptionsLoading: boolean = false
+  // Set to true while loading the options call for the first set of options
+  @observable destinationOptionsPrimaryLoading: boolean = false
+  // Set to true while loading the options call with a set of values in the 'env' parameter
+  @observable destinationOptionsSecondaryLoading: boolean = false
   @observable sourceOptions: OptionValues[] = []
-  @observable sourceOptionsLoading: boolean = false
+  // Set to true while loading the options call for the first set of options
+  @observable sourceOptionsPrimaryLoading: boolean = false
+  // Set to true while loading the options call with a set of values in the 'env' parameter
+  @observable sourceOptionsSecondaryLoading: boolean = false
   @observable sourceSchema: Field[] = []
   @observable sourceSchemaLoading: boolean = false
 
@@ -203,7 +211,9 @@ class ProviderStore {
       return []
     }
 
-    this.getOptionsValuesStart(optionsType)
+    let canceled = false
+    apiCaller.cancelRequests(endpointId)
+    this.getOptionsValuesStart(optionsType, !envData)
 
     try {
       let options = await ProviderSource.getOptionsValues(optionsType, endpointId, envData, useCache)
@@ -211,6 +221,10 @@ class ProviderStore {
       return options
     } catch (err) {
       console.error(err)
+      canceled = err ? err.canceled : false
+      if (canceled) {
+        return optionsType === 'source' ? [...this.sourceOptions] : [...this.destinationOptions]
+      }
       let schemaType = optionsType === 'source' ? this.lastSourceSchemaType : this.lastDestinationSchemaType
       if (!envData) {
         return []
@@ -218,25 +232,39 @@ class ProviderStore {
       let newOptions = await this.loadOptionsSchema({ providerName, schemaType, optionsType })
       return newOptions
     } finally {
-      this.getOptionsValuesDone(optionsType)
+      if (!canceled) {
+        this.getOptionsValuesDone(optionsType, !envData)
+      }
     }
   }
 
-  @action getOptionsValuesStart(optionsType: 'source' | 'destination') {
+  @action getOptionsValuesStart(optionsType: 'source' | 'destination', isPrimary: boolean) {
     if (optionsType === 'source') {
-      this.sourceOptionsLoading = true
-      this.sourceOptions = []
-    } else {
-      this.destinationOptionsLoading = true
+      if (isPrimary) {
+        this.sourceOptions = []
+        this.sourceOptionsPrimaryLoading = true
+      } else {
+        this.sourceOptionsSecondaryLoading = true
+      }
+    } else if (isPrimary) {
       this.destinationOptions = []
+      this.destinationOptionsPrimaryLoading = true
+    } else {
+      this.destinationOptionsSecondaryLoading = true
     }
   }
 
-  @action getOptionsValuesDone(optionsType: 'source' | 'destination') {
+  @action getOptionsValuesDone(optionsType: 'source' | 'destination', isPrimary: boolean) {
     if (optionsType === 'source') {
-      this.sourceOptionsLoading = false
+      if (isPrimary) {
+        this.sourceOptionsPrimaryLoading = false
+      } else {
+        this.sourceOptionsSecondaryLoading = false
+      }
+    } else if (isPrimary) {
+      this.destinationOptionsPrimaryLoading = false
     } else {
-      this.destinationOptionsLoading = false
+      this.destinationOptionsSecondaryLoading = false
     }
   }