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

Display options names instead of IDs

Options names are shown in Wizard Summary page and in Replica /
Migration Details pages, where only the IDs were displayed previously.

For the Wizard Summary page, no extra calls are necessary to display the
option names instead of IDs. For the Replica / Migration Details page,
an extra API call is required, although the call's response is cached
for 15 minutes.

A small loading icon is displayed in the Replica / Migration Details
page, next to the 'Properties' label, while the options names are being
loaded.
Sergiu Miclea 6 лет назад
Родитель
Сommit
b4bf9468ab

+ 22 - 11
src/components/organisms/MainDetails/MainDetails.jsx

@@ -30,6 +30,9 @@ import type { Instance } from '../../../types/Instance'
 import type { MainItem } from '../../../types/MainItem'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Network } from '../../../types/Network'
+import type { Field as FieldType } from '../../../types/Field'
+import fieldHelper from '../../../types/Field'
+
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 import DateUtils from '../../../utils/DateUtils'
@@ -69,6 +72,11 @@ const Label = styled.div`
   color: ${Palette.grayscale[3]};
   font-weight: ${StyleProps.fontWeights.medium};
   text-transform: uppercase;
+  display: flex;
+  align-items: center;
+`
+const StatusIconStub = styled.div`
+  ${StyleProps.exactSize('16px')}
 `
 const Value = styled.div`
   display: ${props => props.flex ? 'flex' : props.block ? 'block' : 'inline-table'};
@@ -112,6 +120,8 @@ const PropertyValue = styled.div`
 
 type Props = {
   item: ?MainItem,
+  destinationSchema: FieldType[],
+  destinationSchemaLoading: boolean,
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
   endpoints: Endpoint[],
@@ -193,17 +203,11 @@ class MainDetails extends React.Component<Props> {
   }
 
   renderPropertiesTable(propertyNames: string[]) {
-    let getValue = (value: any) => {
-      if (value === true) {
-        return 'Yes'
-      }
-      if (value === false) {
-        return 'No'
-      }
+    let getValue = (name: string, value: any) => {
       if (value.join && value.length && value[0].destination && value[0].source) {
         return value.map(v => `${v.source}=${v.destination}`).join(', ')
       }
-      return value.toString()
+      return fieldHelper.getValueAlias(name, value, this.props.destinationSchema)
     }
 
     let properties = []
@@ -222,13 +226,17 @@ class MainDetails extends React.Component<Props> {
           if (p === 'disk_mappings') {
             return null
           }
+          let fieldName = pn
+          if (fieldName === 'migr_image_map') {
+            fieldName = `${p}_os_image`
+          }
           return {
             label: `${label} - ${LabelDictionary.get(p)}`,
-            value: getValue(value[p]),
+            value: getValue(fieldName, value[p]),
           }
         }))
       } else {
-        properties.push({ label, value: getValue(value) })
+        properties.push({ label, value: getValue(pn, value) })
       }
     })
 
