Quellcode durchsuchen

Handle empty source options schema response

If a source provider returns an empty schema body or the request itself
fails due to it not being implemented by the provider, a 'No
Source Options' message block is displayed in the Wizard Source Options
page.
Sergiu Miclea vor 6 Jahren
Ursprung
Commit
1b4aee6c9c

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

@@ -485,6 +485,7 @@ class EditReplica extends React.Component<Props, State> {
         availableHeight={384}
         useAdvancedOptions
         layout="modal"
+        isSource={type === 'source'}
         optionsLoading={optionsLoading}
         optionsLoadingSkipFields={[...optionsLoadingSkipFields, 'description', 'execute_now', 'execute_now_options',
           'default_storage', ...migrationFields.map(f => f.name)]}

+ 35 - 0
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -32,6 +32,8 @@ import { executionOptions, migrationFields } from '../../../constants'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import Palette from '../../styleUtils/Palette'
 
+import endpointImage from './images/endpoint.svg'
+
 const Wrapper = styled.div`
   display: flex;
   min-height: 0;
@@ -91,6 +93,24 @@ const LoadingText = styled.div`
   margin-top: 38px;
   font-size: 18px;
 `
+const EndpointImage = styled.div`
+  ${StyleProps.exactSize('96px')};
+  background: url('${endpointImage}') center no-repeat;
+`
+const NoSourceFieldsWrapper = styled.div`
+  margin-top: 16px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`
+const NoSourceFieldsMessage = styled.div`
+  font-size: 18px;
+  margin-top: 16px;
+`
+const NoSourceFieldsSubMessage = styled.div`
+  margin-top: 16px;
+  color: ${Palette.grayscale[4]};
+`
 
 export const shouldRenderField = (field: Field) => {
   return (field.type !== 'array' || (field.enum && field.enum.length && field.enum.length > 0)) &&
@@ -103,6 +123,7 @@ type FieldRender = {
 }
 type Props = {
   fields: Field[],
+  isSource?: boolean,
   selectedInstances?: ?Instance[],
   data?: ?{ [string]: mixed },
   getFieldValue?: (fieldName: string, defaultValue: any) => any,
@@ -264,7 +285,21 @@ class WizardOptions extends React.Component<Props> {
     )
   }
 
+  renderNoFieldsMessage() {
+    return (
+      <NoSourceFieldsWrapper>
+        <EndpointImage />
+        <NoSourceFieldsMessage>No Source Options</NoSourceFieldsMessage>
+        <NoSourceFieldsSubMessage>There are no options for the specified source cloud provider.</NoSourceFieldsSubMessage>
+      </NoSourceFieldsWrapper>
+    )
+  }
+
   renderOptionsFields() {
+    if (this.props.fields.length === 0 && this.props.isSource) {
+      return this.renderNoFieldsMessage()
+    }
+
     let fieldsSchema: Field[] = this.getDefaultFieldsSchema()
     let nonNullableBooleans: string[] = fieldsSchema.filter(f => f.type === 'boolean').map(f => f.name)
 

+ 11 - 0
src/components/organisms/WizardOptions/images/endpoint.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="96px" height="96px" viewBox="5 3 38 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Symbols" stroke="none" stroke-width="0.7" fill="none" fill-rule="evenodd">
+        <g id="Icon/Endpoint/EndpointListItem">
+            <path d="M20.4,9 C15.74952,9 12.02568,12.8710948 11.82768,17.7118572 C8.45952,18.6784946 6,21.876125 6,25.6669192 C6,30.2463259 9.5676,34 13.92,34 L35.52,34 C39.0804,34 42,30.9281234 42,27.1820248 C42,24.2275689 40.128,21.7215842 37.61232,20.8367625 C37.45968,16.3952304 34.01664,12.787764 29.76,12.787764 C29.01192,12.787764 28.31568,12.9816975 27.62232,13.1907821 C26.07504,10.7135844 23.43624,9 20.4,9 L20.4,9 Z" id="Path-Copy" stroke="#0044CA" stroke-linecap="round" transform="translate(24.000000, 21.500000) scale(-1, 1) translate(-24.000000, -21.500000) "></path>
+            <path d="M21,25 L27,25" id="Stroke-7-Copy" stroke="#0044CA"  stroke-linecap="round"></path>
+            <path d="M34,27.0146641 C33.4491873,27.9107203 32.5141675,28.5 31.4536595,28.5 L29.0675166,28.5 C27.3734272,28.5 26,26.9959475 26,25.1400819 L26,24.8602106 C26,23.0043449 27.3734272,21.5 29.0675166,21.5 L31.4536595,21.5 C32.5133665,21.5 33.4473184,22.08811 33.998398,22.9829963" id="Stroke-9-Copy-3" stroke="#0044CA" stroke-linecap="round" transform="translate(30.000000, 25.000000) scale(-1, 1) translate(-30.000000, -25.000000) "></path>
+            <path d="M22,27.0146641 C21.4491873,27.9107203 20.5141675,28.5 19.4536595,28.5 L17.0675166,28.5 C15.3734272,28.5 14,26.9959475 14,25.1400819 L14,24.8602106 C14,23.0043449 15.3734272,21.5 17.0675166,21.5 L19.4536595,21.5 C20.5133665,21.5 21.4473184,22.08811 21.998398,22.9829963" id="Stroke-9-Copy-4" stroke="#0044CA" stroke-linecap="round"></path>
+        </g>
+    </g>
+</svg>

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

@@ -131,32 +131,34 @@ export const isOptionsPageValid = (data: ?any, schema: Field[]) => {
     return field.default != null
   }
 
-  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 && f.properties.filter(p => isValid(p)).length > 0) {
-        required = required.concat(f.properties.filter(p => p.required))
-      }
+  if (!schema || schema.length === 0) {
+    return true
+  }
 
-      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 required = schema.filter(f => f.required && f.type !== 'object')
+  schema.forEach(f => {
+    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))
+    }
 
-    let validFieldsCount = 0
-    required.forEach(f => {
-      if (isValid(f)) {
-        validFieldsCount += 1
+    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))
       }
-    })
+    }
+  })
 
-    if (validFieldsCount === required.length) {
-      return true
+  let validFieldsCount = 0
+  required.forEach(f => {
+    if (isValid(f)) {
+      validFieldsCount += 1
     }
+  })
+
+  if (validFieldsCount === required.length) {
+    return true
   }
 
   return false
@@ -418,6 +420,7 @@ class WizardPageContent extends React.Component<Props, State> {
             hasStorageMap={false}
             wizardType={`${this.props.type}-source-options`}
             layout="page"
+            isSource
           />
         )
         break

+ 15 - 1
src/components/pages/WizardPage/WizardPage.jsx

@@ -539,6 +539,20 @@ class WizardPage extends React.Component<Props, State> {
     this.separateVms()
   }
 
+  isNextButtonDisabled() {
+    let state = this.state.nextButtonDisabled
+
+    if (wizardStore.currentPage.id === 'dest-options') {
+      return providerStore.destinationSchemaLoading || state
+    }
+
+    if (wizardStore.currentPage.id === 'source-options') {
+      return providerStore.sourceSchemaLoading || state
+    }
+
+    return state
+  }
+
   scheduleReplica(replica: MainItem): Promise<void> {
     if (wizardStore.schedules.length === 0) {
       return Promise.resolve()
@@ -595,7 +609,7 @@ class WizardPage extends React.Component<Props, State> {
             hasSourceOptions={Boolean(this.pages.find(p => p.id === 'source-options'))}
             storageMap={wizardStore.storageMap}
             schedules={wizardStore.schedules}
-            nextButtonDisabled={this.state.nextButtonDisabled}
+            nextButtonDisabled={this.isNextButtonDisabled()}
             type={this.state.type}
             onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
             onBackClick={() => { this.handleBackClick() }}

+ 17 - 8
src/sources/ProviderSource.js

@@ -37,14 +37,23 @@ class ProviderSource {
   async loadOptionsSchema(providerName: string, optionsType: 'source' | 'destination', useCache?: ?boolean, quietError?: ?boolean): Promise<Field[]> {
     let schemaTypeInt = optionsType === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
 
-    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
+    try {
+      let response = await Api.send({
+        url: `${servicesUrl.coriolis}/${Api.projectId}/providers/${providerName}/schemas/${schemaTypeInt}`,
+        cache: useCache,
+        quietError,
+      })
+      let schemas = (response && response.data && response.data.schemas) || {}
+      let schema = optionsType === 'source' ? schemas.source_environment_schema : schemas.destination_environment_schema
+      let fields = []
+      if (schema) {
+        fields = SchemaParser.optionsSchemaToFields(providerName, schema)
+      }
+      return fields
+    } catch (err) {
+      console.error(err)
+      return []
+    }
   }
 
   async getOptionsValues(optionsType: 'source' | 'destination', endpointId: string, envData: ?{ [string]: mixed }, cache?: ?boolean, quietError?: boolean): Promise<OptionValues[]> {

+ 5 - 2
src/utils/ObjectUtils.js

@@ -64,8 +64,11 @@ class ObjectUtils {
     return result
   }
 
+  static async wait(ms: number) {
+    return new Promise(r => setTimeout(() => r(), ms))
+  }
+
   static async waitFor(predicate: () => boolean, timeoutMs?: number = 15000, tryEvery?: number = 1000) {
-    let wait = (ms: number) => new Promise(resolve => { setTimeout(() => { resolve() }, ms) })
     let startTime = new Date().getTime()
     let testLoop = async () => {
       if (predicate()) {
@@ -74,7 +77,7 @@ class ObjectUtils {
       if (new Date().getTime() - startTime > timeoutMs) {
         throw new Error(`Timeout: waiting for more than ${timeoutMs} ms`)
       }
-      await wait(tryEvery)
+      await this.wait(tryEvery)
       await testLoop()
     }
     await testLoop()