Browse Source

Add OCI provider minion pool support

Updated the minion pool feature to support providers which require extra
'options' API calls.

Added plugin architecture for minion pool related API request data
handling.
Sergiu Miclea 5 years ago
parent
commit
a967921df0

+ 50 - 25
src/components/organisms/MinionEndpointModal/MinionEndpointModal.tsx

@@ -15,6 +15,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import React from 'react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
+import { CSSTransitionGroup } from 'react-transition-group'
 
 import Modal from '../../molecules/Modal'
 import { Providers, ProviderTypes } from '../../../@types/Providers'
@@ -37,6 +38,15 @@ const LoadingWrapper = styled.div`
 `
 const ContentWrapper = styled.div`
   padding: 48px;
+  > span {
+    display: flex;
+    justify-content: center;
+    margin-left: -24px;
+    transition: all 250ms ease-out;
+    > div {
+      margin-left: 24px;
+    }
+  }
 `
 const NoEndpoints = styled.div`
   padding: 64px;
@@ -54,6 +64,20 @@ const ProviderWrapper = styled.div`
   align-items: center;
   justify-content: center;
   flex-direction: column;
+  &.providers-group-transition-leave {
+    opacity: 1;
+  }
+  &.providers-group-transition-leave-active {
+    opacity: 0.01;
+    transition: opacity 250ms ease-out;
+  }
+  &.providers-group-transition-enter {
+    opacity: 0.01;
+  }
+  &.providers-group-transition-enter-active {
+    opacity: 1;
+    transition: opacity 250ms ease-out;
+  }
 `
 const ButtonWrapper = styled.div`
   display: flex;
@@ -112,27 +136,6 @@ class MinionEndpointModal extends React.Component<Props, State> {
     )
   }
 