@@ -336,7 +344,10 @@ class MainDetails extends React.Component<Props> {
           {propertyNames.length > 0 ? (
             <Row>
               <Field>
-                <Label>Properties</Label>
+                <Label>Properties{this.props.destinationSchemaLoading ? (
+                  <StatusIcon status="RUNNING" style={{ marginLeft: '8px' }} />
+                ) : <StatusIconStub />
+                }</Label>
                 <Value block>{this.renderPropertiesTable(propertyNames)}</Value>
               </Field>
             </Row>

+ 5 - 0
src/components/organisms/MigrationDetailsContent/MigrationDetailsContent.jsx

@@ -27,6 +27,7 @@ import StyleProps from '../../styleUtils/StyleProps'
 import type { Instance } from '../../../types/Instance'
 import type { MainItem } from '../../../types/MainItem'
 import type { Endpoint } from '../../../types/Endpoint'
+import type { Field } from '../../../types/Field'
 
 const Wrapper = styled.div`
   display: flex;
@@ -57,6 +58,8 @@ type Props = {
   detailsLoading: boolean,
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
+  destinationSchema: Field[],
+  destinationSchemaLoading: boolean,
   endpoints: Endpoint[],
   page: string,
   onDeleteMigrationClick: () => void,
@@ -85,6 +88,8 @@ class MigrationDetailsContent extends React.Component<Props> {
         item={this.props.item}
         instancesDetails={this.props.instancesDetails}
         instancesDetailsLoading={this.props.instancesDetailsLoading}
+        destinationSchema={this.props.destinationSchema}
+        destinationSchemaLoading={this.props.destinationSchemaLoading}
         endpoints={this.props.endpoints}
         bottomControls={this.renderBottomControls()}
         loading={this.props.detailsLoading}

+ 5 - 0
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.jsx

@@ -29,6 +29,7 @@ import type { MainItem } from '../../../types/MainItem'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Execution } from '../../../types/Execution'
 import type { Network } from '../../../types/Network'
+import type { Field } from '../../../types/Field'
 import type { Schedule as ScheduleType } from '../../../types/Schedule'
 import StyleProps from '../../styleUtils/StyleProps'
 
@@ -72,6 +73,8 @@ type TimezoneValue = 'utc' | 'local'
 type Props = {
   item: ?MainItem,
   endpoints: Endpoint[],
+  destinationSchema: Field[],
+  destinationSchemaLoading: boolean,
   networks: Network[],
   instancesDetails: Instance[],
   instancesDetailsLoading: boolean,
@@ -155,6 +158,8 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
     return (
       <MainDetails
         item={this.props.item}
+        destinationSchema={this.props.destinationSchema}
+        destinationSchemaLoading={this.props.destinationSchemaLoading}
         instancesDetails={this.props.instancesDetails}
         instancesDetailsLoading={this.props.instancesDetailsLoading}
         loading={this.props.detailsLoading}

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

@@ -429,6 +429,8 @@ class WizardPageContent extends React.Component<Props, State> {
             storageMap={this.props.storageMap}
             wizardType={this.props.type}
             instancesDetails={this.props.instanceStore.instancesDetails}
+            sourceSchema={this.props.providerStore.sourceSchema}
+            destinationSchema={this.props.providerStore.destinationSchema}
           />
         )
         break

+ 9 - 18
src/components/organisms/WizardSummary/WizardSummary.jsx

@@ -29,7 +29,9 @@ import type { Schedule } from '../../../types/Schedule'
 import type { WizardData } from '../../../types/WizardData'
 import type { StorageMap, StorageBackend } from '../../../types/Endpoint'
 import type { Instance, Disk } from '../../../types/Instance'
+import type { Field } from '../../../types/Field'
 
+import fieldHelper from '../../../types/Field'
 import { getDisks } from '../WizardStorage'
 
 import networkArrowImage from './images/network-arrow.svg'
@@ -158,10 +160,12 @@ type Props = {
   schedules: Schedule[],
   storageMap: StorageMap[],
   instancesDetails: Instance[],
+  sourceSchema: Field[],
+  destinationSchema: Field[],
 }
 @observer
 class WizardSummary extends React.Component<Props> {
-  getDefaultOption(fieldName: string) {
+  getDefaultOption(fieldName: string): boolean {
     if (this.props.data.destOptions && this.props.data.destOptions[fieldName] === false) {
       return false
     }
@@ -237,19 +241,6 @@ class WizardSummary extends React.Component<Props> {
     )
   }
 
-  renderOptionValue(value: any) {
-    if (value === true) {
-      return 'Yes'
-    }
-    if (value === false) {
-      return 'No'
-    }
-    if (value.join) {
-      return value.join(', ')
-    }
-    return value
-  }
-
   renderSourceOptionsSection() {
     let data = this.props.data
     let type = this.props.wizardType.charAt(0).toUpperCase() + this.props.wizardType.substr(1)
@@ -272,7 +263,7 @@ class WizardSummary extends React.Component<Props> {
                   {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
                 </OptionLabel>
                 <OptionValue>{
-                  this.renderOptionValue(data.sourceOptions && data.sourceOptions[optionName])
+                  fieldHelper.getValueAlias(optionName, data.sourceOptions && data.sourceOptions[optionName], this.props.sourceSchema)
                 }</OptionValue>
               </Option>
             )
@@ -289,14 +280,14 @@ class WizardSummary extends React.Component<Props> {
     let executeNowOption = (
       <Option>
         <OptionLabel>Execute now?</OptionLabel>
-        <OptionValue>{this.renderOptionValue(this.getDefaultOption('execute_now'))}</OptionValue>
+        <OptionValue>{this.getDefaultOption('execute_now') ? 'Yes' : 'No'}</OptionValue>
       </Option>
     )
 
     let separateVmOption = (
       <Option>
         <OptionLabel>Separate {type}/VM?</OptionLabel>
-        <OptionValue>{this.renderOptionValue(this.getDefaultOption('separate_vm'))}</OptionValue>
+        <OptionValue>{this.getDefaultOption('separate_vm') ? 'Yes' : 'No'}</OptionValue>
       </Option>
     )
 
@@ -321,7 +312,7 @@ class WizardSummary extends React.Component<Props> {
                   {optionName.split('/').map(n => LabelDictionary.get(n)).join(' - ')}
                 </OptionLabel>
                 <OptionValue data-test-id={`wSummary-optionValue-${optionName}`}>{
-                  this.renderOptionValue(data.destOptions && data.destOptions[optionName])
+                  fieldHelper.getValueAlias(optionName, data.destOptions && data.destOptions[optionName], this.props.destinationSchema)
                 }</OptionValue>
               </Option>
             )

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

@@ -21,7 +21,7 @@ import WizardSummary from '.'
 
 const wrap = props => new TW(shallow(
   // $FlowIgnore
-  <WizardSummary storageMap={[]} instancesDetails={[]} {...props} />
+  <WizardSummary storageMap={[]} instancesDetails={[]} sourceSchema={[]} destinationSchema={[]} {...props} />
 ), 'wSummary')
 
 let schedules = [

+ 37 - 3
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.jsx

@@ -33,6 +33,7 @@ import endpointStore from '../../../stores/EndpointStore'
 import notificationStore from '../../../stores/NotificationStore'
 import networkStore from '../../../stores/NetworkStore'
 import instanceStore from '../../../stores/InstanceStore'
+import providerStore from '../../../stores/ProviderStore'
 import configLoader from '../../../utils/Config'
 
 import migrationImage from './images/migration.svg'
@@ -66,8 +67,38 @@ class MigrationDetailsPage extends React.Component<Props, State> {
   componentWillMount() {
     document.title = 'Migration Details'
 
-    endpointStore.getEndpoints({ showLoading: true })
-    this.loadMigrationWithInstances(this.props.match.params.id, true)
+    let loadMigration = async () => {
+      await Promise.all([
+        endpointStore.getEndpoints({ showLoading: true }),
+        this.loadMigrationWithInstances(this.props.match.params.id, true),
+      ])
+      let details = migrationStore.migrationDetails
+      if (!details) {
+        return
+      }
+      let endpoint = endpointStore.endpoints.find(e => e.id === details.destination_endpoint_id)
+      if (!endpoint) {
+        return
+      }
+      // This allows the values to be displayed with their allocated names instead of their IDs
+      await providerStore.loadOptionsSchema({
+        providerName: endpoint.type,
+        schemaType: details.type,
+        optionsType: 'destination',
+        useCache: true,
+        quietError: true,
+      })
+      await providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: details.destination_endpoint_id,
+        providerName: endpoint.type,
+        envData: details.destination_environment,
+        useCache: true,
+        quietError: true,
+      })
+    }
+    loadMigration()
+
     this.pollData()
   }
 
@@ -266,6 +297,10 @@ class MigrationDetailsPage extends React.Component<Props, State> {
             item={migrationStore.migrationDetails}
             instancesDetails={instanceStore.instancesDetails}
             instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+            destinationSchema={providerStore.destinationSchema}
+            destinationSchemaLoading={providerStore.destinationSchemaLoading
+              || providerStore.destinationOptionsPrimaryLoading
+              || providerStore.destinationOptionsSecondaryLoading}
             endpoints={endpointStore.endpoints}
             page={this.props.match.params.page || ''}
             detailsLoading={endpointStore.loading || migrationStore.detailsLoading}
@@ -302,7 +337,6 @@ class MigrationDetailsPage extends React.Component<Props, State> {
           </Modal>
         ) : null}
         {this.renderEditModal()}
-
       </Wrapper>
     )
   }

+ 36 - 2
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.jsx

@@ -86,8 +86,38 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
   componentWillMount() {
     document.title = 'Replica Details'
 
-    this.loadReplicaWithInstances(this.props.match.params.id, true)
-    endpointStore.getEndpoints({ showLoading: true })
+    let loadReplica = async () => {
+      await Promise.all([
+        this.loadReplicaWithInstances(this.props.match.params.id, true),
+        endpointStore.getEndpoints({ showLoading: true }),
+      ])
+      let details = replicaStore.replicaDetails
+      if (!details) {
+        return
+      }
+      let endpoint = endpointStore.endpoints.find(e => e.id === details.destination_endpoint_id)
+      if (!endpoint) {
+        return
+      }
+      // This allows the values to be displayed with their allocated names instead of their IDs
+      await providerStore.loadOptionsSchema({
+        providerName: endpoint.type,
+        schemaType: details.type,
+        optionsType: 'destination',
+        useCache: true,
+        quietError: true,
+      })
+      await providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: details.destination_endpoint_id,
+        providerName: endpoint.type,
+        envData: details.destination_environment,
+        useCache: true,
+        quietError: true,
+      })
+    }
+    loadReplica()
+
     scheduleStore.getSchedules(this.props.match.params.id)
     this.pollData(true)
   }
@@ -422,6 +452,10 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
             scheduleStore={scheduleStore}
             networks={networkStore.networks}
             detailsLoading={replicaStore.detailsLoading || endpointStore.loading}
+            destinationSchema={providerStore.destinationSchema}
+            destinationSchemaLoading={providerStore.destinationSchemaLoading
+              || providerStore.destinationOptionsPrimaryLoading
+              || providerStore.destinationOptionsSecondaryLoading}
             executionsLoading={replicaStore.executionsLoading}
             page={this.props.match.params.page || ''}
             onCancelExecutionClick={execution => { this.handleCancelExecution(execution) }}

+ 4 - 2
src/sources/ProviderSource.js

@@ -34,7 +34,7 @@ class ProviderSource {
     return response.data.providers
   }
 
-  async loadOptionsSchema(providerName: string, schemaType: 'migration' | 'replica', optionsType: 'source' | 'destination', useCache?: ?boolean): Promise<Field[]> {
+  async loadOptionsSchema(providerName: string, schemaType: 'migration' | 'replica', optionsType: 'source' | 'destination', useCache?: ?boolean, quietError?: ?boolean): Promise<Field[]> {
     let schemaTypeInt = schemaType === 'migration' ?
       optionsType === 'source' ? providerTypes.SOURCE_MIGRATION : providerTypes.TARGET_MIGRATION :
       optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
@@ -42,13 +42,14 @@ class ProviderSource {
     let response = await Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`,
       cache: useCache,
+      quietError,
     })
     let schema = optionsType === 'source' ? response.data.schemas.source_environment_schema : response.data.schemas.destination_environment_schema
     let fields = SchemaParser.optionsSchemaToFields(providerName, schema)
     return fields
   }
 
