Explorar o código

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 %!s(int64=5) %!d(string=hai) anos
pai
achega
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 React from 'react'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import { CSSTransitionGroup } from 'react-transition-group'
 
 
 import Modal from '../../molecules/Modal'
 import Modal from '../../molecules/Modal'
 import { Providers, ProviderTypes } from '../../../@types/Providers'
 import { Providers, ProviderTypes } from '../../../@types/Providers'
@@ -37,6 +38,15 @@ const LoadingWrapper = styled.div`
 `
 `
 const ContentWrapper = styled.div`
 const ContentWrapper = styled.div`
   padding: 48px;
   padding: 48px;
+  > span {
+    display: flex;
+    justify-content: center;
+    margin-left: -24px;
+    transition: all 250ms ease-out;
+    > div {
+      margin-left: 24px;
+    }
+  }
 `
 `
 const NoEndpoints = styled.div`
 const NoEndpoints = styled.div`
   padding: 64px;
   padding: 64px;
@@ -54,6 +64,20 @@ const ProviderWrapper = styled.div`
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   flex-direction: column;
   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`
 const ButtonWrapper = styled.div`
   display: flex;
   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() {
   renderPoolPlatform() {
     return (
     return (
       <PoolPlatformWrapper>
       <PoolPlatformWrapper>
@@ -178,10 +181,32 @@ class MinionEndpointModal extends React.Component<Props, State> {
 
 
     return (
     return (
       <ContentWrapper>
       <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>
       </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 type { Field } from '../../../@types/Field'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import ObjectUtils from '../../../utils/ObjectUtils'
 import KeyboardManager from '../../../utils/KeyboardManager'
 import KeyboardManager from '../../../utils/KeyboardManager'
-import { MinionPool } from '../../../@types/MinionPool'
 import MinionPoolModalContent from './MinionPoolModalContent'
 import MinionPoolModalContent from './MinionPoolModalContent'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 import minionPoolStore from '../../../stores/MinionPoolStore'
 
 
 import minionPoolImage from './images/minion-pool.svg'
 import minionPoolImage from './images/minion-pool.svg'
 import StyleProps from '../../styleUtils/StyleProps'
 import StyleProps from '../../styleUtils/StyleProps'
 import notificationStore from '../../../stores/NotificationStore'
 import notificationStore from '../../../stores/NotificationStore'
+import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
+import { MinionPool } from '../../../@types/MinionPool'
 
 
 const Wrapper = styled.div<any>`
 const Wrapper = styled.div<any>`
   padding: 24px 0 32px 0;
   padding: 24px 0 32px 0;
@@ -71,7 +72,8 @@ const Buttons = styled.div<any>`
 type Props = {
 type Props = {
   cancelButtonText: string,
   cancelButtonText: string,
   endpoint: EndpointType,
   endpoint: EndpointType,
-  minionPool?: MinionPool | null
+  minionPool?: MinionPool | null,
+  editableData?: any | null
   platform: 'source' | 'destination',
   platform: 'source' | 'destination',
   onCancelClick: () => void,
   onCancelClick: () => void,
   onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
   onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
@@ -80,7 +82,7 @@ type Props = {
 }
 }
 type State = {
 type State = {
   invalidFields: any[],
   invalidFields: any[],
-  minionPool: any | null
+  editableData: any | null
   saving: boolean
   saving: boolean
 }
 }
 @observer
 @observer
@@ -91,7 +93,7 @@ class MinionPoolModal extends React.Component<Props, State> {
 
 
   state: State = {
   state: State = {
     invalidFields: [],
     invalidFields: [],
-    minionPool: null,
+    editableData: null,
     saving: false,
     saving: false,
   }
   }
 
 
@@ -112,11 +114,19 @@ class MinionPoolModal extends React.Component<Props, State> {
         return
         return
       }
       }
       await minionPoolStore.loadMinionPoolSchema(this.props.endpoint.type, this.props.platform)
       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()
       this.fillRequiredDefaults()
     }
     }
@@ -127,19 +137,19 @@ class MinionPoolModal extends React.Component<Props, State> {
   }
   }
 
 
   UNSAFE_componentWillReceiveProps(props: Props) {
   UNSAFE_componentWillReceiveProps(props: Props) {
-    if (props.minionPool) {
+    if (props.editableData) {
       this.setState(prevState => ({
       this.setState(prevState => ({
-        minionPool: {
-          ...prevState.minionPool,
-          ...ObjectUtils.flatten(props.minionPool || {}),
+        editableData: {
+          ...prevState.editableData,
+          ...ObjectUtils.flatten(props.editableData || {}),
         },
         },
       }))
       }))
     }
     }
 
 
     if (props.platform) {
     if (props.platform) {
       this.setState(prevState => ({
       this.setState(prevState => ({
-        minionPool: {
-          ...prevState.minionPool,
+        editableData: {
+          ...prevState.editableData,
           platform: props.platform,
           platform: props.platform,
         },
         },
       }))
       }))
@@ -154,16 +164,29 @@ class MinionPoolModal extends React.Component<Props, State> {
   }
   }
 
 
   get isLoading() {
   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) {
   getFieldValue(field?: Field | null) {
-    if (!field || !this.state.minionPool) {
+    if (!field || !this.state.editableData) {
       return ''
       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')) {
     if (Object.keys(field).find(k => k === 'default')) {
@@ -204,39 +227,39 @@ class MinionPoolModal extends React.Component<Props, State> {
     }
     }
     this.setState({ saving: true })
     this.setState({ saving: true })
     try {
     try {
-      if (this.state.minionPool?.id) {
+      if (this.props.minionPool?.id) {
         await this.update()
         await this.update()
       } else {
       } else {
         await this.add()
         await this.add()
       }
       }
     } catch (err) {
     } catch (err) {
+      console.error(err)
       this.setState({ saving: false })
       this.setState({ saving: false })
     }
     }
   }
   }
 
 
   async update() {
   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.platform
     delete stateMinionPool.endpoint_id
     delete stateMinionPool.endpoint_id
-    await minionPoolStore.update(stateMinionPool)
+    await minionPoolStore.update(this.props.endpoint.type, stateMinionPool)
     if (this.props.onUpdateComplete) {
     if (this.props.onUpdateComplete) {
-      this.props.onUpdateComplete(`/minion-pools/${stateMinionPool.id}`)
+      this.props.onUpdateComplete(`/minion-pools/${this.props.minionPool?.id}`)
     }
     }
   }
   }
 
 
   async add() {
   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')
     notificationStore.alert('Minion Pool created', 'success')
     this.props.onRequestClose()
     this.props.onRequestClose()
   }
   }
 
 
   fillRequiredDefaults() {
   fillRequiredDefaults() {
     this.setState(prevState => {
     this.setState(prevState => {
-      const minionPool: any = { ...prevState.minionPool }
+      const minionPool: any = { ...prevState.editableData }
       const requiredFieldsDefaults = minionPoolStore.minionPoolCombinedSchema
       const requiredFieldsDefaults = minionPoolStore.minionPoolCombinedSchema
         .filter(f => f.required && f.default != null)
         .filter(f => f.required && f.default != null)
       requiredFieldsDefaults.forEach(f => {
       requiredFieldsDefaults.forEach(f => {
@@ -244,26 +267,48 @@ class MinionPoolModal extends React.Component<Props, State> {
           minionPool[f.name] = f.default
           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 => {
     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
         <MinionPoolModalContent
           endpoint={this.props.endpoint}
           endpoint={this.props.endpoint}
           platform={this.props.platform}
           platform={this.props.platform}
+          optionsLoading={minionPoolStore.optionsSecondaryLoading}
+          optionsLoadingSkipFields={minionPoolStore.minionPoolDefaultSchema.map(f => f.name)}
           envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
           envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
           defaultSchema={minionPoolStore.minionPoolDefaultSchema}
           defaultSchema={minionPoolStore.minionPoolDefaultSchema}
           envSchema={minionPoolStore.minionPoolEnvSchema}
           envSchema={minionPoolStore.minionPoolEnvSchema}
@@ -314,7 +361,7 @@ class MinionPoolModal extends React.Component<Props, State> {
           getFieldValue={field => this.getFieldValue(field)}
           getFieldValue={field => this.getFieldValue(field)}
           onFieldChange={(field, value) => {
           onFieldChange={(field, value) => {
             if (field) {
             if (field) {
-              this.handleFieldsChange([{ field, value }])
+              this.handleFieldChange(field, value)
             }
             }
           }}
           }}
           onCreateClick={() => { this.create() }}
           onCreateClick={() => { this.create() }}

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

@@ -124,6 +124,8 @@ type Props = {
   invalidFields: string[],
   invalidFields: string[],
   endpoint: Endpoint
   endpoint: Endpoint
   platform: 'source' | 'destination'
   platform: 'source' | 'destination'
+  optionsLoading: boolean
+  optionsLoadingSkipFields: string[],
   getFieldValue: (field: Field | null | undefined) => any,
   getFieldValue: (field: Field | null | undefined) => any,
   onFieldChange: (field: Field | null, value: any) => void,
   onFieldChange: (field: Field | null, value: any) => void,
   disabled: boolean,
   disabled: boolean,
@@ -216,6 +218,7 @@ class MinionPoolModalContent extends React.Component<Props, State> {
             value={this.props.getFieldValue(field)}
             value={this.props.getFieldValue(field)}
             onChange={value => { this.props.onFieldChange(field, value) }}
             onChange={value => { this.props.onFieldChange(field, value) }}
             nullableBoolean={field.nullableBoolean != null ? field.nullableBoolean : true}
             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 replicaStore from '../../../stores/ReplicaStore'
 import migrationStore from '../../../stores/MigrationStore'
 import migrationStore from '../../../stores/MigrationStore'
 import MinionPoolConfirmationModal from '../../molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal'
 import MinionPoolConfirmationModal from '../../molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal'
+import providerStore from '../../../stores/ProviderStore'
+import { Field } from '../../../@types/Field'
 
 
 const Wrapper = styled.div<any>``
 const Wrapper = styled.div<any>``
 
 
@@ -93,6 +95,28 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     return minionPoolStore.minionPoolDetails
     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() {
   getStatus() {
     return this.minionPool?.status
     return this.minionPool?.status
   }
   }
@@ -121,12 +145,14 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
       endpoint.type,
       endpoint.type,
       minionPool.platform,
       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 }) {
   handleUserItemClick(item: { value: string }) {
@@ -241,6 +267,7 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
           endpoint={endpoint}
           endpoint={endpoint}
           onCancelClick={() => { this.closeEditModal() }}
           onCancelClick={() => { this.closeEditModal() }}
           onRequestClose={() => { this.closeEditModal() }}
           onRequestClose={() => { this.closeEditModal() }}
+          editableData={this.editableData}
           minionPool={this.minionPool}
           minionPool={this.minionPool}
           platform={this.minionPool?.platform || 'source'}
           platform={this.minionPool?.platform || 'source'}
           onUpdateComplete={r => { this.handleUpdateComplete(r) }}
           onUpdateComplete={r => { this.handleUpdateComplete(r) }}
@@ -329,7 +356,9 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
               endpoints={endpointStore.endpoints}
               endpoints={endpointStore.endpoints}
               schema={minionPoolStore.minionPoolCombinedSchema}
               schema={minionPoolStore.minionPoolCombinedSchema}
               schemaLoading={minionPoolStore.loadingMinionPoolSchema
               schemaLoading={minionPoolStore.loadingMinionPoolSchema
-                || minionPoolStore.loadingEnvOptions}
+                || minionPoolStore.optionsPrimaryLoading
+                || providerStore.providersLoading
+                || minionPoolStore.optionsSecondaryLoading}
               page={this.props.match.params.page || ''}
               page={this.props.match.params.page || ''}
               loading={minionPoolStore.loadingMinionPoolDetails}
               loading={minionPoolStore.loadingMinionPoolDetails}
               onDeleteMinionPoolClick={() => { this.handleDeleteMinionPoolClick() }}
               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 OciInstanceInfoPlugin from './oci/InstanceInfoPlugin'
 import { ProviderTypes } from '../../@types/Providers'
 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
 const hasKey = <O>(obj: O, key: keyof any): key is keyof O => key in obj
 
 
 export const ConnectionSchemaPlugin = {
 export const ConnectionSchemaPlugin = {
@@ -88,3 +91,16 @@ export const InstanceInfoPlugin = {
     return map.default
     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 Api from '../utils/ApiCaller'
+import DefaultMinionPoolSchemaPlugin from '../plugins/endpoint/default/MinionPoolSchemaPlugin'
 
 
 import configLoader from '../utils/Config'
 import configLoader from '../utils/Config'
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
@@ -20,24 +21,9 @@ import { ProviderTypes } from '../@types/Providers'
 import { Field } from '../@types/Field'
 import { Field } from '../@types/Field'
 import { providerTypes } from '../constants'
 import { providerTypes } from '../constants'
 import { SchemaParser } from './Schemas'
 import { SchemaParser } from './Schemas'
-import { OptionValues } from '../@types/Endpoint'
+import { Endpoint, OptionValues } from '../@types/Endpoint'
 import { MinionPoolAction } from '../stores/MinionPoolStore'
 import { MinionPoolAction } from '../stores/MinionPoolStore'
 import { Execution } from '../@types/Execution'
 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 {
 class MinionPoolSource {
   getMinionPoolDefaultSchema(): Field[] {
   getMinionPoolDefaultSchema(): Field[] {
@@ -141,13 +127,25 @@ class MinionPoolSource {
     return response.data.minion_pool
     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({
     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,
       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[]> {
   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`]
       const schema = response.data?.schemas?.[`${platform}_minion_pool_environment_schema`]
       let fields = []
       let fields = []
       if (schema) {
       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
       return fields
     } catch (err) {
     } catch (err) {
       console.error(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 = {
     const payload = {
       minion_pool: {
       minion_pool: {
-        ...transformFieldsToPayload(defaultSchema, data),
+        ...DefaultMinionPoolSchemaPlugin.getMinionPoolEnv(defaultSchema, data),
         endpoint_id: endpointId,
         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({
     const response = await Api.send({
@@ -190,14 +192,19 @@ class MinionPoolSource {
     return response.data.minion_pool
     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 = {
     const payload = {
       minion_pool: {
       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({
     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/>.
 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 { Schema } from '../@types/Schema'
 import type { Endpoint } from '../@types/Endpoint'
 import type { Endpoint } from '../@types/Endpoint'
 import { ProviderTypes } from '../@types/Providers'
 import { ProviderTypes } from '../@types/Providers'
+import { Field } from '../@types/Field'
 
 
 class SchemaParser {
 class SchemaParser {
   static storedConnectionsSchemas: any = {}
   static storedConnectionsSchemas: any = {}
@@ -61,6 +62,23 @@ class SchemaParser {
   static parseConnectionResponse(endpoint: Endpoint) {
   static parseConnectionResponse(endpoint: Endpoint) {
     return ConnectionSchemaPlugin.for(endpoint.type).parseConnectionResponse(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 }
 export { SchemaParser }

+ 78 - 32
src/stores/MinionPoolStore.ts

@@ -18,8 +18,11 @@ import {
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
 import { MinionPool, MinionPoolDetails } from '../@types/MinionPool'
 import MinionPoolSource from '../sources/MinionPoolSource'
 import MinionPoolSource from '../sources/MinionPoolSource'
 import { Field } from '../@types/Field'
 import { Field } from '../@types/Field'
-import { ProviderTypes } from '../@types/Providers'
+import { Providers, ProviderTypes } from '../@types/Providers'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
 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'
 export type MinionPoolAction = 'allocate' | 'deallocate' | 'refresh'
 
 
@@ -50,7 +53,10 @@ class MinionPoolStore {
   minionPoolEnvSchema: Field[] = []
   minionPoolEnvSchema: Field[] = []
 
 
   @observable
   @observable
-  loadingEnvOptions: boolean = false
+  optionsPrimaryLoading: boolean = false
+
+  @observable
+  optionsSecondaryLoading: boolean = false
 
 
   @computed
   @computed
   get minionPoolCombinedSchema() {
   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 {
     } 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
   @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
   @action
-  async add(endpointId: string, minionPoolData: any) {
-    return MinionPoolSource.add(
+  async add(provider: ProviderTypes, endpointId: string, minionPoolData: any) {
+    return MinionPoolSource.add({
       endpointId,
       endpointId,
-      minionPoolData,
-      this.minionPoolDefaultSchema,
-      this.minionPoolEnvSchema,
-    )
+      data: minionPoolData,
+      defaultSchema: this.minionPoolDefaultSchema,
+      envSchema: this.minionPoolEnvSchema,
+      provider,
+    })
   }
   }
 
 
   @action
   @action