-  renderProvider(providerName: ProviderTypes, providerEndpoints: Endpoint[]) {
-    return (
-      <ProviderWrapper key={providerName}>
-        <EndpointLogos
-          height={128}
-          endpoint={providerName}
-          style={{ marginBottom: '16px' }}
-        />
-        <Dropdown
-          items={providerEndpoints}
-          valueField="id"
-          labelField="name"
-          noSelectionMessage="Choose an endpoint"
-          centered
-          selectedItem={this.state.selectedEndpoint}
-          onChange={endpoint => { this.setState({ selectedEndpoint: endpoint }) }}
-        />
-      </ProviderWrapper>
-    )
-  }
-
   renderPoolPlatform() {
     return (
       <PoolPlatformWrapper>
@@ -178,10 +181,32 @@ class MinionEndpointModal extends React.Component<Props, State> {
 
     return (
       <ContentWrapper>
-        {availableProviders.map(providerName => this.renderProvider(
-          providerName as any,
-          this.props.endpoints.filter(e => e.type === providerName),
-        ))}
+        <CSSTransitionGroup
+          transitionName="providers-group-transition"
+          transitionLeave
+          transitionEnter
+          transitionLeaveTimeout={250}
+          transitionEnterTimeout={250}
+        >
+          {availableProviders.map(providerName => (
+            <ProviderWrapper key={providerName}>
+              <EndpointLogos
+                height={128}
+                endpoint={providerName}
+                style={{ marginBottom: '16px' }}
+              />
+              <Dropdown
+                items={this.props.endpoints.filter(e => e.type === providerName)}
+                valueField="id"
+                labelField="name"
+                noSelectionMessage="Choose an endpoint"
+                centered
+                selectedItem={this.state.selectedEndpoint?.type === providerName ? this.state.selectedEndpoint : null}
+                onChange={endpoint => { this.setState({ selectedEndpoint: endpoint }) }}
+              />
+            </ProviderWrapper>
+          ))}
+        </CSSTransitionGroup>
       </ContentWrapper>
     )
   }

+ 91 - 44
src/components/organisms/MinionPoolModal/MinionPoolModal.tsx

@@ -25,13 +25,14 @@ import type { Endpoint as EndpointType } from '../../../@types/Endpoint'
 import type { Field } from '../../../@types/Field'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import KeyboardManager from '../../../utils/KeyboardManager'
-import { MinionPool } from '../../../@types/MinionPool'
 import MinionPoolModalContent from './MinionPoolModalContent'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 
 import minionPoolImage from './images/minion-pool.svg'
 import StyleProps from '../../styleUtils/StyleProps'
 import notificationStore from '../../../stores/NotificationStore'
+import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
+import { MinionPool } from '../../../@types/MinionPool'
 
 const Wrapper = styled.div<any>`
   padding: 24px 0 32px 0;
@@ -71,7 +72,8 @@ const Buttons = styled.div<any>`
 type Props = {
   cancelButtonText: string,
   endpoint: EndpointType,
-  minionPool?: MinionPool | null
+  minionPool?: MinionPool | null,
+  editableData?: any | null
   platform: 'source' | 'destination',
   onCancelClick: () => void,
   onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
@@ -80,7 +82,7 @@ type Props = {
 }
 type State = {
   invalidFields: any[],
-  minionPool: any | null
+  editableData: any | null
   saving: boolean
 }
 @observer
@@ -91,7 +93,7 @@ class MinionPoolModal extends React.Component<Props, State> {
 
   state: State = {
     invalidFields: [],
-    minionPool: null,
+    editableData: null,
     saving: false,
   }
 
@@ -112,11 +114,19 @@ class MinionPoolModal extends React.Component<Props, State> {
         return
       }
       await minionPoolStore.loadMinionPoolSchema(this.props.endpoint.type, this.props.platform)
-      await minionPoolStore.loadEnvOptions(
-        this.props.endpoint.id,
-        this.props.endpoint.type,
-        this.props.platform,
-      )
+
+      await providerStore.loadProviders()
+      const providers = providerStore.providers
+      if (!providers) {
+        return
+      }
+      await minionPoolStore.loadOptions({
+        providers,
+        optionsType: this.props.platform,
+        endpoint: this.props.endpoint,
+        envData: this.envData,
+        useCache: true,
+      })
 
       this.fillRequiredDefaults()
     }
@@ -127,19 +137,19 @@ class MinionPoolModal extends React.Component<Props, State> {
   }
 
   UNSAFE_componentWillReceiveProps(props: Props) {
-    if (props.minionPool) {
+    if (props.editableData) {
       this.setState(prevState => ({
-        minionPool: {
-          ...prevState.minionPool,
-          ...ObjectUtils.flatten(props.minionPool || {}),
+        editableData: {
+          ...prevState.editableData,
+          ...ObjectUtils.flatten(props.editableData || {}),
         },
       }))
     }
 
     if (props.platform) {
       this.setState(prevState => ({
-        minionPool: {
-          ...prevState.minionPool,
+        editableData: {
+          ...prevState.editableData,
           platform: props.platform,
         },
       }))
@@ -154,16 +164,29 @@ class MinionPoolModal extends React.Component<Props, State> {
   }
 
   get isLoading() {
-    return minionPoolStore.loadingMinionPoolSchema || minionPoolStore.loadingMinionPools
-      || minionPoolStore.loadingEnvOptions
+    return minionPoolStore.loadingMinionPoolSchema
+      || minionPoolStore.loadingMinionPools
+      || minionPoolStore.optionsPrimaryLoading
+      || providerStore.providersLoading
+  }
+
+  get envData() {
+    let envData: any = null
+    Object.keys(this.state.editableData).forEach(prop => {
+      if (!minionPoolStore.minionPoolDefaultSchema.find(f => f.name === prop)) {
+        envData = envData || {}
+        envData[prop] = this.state.editableData[prop]
+      }
+    })
+    return envData
   }
 
   getFieldValue(field?: Field | null) {
-    if (!field || !this.state.minionPool) {
+    if (!field || !this.state.editableData) {
       return ''
     }
-    if (this.state.minionPool[field.name] != null) {
-      return this.state.minionPool[field.name]
+    if (this.state.editableData[field.name] != null) {
+      return this.state.editableData[field.name]
     }
 
     if (Object.keys(field).find(k => k === 'default')) {
@@ -204,39 +227,39 @@ class MinionPoolModal extends React.Component<Props, State> {
     }
     this.setState({ saving: true })
     try {
-      if (this.state.minionPool?.id) {
+      if (this.props.minionPool?.id) {
         await this.update()
       } else {
         await this.add()
       }
     } catch (err) {
+      console.error(err)
       this.setState({ saving: false })
     }
   }
 
   async update() {
-    const stateMinionPool = { ...this.state.minionPool }
-    const minionPool = minionPoolStore.minionPools.find(e => e.id === stateMinionPool.id)
-    if (!minionPool) {
-      throw new Error('Minion pool not found!')
+    const stateMinionPool = {
+      ...this.state.editableData,
+      id: this.props.minionPool?.id,
     }
     delete stateMinionPool.platform
     delete stateMinionPool.endpoint_id
-    await minionPoolStore.update(stateMinionPool)
+    await minionPoolStore.update(this.props.endpoint.type, stateMinionPool)
     if (this.props.onUpdateComplete) {
-      this.props.onUpdateComplete(`/minion-pools/${stateMinionPool.id}`)
+      this.props.onUpdateComplete(`/minion-pools/${this.props.minionPool?.id}`)
     }
   }
 
   async add() {
-    await minionPoolStore.add(this.props.endpoint.id, this.state.minionPool)
+    await minionPoolStore.add(this.props.endpoint.type, this.props.endpoint.id, this.state.editableData)
     notificationStore.alert('Minion Pool created', 'success')
     this.props.onRequestClose()
   }
 
   fillRequiredDefaults() {
     this.setState(prevState => {
-      const minionPool: any = { ...prevState.minionPool }
+      const minionPool: any = { ...prevState.editableData }
       const requiredFieldsDefaults = minionPoolStore.minionPoolCombinedSchema
         .filter(f => f.required && f.default != null)
       requiredFieldsDefaults.forEach(f => {
@@ -244,26 +267,48 @@ class MinionPoolModal extends React.Component<Props, State> {
           minionPool[f.name] = f.default
         }
       })
-      return { minionPool }
+      return { editableData: minionPool }
     })
   }
 
-  handleFieldsChange(items: { field: Field, value: any }[]) {
+  async loadExtraOptions(field: Field | null, type: 'source' | 'destination', useCache: boolean = true) {
+    const envData = getFieldChangeOptions({
+      providerName: this.props.endpoint.type,
+      schema: minionPoolStore.minionPoolEnvSchema,
+      data: this.envData,
+      field,
+      type,
+    })
+    if (!envData) {
+      return
+    }
+    await minionPoolStore.loadOptions({
+      providers: providerStore.providers!,
+      optionsType: type,
+      endpoint: this.props.endpoint,
+      envData,
+      useCache,
+    })
+    this.fillRequiredDefaults()
+  }
+
+  handleFieldChange(field: Field, value: any) {
     this.setState(prevState => {
-      const minionPool: any = { ...prevState.minionPool }
-
-      items.forEach(item => {
-        let value = item.value
-        if (item.field.type === 'array') {
-          const arrayItems = minionPool[item.field.name] || []
-          value = arrayItems.find((v: any) => v === item.value)
-            ? arrayItems.filter((v: any) => v !== item.value) : [...arrayItems, item.value]
-        }
+      const minionPool: any = { ...prevState.editableData }
 
-        minionPool[item.field.name] = value
-      })
+      if (field.type === 'array') {
+        const arrayItems = minionPool[field.name] || []
+        value = arrayItems.find((v: any) => v === value)
+          ? arrayItems.filter((v: any) => v !== value) : [...arrayItems, value]
+      }
 
-      return { minionPool }
+      minionPool[field.name] = value
+
+      return { editableData: minionPool }
+    }, () => {
+      if (field.type !== 'string' || field.enum) {
+        this.loadExtraOptions(field, this.props.platform, true)
+      }
     })
   }
 
@@ -305,6 +350,8 @@ class MinionPoolModal extends React.Component<Props, State> {
         <MinionPoolModalContent
           endpoint={this.props.endpoint}
           platform={this.props.platform}
+          optionsLoading={minionPoolStore.optionsSecondaryLoading}
+          optionsLoadingSkipFields={minionPoolStore.minionPoolDefaultSchema.map(f => f.name)}
           envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
           defaultSchema={minionPoolStore.minionPoolDefaultSchema}
           envSchema={minionPoolStore.minionPoolEnvSchema}
@@ -314,7 +361,7 @@ class MinionPoolModal extends React.Component<Props, State> {
           getFieldValue={field => this.getFieldValue(field)}
           onFieldChange={(field, value) => {
             if (field) {
-              this.handleFieldsChange([{ field, value }])
+              this.handleFieldChange(field, value)
             }
           }}
           onCreateClick={() => { this.create() }}

+ 3 - 0
src/components/organisms/MinionPoolModal/MinionPoolModalContent.tsx

@@ -124,6 +124,8 @@ type Props = {
   invalidFields: string[],
   endpoint: Endpoint
   platform: 'source' | 'destination'
+  optionsLoading: boolean
+  optionsLoadingSkipFields: string[],
   getFieldValue: (field: Field | null | undefined) => any,
   onFieldChange: (field: Field | null, value: any) => void,
   disabled: boolean,
@@ -216,6 +218,7 @@ class MinionPoolModalContent extends React.Component<Props, State> {
             value={this.props.getFieldValue(field)}
             onChange={value => { this.props.onFieldChange(field, value) }}
             nullableBoolean={field.nullableBoolean != null ? field.nullableBoolean : true}
+            disabledLoading={this.props.optionsLoading && !this.props.optionsLoadingSkipFields.find(fn => fn === field.name)}
           />
         )
       }

+ 36 - 7
src/components/pages/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx

@@ -38,6 +38,8 @@ import MinionPoolDetailsContent from '../../organisms/MinionPoolDetailsContent/M
 import replicaStore from '../../../stores/ReplicaStore'
 import migrationStore from '../../../stores/MigrationStore'
 import MinionPoolConfirmationModal from '../../molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal'
+import providerStore from '../../../stores/ProviderStore'
+import { Field } from '../../../@types/Field'
 
 const Wrapper = styled.div<any>``
 
@@ -93,6 +95,28 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     return minionPoolStore.minionPoolDetails
   }
 
+  get envData() {
+    return this.getSchemaData(minionPoolStore.minionPoolEnvSchema, minionPoolStore.minionPoolDetails?.environment_options)
+  }
+
+  get editableData() {
+    const envData = this.envData
+    const defaultData = this.getSchemaData(minionPoolStore.minionPoolDefaultSchema, minionPoolStore.minionPoolDetails)
+    return defaultData || envData ? { ...defaultData, ...envData } : null
+  }
+
+  getSchemaData(schema: Field[], data: any | null) {
+    let schemaData: any = null
+    const details: any = data || {}
+    Object.keys(details).forEach(prop => {
+      if (schema.find(f => f.name === prop)) {
+        schemaData = schemaData || {}
+        schemaData[prop] = details[prop]
+      }
+    })
+    return schemaData
+  }
+
   getStatus() {
     return this.minionPool?.status
   }
@@ -121,12 +145,14 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
       endpoint.type,
       minionPool.platform,
     )
-    await minionPoolStore.loadEnvOptions(
-      endpoint.id,
-      endpoint.type,
-      minionPool.platform,
-      { useCache: true },
-    )
+    await providerStore.loadProviders()
+    await minionPoolStore.loadOptions({
+      providers: providerStore.providers!,
+      endpoint,
+      optionsType: minionPool.platform,
+      useCache: true,
+      envData: this.envData,
+    })
   }
 
   handleUserItemClick(item: { value: string }) {
@@ -241,6 +267,7 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
           endpoint={endpoint}
           onCancelClick={() => { this.closeEditModal() }}
           onRequestClose={() => { this.closeEditModal() }}
+          editableData={this.editableData}
           minionPool={this.minionPool}
           platform={this.minionPool?.platform || 'source'}
           onUpdateComplete={r => { this.handleUpdateComplete(r) }}
@@ -329,7 +356,9 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
               endpoints={endpointStore.endpoints}
               schema={minionPoolStore.minionPoolCombinedSchema}
               schemaLoading={minionPoolStore.loadingMinionPoolSchema
-                || minionPoolStore.loadingEnvOptions}
+                || minionPoolStore.optionsPrimaryLoading
+                || providerStore.providersLoading
+                || minionPoolStore.optionsSecondaryLoading}
               page={this.props.match.params.page || ''}
               loading={minionPoolStore.loadingMinionPoolDetails}
               onDeleteMinionPoolClick={() => { this.handleDeleteMinionPoolClick() }}

+ 30 - 0
src/plugins/endpoint/default/MinionPoolSchemaPlugin.ts

@@ -0,0 +1,30 @@
+import { Field } from '../../../@types/Field'
+import DomUtils from '../../../utils/DomUtils'
+
+export default class MinionPoolSchemaPlugin {
+  static getMinionPoolToOptionsQuery(envData?: any) {
+    let envString = ''
+    if (envData) {
+      envString = `?env=${DomUtils.encodeToBase64Url(envData)}`
+    }
+    return envString
+  }
+
+  static minionPoolTransformOptionsFields(fields: Field[]) {
+    return fields
+  }
+
+  static getMinionPoolEnv(schema: Field[], data: any) {
+    const payload: any = {}
+    schema.forEach(field => {
+      if (data[field.name] === null || data[field.name] === undefined || data[field.name] === '') {
+        if (field.default !== null) {
+          payload[field.name] = field.default
+        }
+      } else {
+        payload[field.name] = data[field.name]
+      }
+    })
+    return payload
+  }
+}

+ 16 - 0
src/plugins/endpoint/index.ts

@@ -30,6 +30,9 @@ import DefaultInstanceInfoPlugin from './default/InstanceInfoPlugin'
 import OciInstanceInfoPlugin from './oci/InstanceInfoPlugin'
 import { ProviderTypes } from '../../@types/Providers'
 
+import DefaultMinionPoolSchemaPlugin from './default/MinionPoolSchemaPlugin'
+import OpenstackMinionPoolSchemaPlugin from './openstack/MinionPoolSchemaPlugin'
+
 const hasKey = <O>(obj: O, key: keyof any): key is keyof O => key in obj
 
 export const ConnectionSchemaPlugin = {
@@ -88,3 +91,16 @@ export const InstanceInfoPlugin = {
     return map.default
   },
 }
+
+export const MinionPoolSchemaPlugin = {
+  for: (provider: ProviderTypes) => {
+    const map = {
+      default: DefaultMinionPoolSchemaPlugin,
+      openstack: OpenstackMinionPoolSchemaPlugin,
+    }
+    if (hasKey(map, provider)) {
+      return map[provider]
+    }
+    return map.default
+  },
+}

+ 20 - 0
src/plugins/endpoint/openstack/MinionPoolSchemaPlugin.ts

@@ -0,0 +1,20 @@
+import DefaultMinionPoolSchemaPlugin from '../default/MinionPoolSchemaPlugin'
+import { Field } from '../../../@types/Field'
+import DomUtils from '../../../utils/DomUtils'
+
+export default class MinionPoolSchemaPlugin {
+  static getMinionPoolToOptionsQuery(envData: any) {
+    return `?env=${DomUtils.encodeToBase64Url({ ...envData, list_all_destination_networks: true })}`
+  }
+
+  static minionPoolTransformOptionsFields(fields: Field[]) {
+    // Remove this field, as all networks are always listed
+    fields = fields.filter(f => f.name !== 'list_all_destination_networks')
+    return fields
+  }
+
+  static getMinionPoolEnv(schema: Field[], data: any) {
+    const payload: any = DefaultMinionPoolSchemaPlugin.getMinionPoolEnv(schema, data)
+    return { ...payload, list_all_destination_networks: true }
+  }
+}

+ 42 - 35
src/sources/MinionPoolSource.ts

@@ -13,6 +13,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 import Api from '../utils/ApiCaller'
+import DefaultMinionPoolSchemaPlugin from '../plugins/endpoint/default/MinionPoolSchemaPlugin'
 
 import configLoader from '../utils/Config'
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
@@ -20,24 +21,9 @@ import { ProviderTypes } from '../@types/Providers'
 import { Field } from '../@types/Field'
 import { providerTypes } from '../constants'
 import { SchemaParser } from './Schemas'
-import { OptionValues } from '../@types/Endpoint'
+import { Endpoint, OptionValues } from '../@types/Endpoint'
 import { MinionPoolAction } from '../stores/MinionPoolStore'
 import { Execution } from '../@types/Execution'
-import DomUtils from '../utils/DomUtils'
-
-const transformFieldsToPayload = (schema: Field[], data: any) => {
-  const payload: any = {}
-  schema.forEach(field => {
-    if (data[field.name] === null || data[field.name] === undefined || data[field.name] === '') {
-      if (field.default !== null) {
-        payload[field.name] = field.default
-      }
-    } else {
-      payload[field.name] = data[field.name]
-    }
-  })
-  return payload
-}
 
 class MinionPoolSource {
   getMinionPoolDefaultSchema(): Field[] {
@@ -141,13 +127,25 @@ class MinionPoolSource {
     return response.data.minion_pool
   }
 
-  async loadEnvOptions(endpointId: string, platform: 'source' | 'destination', useCache?: boolean): Promise<OptionValues[]> {
-    const env = DomUtils.encodeToBase64Url({ list_all_destination_networks: true })
+  async loadOptions(config: {
+    optionsType: 'source' | 'destination',
+    endpoint: Endpoint,
+    envData: { [prop: string]: any } | null | undefined,
+    useCache?: boolean | null,
+  }): Promise<OptionValues[]> {
+    const {
+      optionsType, endpoint, envData, useCache,
+    } = config
+    const envString = SchemaParser.getMinionPoolToOptionsQuery(envData, endpoint.type)
+    const callName = optionsType === 'source' ? 'source-minion-pool-options' : 'destination-minion-pool-options'
+    const fieldName = optionsType === 'source' ? 'source_minion_pool_options' : 'destination_minion_pool_options'
+
     const response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/endpoints/${endpointId}/${platform}-minion-pool-options?env=${env}`,
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/endpoints/${endpoint.id}/${callName}${envString}`,
       cache: useCache,
+      cancelId: endpoint.id,
     })
-    return response.data[`${platform}_minion_pool_options`]
+    return response.data[fieldName]
   }
 
   async loadMinionPoolSchema(providerName: ProviderTypes, platform: 'source' | 'destination'): Promise<Field[]> {
@@ -160,10 +158,8 @@ class MinionPoolSource {
       const schema = response.data?.schemas?.[`${platform}_minion_pool_environment_schema`]
       let fields = []
       if (schema) {
-        fields = SchemaParser.optionsSchemaToFields(providerName, schema, `${providerName}-minion-pool`)
+        fields = SchemaParser.minionPoolOptionsSchemaToFields(providerName, schema, `${providerName}-minion-pool`)
       }
-      // Remove this field, as all networks are always listed
-      fields = fields.filter(f => f.name !== 'list_all_destination_networks')
       return fields
     } catch (err) {
       console.error(err)
@@ -171,15 +167,21 @@ class MinionPoolSource {
     }
   }
 
-  async add(endpointId: string, data: any, defaultSchema: Field[], envSchema: Field[]) {
+  async add(config: {
+    endpointId: string,
+    data: any,
+    defaultSchema: Field[],
+    envSchema: Field[],
+    provider: ProviderTypes
+  }) {
+    const {
+      endpointId, data, defaultSchema, envSchema, provider,
+    } = config
     const payload = {
       minion_pool: {
-        ...transformFieldsToPayload(defaultSchema, data),
+        ...DefaultMinionPoolSchemaPlugin.getMinionPoolEnv(defaultSchema, data),
         endpoint_id: endpointId,
-        environment_options: {
-          ...transformFieldsToPayload(envSchema, data),
-          list_all_destination_networks: true,
-        },
+        environment_options: SchemaParser.getMinionPoolEnv(provider, envSchema, data),
       },
     }
     const response = await Api.send({
@@ -190,14 +192,19 @@ class MinionPoolSource {
     return response.data.minion_pool
   }
 
-  async update(data: any, defaultSchema: Field[], envSchema: Field[]) {
+  async update(config: {
+    data: any,
+    defaultSchema: Field[],
+    envSchema: Field[],
+    provider: ProviderTypes
+  }) {
+    const {
+      data, defaultSchema, envSchema, provider,
+    } = config
     const payload = {
       minion_pool: {
-        ...transformFieldsToPayload(defaultSchema, data),
-        environment_options: {
-          ...transformFieldsToPayload(envSchema, data),
-          list_all_destination_networks: true,
-        },
+        ...DefaultMinionPoolSchemaPlugin.getMinionPoolEnv(defaultSchema, data),
+        environment_options: SchemaParser.getMinionPoolEnv(provider, envSchema, data),
       },
     }
     const response = await Api.send({

+ 19 - 1
src/sources/Schemas.ts

@@ -12,10 +12,11 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-import { ConnectionSchemaPlugin, OptionsSchemaPlugin } from '../plugins/endpoint'
+import { ConnectionSchemaPlugin, MinionPoolSchemaPlugin, OptionsSchemaPlugin } from '../plugins/endpoint'
 import type { Schema } from '../@types/Schema'
 import type { Endpoint } from '../@types/Endpoint'
 import { ProviderTypes } from '../@types/Providers'
+import { Field } from '../@types/Field'
 
 class SchemaParser {
   static storedConnectionsSchemas: any = {}
@@ -61,6 +62,23 @@ class SchemaParser {
   static parseConnectionResponse(endpoint: Endpoint) {
     return ConnectionSchemaPlugin.for(endpoint.type).parseConnectionResponse(endpoint)
   }
+
+  static getMinionPoolToOptionsQuery(env: any, provider: ProviderTypes) {
+    const parsers = MinionPoolSchemaPlugin.for(provider)
+    return parsers.getMinionPoolToOptionsQuery(env)
+  }
+
+  static minionPoolOptionsSchemaToFields(provider: ProviderTypes, schema: any, dictionaryKey: string) {
+    let fields = this.optionsSchemaToFields(provider, schema, dictionaryKey)
+    const parsers = MinionPoolSchemaPlugin.for(provider)
+    fields = parsers.minionPoolTransformOptionsFields(fields)
+    return fields
+  }
+
+  static getMinionPoolEnv(provider: ProviderTypes, schema: Field[], data: any) {
+    const parsers = MinionPoolSchemaPlugin.for(provider)
+    return parsers.getMinionPoolEnv(schema, data)
+  }
 }
 
 export { SchemaParser }

+ 78 - 32
src/stores/MinionPoolStore.ts

@@ -18,8 +18,11 @@ import {
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
 import MinionPoolSource from '../sources/MinionPoolSource'
 import { Field } from '../@types/Field'
-import { ProviderTypes } from '../@types/Providers'
+import { Providers, ProviderTypes } from '../@types/Providers'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
+import { providerTypes } from '../constants'
+import apiCaller from '../utils/ApiCaller'
+import { Endpoint, OptionValues } from '../@types/Endpoint'
 
 export type MinionPoolAction = 'allocate' | 'deallocate' | 'refresh'
 
@@ -50,7 +53,10 @@ class MinionPoolStore {
   minionPoolEnvSchema: Field[] = []
 
   @observable
-  loadingEnvOptions: boolean = false
+  optionsPrimaryLoading: boolean = false
+
+  @observable
+  optionsSecondaryLoading: boolean = false
 
   @computed
   get minionPoolCombinedSchema() {
@@ -121,48 +127,88 @@ class MinionPoolStore {
     }
   }
 
-  @action
-  async loadEnvOptions(
-    endpointId: string,
-    providerName: ProviderTypes,
-    platform: 'source' | 'destination',
-    opts?: { useCache?: boolean },
-  ) {
-    this.loadingEnvOptions = true
+  private getOptionsValuesLastReqId: string = ''
+
+  async loadOptions(config: {
+    endpoint: Endpoint,
+    providers: Providers,
+    optionsType: 'source' | 'destination',
+    envData?: { [prop: string]: any } | null,
+    useCache?: boolean,
+  }) {
+    const {
+      optionsType, endpoint, envData, useCache, providers,
+    } = config
+    const providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS
+
+    const providerWithExtraOptions = providers[endpoint.type].types.find(t => t === providerType)
+    if (!providerWithExtraOptions) {
+      return
+    }
 
-    try {
-      const options = await MinionPoolSource.loadEnvOptions(endpointId, platform, opts?.useCache)
+    let canceled = false
+    apiCaller.cancelRequests(endpoint.id)
 
-      runInAction(() => {
-        this.minionPoolEnvSchema.forEach(field => {
-          const parser = OptionsSchemaPlugin.for(providerName)
-          parser.fillFieldValues(field, options)
-        })
+    this.optionsPrimaryLoading = !envData
+    this.optionsSecondaryLoading = !!envData
+
+    const reqId = `${(endpoint.id)}-${providerType}`
+    this.getOptionsValuesLastReqId = reqId
+
+    try {
+      const options = await MinionPoolSource.loadOptions({
+        optionsType, endpoint, envData, useCache,
       })
+      this.getOptionsValuesSuccess(
+        endpoint.type,
+        options,
+        this.getOptionsValuesLastReqId === reqId,
+      )
+    } catch (err) {
+      canceled = err ? err.canceled : false
+      throw err
     } finally {
-      runInAction(() => {
-        this.loadingEnvOptions = false
-      })
+      if (!canceled && this.getOptionsValuesLastReqId === reqId) {
+        this.optionsPrimaryLoading = false
+        this.optionsSecondaryLoading = false
+      }
+    }
+  }
+
+  @action getOptionsValuesSuccess(
+    provider: ProviderTypes,
+    options: OptionValues[],
+    isValid: boolean,
+  ) {
+    if (!isValid) {
+      return
     }
+    this.minionPoolEnvSchema.forEach(field => {
+      const parser = OptionsSchemaPlugin.for(provider)
+      parser.fillFieldValues(field, options)
+    })
+    this.minionPoolEnvSchema = [...this.minionPoolEnvSchema]
   }
 
   @action
-  async update(minionPoolData: any) {
-    return MinionPoolSource.update(
-      minionPoolData,
-      this.minionPoolDefaultSchema,
-      this.minionPoolEnvSchema,
-    )
+  async update(provider: ProviderTypes, minionPoolData: any) {
+    return MinionPoolSource.update({
+      data: minionPoolData,
+      defaultSchema: this.minionPoolDefaultSchema,
+      envSchema: this.minionPoolEnvSchema,
+      provider,
+    })
   }
 
   @action
-  async add(endpointId: string, minionPoolData: any) {
-    return MinionPoolSource.add(
+  async add(provider: ProviderTypes, endpointId: string, minionPoolData: any) {
+    return MinionPoolSource.add({
       endpointId,
-      minionPoolData,
-      this.minionPoolDefaultSchema,
-      this.minionPoolEnvSchema,
-    )
+      data: minionPoolData,
+      defaultSchema: this.minionPoolDefaultSchema,
+      envSchema: this.minionPoolEnvSchema,
+      provider,
+    })
   }
 
   @action