-  async getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }, cache?: ?boolean): Promise<OptionValues[]> {
+  async getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }, cache?: ?boolean, quietError?: boolean): Promise<OptionValues[]> {
     let envString = ''
     if (envData) {
       envString = `?env=${btoa(JSON.stringify(envData))}`
@@ -60,6 +61,7 @@ class ProviderSource {
       url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints/${endpointId}/${callName}${envString}`,
       cache,
       cancelId: endpointId,
+      quietError,
     })
     return response.data[fieldName]
   }

+ 6 - 4
src/stores/ProviderStore.js

@@ -152,8 +152,9 @@ class ProviderStore {
     schemaType: 'migration' | 'replica',
     optionsType: 'source' | 'destination',
     useCache?: boolean,
+    quietError?: boolean,
   }): Promise<void> {
-    let { schemaType, providerName, optionsType, useCache } = options
+    let { schemaType, providerName, optionsType, useCache, quietError } = options
     if (optionsType === 'source') {
       this.lastSourceSchemaType = schemaType
     } else {
@@ -167,7 +168,7 @@ class ProviderStore {
     }
 
     try {
-      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, schemaType, optionsType, useCache)
+      let fields: Field[] = await ProviderSource.loadOptionsSchema(providerName, schemaType, optionsType, useCache, quietError)
       this.loadOptionsSchemaSuccess(fields, optionsType)
     } catch (err) {
       throw err
@@ -198,8 +199,9 @@ class ProviderStore {
     providerName: string,
     envData?: { [string]: mixed },
     useCache?: boolean,
+    quietError?: boolean,
   }): Promise<OptionValues[]> {
-    let { providerName, optionsType, endpointId, envData, useCache } = config
+    let { providerName, optionsType, endpointId, envData, useCache, quietError } = config
     let providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS
 
     await this.loadProviders()
@@ -216,7 +218,7 @@ class ProviderStore {
     this.getOptionsValuesStart(optionsType, !envData)
 
     try {
-      let options = await ProviderSource.getOptionsValues(optionsType, endpointId, envData, useCache)
+      let options = await ProviderSource.getOptionsValues(optionsType, endpointId, envData, useCache, quietError)
       this.getOptionsValuesSuccess(optionsType, providerName, options)
       return options
     } catch (err) {

+ 45 - 0
src/types/Field.js

@@ -36,3 +36,48 @@ export type Field = {
   title?: string,
   description?: string,
 }
+
+const migrationImageOsTypes = ['windows', 'linux']
+
+class FieldHelper {
+  getValueAlias(name: string, value: any, fields: Field[]): string {
+    if (value === true) {
+      return 'Yes'
+    }
+    if (value === false) {
+      return 'No'
+    }
+    let field = fields.find(f => f.name === name)
+    let findInEnum = (v: any) => {
+      let valueName = v
+      if (field && field.enum) {
+        let enumObject = field.enum.find(e => e.id ? e.id === v : false)
+        if (enumObject && enumObject.name) {
+          valueName = enumObject.name
+        }
+      }
+      return valueName
+    }
+    if (value.join) {
+      return value.map(v => findInEnum(v)).join(', ')
+    }
+
+    let isImageMapField = migrationImageOsTypes.find(os => `${os}_os_image` === name)
+    if (isImageMapField) {
+      let migrImageField = fields.find(f => f.name === 'migr_image_map')
+      if (migrImageField && migrImageField.properties) {
+        let imageField = migrImageField.properties.find(p => p.name === name)
+        if (imageField && imageField.enum) {
+          let imageFieldValueObject = imageField.enum.find(e => e.id ? e.id === value : false)
+          if (imageFieldValueObject) {
+            return imageFieldValueObject.name
+          }
+        }
+      }
+    }
+    // $FlowIssue
+    return findInEnum(value)
+  }
+}
+
+export default new FieldHelper()

+ 1 - 1
src/types/MainItem.js

@@ -50,7 +50,7 @@ export type MainItem = {
   origin_endpoint_id: string,
   destination_endpoint_id: string,
   instances: string[],
-  type: string,
+  type: 'replica' | 'migration',
   info: { [string]: MainItemInfo },
   destination_environment: { [string]: mixed },
   source_environment: { [string]: mixed },

+ 1 - 1
src/utils/ApiCaller.js

@@ -34,7 +34,7 @@ type RequestOptions = {
   headers?: { [string]: string },
   data?: any,
   responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream',
-  quietError?: boolean,
+  quietError?: ?boolean,
   skipLog?: ?boolean,
   cache?: ?boolean,
   cacheFor?: ?number,

+ 1 - 0
src/utils/Cacher.js

@@ -37,6 +37,7 @@ class Cacher {
       data,
       createdAt: new Date().toISOString(),
     }
+
     localStorage.setItem(STORE, JSON.stringify(storage))
   }
 }