Ver Fonte

Implement Minion Pool feature redesign

Minion Pool List
- status pill updates
- number of machines created

Minion Pool Details
- details and status for each machine
- list of events and product updates
- new minion pool actions

Minion Pool Popup
- new fields
- simple and advanced tabs
Sergiu Miclea há 5 anos atrás
pai
commit
35961d5b26
45 ficheiros alterados com 1495 adições e 619 exclusões
  1. 2 0
      config.ts
  2. 1 0
      src/@types/Config.ts
  3. 8 1
      src/@types/Field.ts
  4. 32 8
      src/@types/MinionPool.ts
  5. 9 0
      src/components/atoms/StatusIcon/StatusIcon.tsx
  6. 17 0
      src/components/atoms/StatusIcon/images/triangle.svg
  7. 5 0
      src/components/atoms/StatusIcon/story.tsx
  8. 33 7
      src/components/atoms/StatusPill/StatusPill.tsx
  9. 4 0
      src/components/atoms/StatusPill/story.tsx
  10. 20 0
      src/components/atoms/Switch/Switch.tsx
  11. 44 5
      src/components/molecules/Dropdown/Dropdown.tsx
  12. 16 0
      src/components/molecules/Dropdown/story.tsx
  13. 5 1
      src/components/molecules/FieldInput/FieldInput.tsx
  14. 13 4
      src/components/molecules/MainDetailsTable/MainDetailsTable.tsx
  15. 110 0
      src/components/molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal.tsx
  16. 6 0
      src/components/molecules/MinionPoolConfirmationModal/package.json
  17. 10 7
      src/components/molecules/MinionPoolListItem/MinionPoolListItem.tsx
  18. 1 1
      src/components/organisms/EditReplica/EditReplica.tsx
  19. 1 1
      src/components/organisms/Executions/Executions.tsx
  20. 31 2
      src/components/organisms/MainDetails/MainDetails.tsx
  21. 5 2
      src/components/organisms/MinionEndpointModal/MinionEndpointModal.tsx
  22. 53 49
      src/components/organisms/MinionPoolDetailsContent/MinionPoolDetailsContent.tsx
  23. 353 0
      src/components/organisms/MinionPoolDetailsContent/MinionPoolEvents.tsx
  24. 271 0
      src/components/organisms/MinionPoolDetailsContent/MinionPoolMachines.tsx
  25. 29 41
      src/components/organisms/MinionPoolDetailsContent/MinionPoolMainDetails.tsx
  26. 15 0
      src/components/organisms/MinionPoolDetailsContent/images/network.svg
  27. 19 23
      src/components/organisms/MinionPoolModal/MinionPoolModal.tsx
  28. 69 15
      src/components/organisms/MinionPoolModal/MinionPoolModalContent.tsx
  29. 2 1
      src/components/organisms/PageHeader/PageHeader.tsx
  30. 1 1
      src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx
  31. 21 18
      src/components/organisms/WizardOptions/WizardOptions.tsx
  32. 2 2
      src/components/organisms/WizardPageContent/WizardPageContent.tsx
  33. 1 1
      src/components/organisms/WizardSummary/WizardSummary.tsx
  34. 1 1
      src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx
  35. 4 2
      src/components/pages/MigrationsPage/MigrationsPage.tsx
  36. 89 155
      src/components/pages/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx
  37. 84 101
      src/components/pages/MinionPoolsPage/MinionPoolsPage.tsx
  38. 4 3
      src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  39. 5 3
      src/components/pages/ReplicasPage/ReplicasPage.tsx
  40. 1 1
      src/plugins/endpoint/default/OptionsSchemaPlugin.ts
  41. 4 4
      src/sources/MigrationSource.ts
  42. 71 81
      src/sources/MinionPoolSource.ts
  43. 4 6
      src/sources/ReplicaSource.ts
  44. 15 72
      src/stores/MinionPoolStore.ts
  45. 4 0
      src/utils/ObjectUtils.ts

+ 2 - 0
config.ts

@@ -109,6 +109,8 @@ const conf: Config = {
   // replicas, migrations, endpoints, users etc.
   mainListItemsPerPage: 20,
 
+  maxMinionPoolEventsPerPage: 50,
+
   servicesUrls: {
     keystone: '{BASE_URL}/identity',
     barbican: '{BASE_URL}/barbican',

+ 1 - 0
src/@types/Config.ts

@@ -34,4 +34,5 @@ export type Config = {
   passwordFields: string[],
   mainListItemsPerPage: number,
   servicesUrls: Services,
+  maxMinionPoolEventsPerPage: number,
 }

+ 8 - 1
src/@types/Field.ts

@@ -17,7 +17,14 @@ import LabelDictionary from '../utils/LabelDictionary'
 import { ProviderTypes } from './Providers'
 
 type Separator = { separator: boolean }
-type EnumItemObject = { label?: string, value?: any, name?: string, id?: string | null }
+type EnumItemObject = {
+  label?: string,
+  value?: any,
+  name?: string,
+  id?: string | null,
+  disabled?: boolean,
+  subtitleLabel?: string,
+}
 export const isEnumSeparator = (e: any): e is Separator => (typeof e !== 'string' && e.separator === true)
 
 export type EnumItem = (

+ 32 - 8
src/@types/MinionPool.ts

@@ -12,23 +12,47 @@ 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 { Execution } from './Execution'
-
+export type MinionMachine = {
+  id: string
+  created_at: string
+  updated_at: string
+  status: string
+  connection_info?: any
+  provider_properties: any
+}
+export type MinionPoolEvent = {
+  id: string
+  index: number
+  level: 'INFO' | 'DEBUG' | 'ERROR'
+  message: string
+  created_at: string
+}
+export type MinionPoolProgressUpdate = {
+  id: string
+  current_step: number
+  message: string
+  created_at: string
+}
+export type MinionPoolEventProgressUpdate = MinionPoolEvent | MinionPoolProgressUpdate
 export type MinionPool = {
   id: string
   created_at: string
   updated_at: string | null
-  pool_name: string
-  pool_os_type: string
-  pool_status: string
+  name: string
+  os_type: string
+  status: string
   minimum_minions: number
+  maximum_minions: number
   environment_options: { [prop: string]: any }
   endpoint_id: string
-  last_execution_status: string
   notes?: string
-  pool_platform: 'source' | 'destination'
+  platform: 'source' | 'destination',
+  minion_machines: MinionMachine[],
+  minion_retention_strategy: 'poweroff' | 'delete'
+  minion_max_idle_time: number,
 }
 
 export type MinionPoolDetails = MinionPool & {
-  executions: Execution[]
+  events: MinionPoolEvent[],
+  progress_updates: MinionPoolProgressUpdate[]
 }

+ 9 - 0
src/components/atoms/StatusIcon/StatusIcon.tsx

@@ -26,6 +26,7 @@ import warningImage from './images/warning'
 import pendingImage from './images/pending.svg'
 import successHollowImage from './images/success-hollow.svg'
 import errorHollowImage from './images/error-hollow.svg'
+import triangleImage from './images/triangle.svg'
 
 type Props = {
   status: string,
@@ -33,7 +34,9 @@ type Props = {
   hollow?: boolean,
   secondary?: boolean,
   style?: React.CSSProperties
+  triangle?: boolean
   onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+  title?: string
 }
 
 const getSpinnerUrl = (
@@ -49,6 +52,10 @@ const getRunningImageUrl = (props: Props) => {
 const getWarningUrl = (background: string) => `url('data:image/svg+xml;utf8,${encodeURIComponent(warningImage(background))}')`
 
 const statuses = (status: any, props: any) => {
+  if (props.triangle) {
+    return css`background-image: url('${triangleImage}');`
+  }
+
   switch (status) {
     case 'COMPLETED':
       return css`
@@ -57,6 +64,7 @@ const statuses = (status: any, props: any) => {
     case 'STARTING':
     case 'RUNNING':
     case 'PENDING':
+    case 'AWAITING_MINION_ALLOCATIONS':
       return css`
         background-image: ${getRunningImageUrl(props)};
         ${StyleProps.animations.rotation}
@@ -74,6 +82,7 @@ const statuses = (status: any, props: any) => {
     case 'FAILED_TO_SCHEDULE':
     case 'FAILED_TO_CANCEL':
     case 'ERROR':
+    case 'ERROR_ALLOCATING_MINIONS':
       return css`
         background-image: url('${props.hollow ? errorHollowImage : errorImage}');
       `

+ 17 - 0
src/components/atoms/StatusIcon/images/triangle.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<svg version="1.1" id="Layer_1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="16"
+ height="16"
+	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+	<g>
+		<path
+    fill="#dedfe6"
+    d="M501.362,383.95L320.497,51.474c-29.059-48.921-99.896-48.986-128.994,0L10.647,383.95
+			c-29.706,49.989,6.259,113.291,64.482,113.291h361.736C495.039,497.241,531.068,433.99,501.362,383.95z M256,437.241
+			c-16.538,0-30-13.462-30-30c0-16.538,13.462-30,30-30c16.538,0,30,13.462,30,30C286,423.779,272.538,437.241,256,437.241z
+			 M286,317.241c0,16.538-13.462,30-30,30c-16.538,0-30-13.462-30-30v-150c0-16.538,13.462-30,30-30c16.538,0,30,13.462,30,30
+			V317.241z"/>
+	</g>
+</svg>

+ 5 - 0
src/components/atoms/StatusIcon/story.tsx

@@ -70,3 +70,8 @@ storiesOf('StatusIcon', module)
       <StatusIcon status="CANCELLING" useBackground />
     </Wrapper>
   ))
+  .add('triangle', () => (
+    <Wrapper>
+      <StatusIcon status="INFO" triangle />
+    </Wrapper>
+  ))

+ 33 - 7
src/components/atoms/StatusPill/StatusPill.tsx

@@ -29,12 +29,24 @@ const LABEL_MAP: { [status: string]: string } = {
   CANCELLING_AFTER_COMPLETION: 'CANCELLING',
   FAILED_TO_SCHEDULE: 'UNSCHEDULABLE',
   FAILED_TO_CANCEL: 'CANCELED',
+  AWAITING_MINION_ALLOCATIONS: 'AWAITING MINIONS',
+  ERROR_ALLOCATING_MINIONS: 'MINIONS ERROR',
+  // Minion Pool statuses
+  VALIDATING_INPUTS: 'VALIDATING',
+  ALLOCATING_SHARED_RESOURCES: 'ALLOCATING',
+  ALLOCATING_MACHINES: 'ALLOCATING',
+  DEALLOCATING_MACHINES: 'DEALLOCATING',
+  DEALLOCATING_SHARED_RESOURCES: 'DEALLOCATING',
+  IN_MAINTENANCE: 'MAINTENANCE',
+  RESCALING: 'SCALING',
+  IN_USE: 'IN USE',
 }
 
 const statuses = (status: any) => {
   switch (status) {
     case 'COMPLETED':
-    case 'ALLOCATED':
+    case 'ALLOCATED': // Minion Pool status
+    case 'AVAILABLE': // Minion Pool status
       return css`
         background: ${Palette.success};
         color: white;
@@ -43,6 +55,7 @@ const statuses = (status: any) => {
     case 'FAILED_TO_SCHEDULE':
     case 'FAILED_TO_CANCEL':
     case 'ERROR':
+    case 'ERROR_ALLOCATING_MINIONS':
       return css`
         background: ${Palette.alert};
         color: white;
@@ -52,6 +65,7 @@ const statuses = (status: any) => {
     case 'CANCELED_FOR_DEBUGGING':
     case 'CANCELED_AFTER_COMPLETION':
     case 'FORCE_CANCELED':
+    case 'IN_MAINTENANCE': // Minion Pool status
       return css`
         background: ${Palette.warning};
         color: ${Palette.black};
@@ -66,9 +80,18 @@ const statuses = (status: any) => {
     case 'STARTING':
     case 'RUNNING':
     case 'PENDING':
-    case 'INITIALIZING':
-    case 'ALLOCATING':
-    case 'RECONFIGURING':
+    case 'AWAITING_MINION_ALLOCATIONS':
+    case 'INITIALIZING': // Minion Pool status
+    case 'ALLOCATING': // Minion Pool status
+    case 'RECONFIGURING': // Minion Pool status
+    case 'VALIDATING_INPUTS': // Minion Pool status
+    case 'ALLOCATING_SHARED_RESOURCES': // Minion Pool status
+    case 'ALLOCATING_MACHINES': // Minion Pool status
+    case 'SCALING': // Minion Pool status
+    case 'RESCALING': // Minion Pool status
+    case 'DEPLOYING': // Minion Pool status
+    case 'IN_USE': // Minion Pool status
+    case 'HEALTHCHECKING': // Minion Pool status
       return css`
         background: url('${runningImage}');
         animation: bgMotion 1s infinite linear;
@@ -83,6 +106,8 @@ const statuses = (status: any) => {
     case 'UNINITIALIZING':
     case 'DEALLOCATING':
     case 'CANCELLING_AFTER_COMPLETION':
+    case 'DEALLOCATING_MACHINES': // Minion Pool status
+    case 'DEALLOCATING_SHARED_RESOURCES': // Minion Pool status
       return css`
         background: url('${cancellingImage}');
         animation: bgMotion 1s infinite linear;
@@ -101,9 +126,9 @@ const statuses = (status: any) => {
         border-color: transparent;
       `
     case 'UNSCHEDULED':
-    case 'UNINITIALIZED':
-    case 'DEALLOCATED':
-    case 'INITIALIZED':
+    case 'UNINITIALIZED': // Minion Pool status
+    case 'DEALLOCATED': // Minion Pool status
+    case 'INITIALIZED': // Minion Pool status
       return css`
         background: ${Palette.grayscale[2]};
         color: ${Palette.black};
@@ -156,6 +181,7 @@ const Wrapper = styled.div<any>`
   ${(props: any) => statuses(props.status)}
   ${(props: any) => (props.status === 'INFO' ? getInfoStatusColor(props) : '')}
   text-transform: uppercase;
+  overflow: hidden;
 `
 
 type Props = {

+ 4 - 0
src/components/atoms/StatusPill/story.tsx

@@ -40,6 +40,8 @@ const STATUSES = [
   'FAILED_TO_SCHEDULE',
   'DEADLOCKED',
   'STRANDED_AFTER_DEADLOCK',
+  'AWAITING_MINION_ALLOCATIONS',
+  'ERROR_ALLOCATING_MINIONS',
   // Minion Pool statuses
   'INITIALIZED',
   'UNINITIALIZED',
@@ -50,6 +52,8 @@ const STATUSES = [
   'ALLOCATING',
   'ALLOCATED',
   'RECONFIGURING',
+  'IN_USE',
+  'HEALTHCHECKING',
 ]
 
 const renderAllStatuses = (small?: boolean) => (

+ 20 - 0
src/components/atoms/Switch/Switch.tsx

@@ -53,6 +53,13 @@ const InputWrapper = styled.div<any>`
   }
 `
 const inputBackground = (props: any) => {
+  if (props.checkedColor && props.checked) {
+    return props.checkedColor
+  }
+  if (props.uncheckedColor && !props.checked) {
+    return props.uncheckedColor
+  }
+
   if (props.big) {
     if (props.checked) {
       return Palette.alert
@@ -71,6 +78,13 @@ const inputBackground = (props: any) => {
   return 'white'
 }
 const getInputBorderColor = (props: any) => {
+  if (props.checkedColor && props.checked) {
+    return props.checkedColor
+  }
+  if (props.uncheckedColor && !props.checked) {
+    return props.uncheckedColor
+  }
+
   if (props.big && props.checked) {
     return Palette.alert
   }
@@ -148,6 +162,8 @@ type Props = {
   style?: React.CSSProperties,
   required?: boolean,
   highlight?: boolean,
+  checkedColor?: string,
+  uncheckedColor?: string,
 }
 type State = {
   lastChecked: boolean | null | undefined,
@@ -218,6 +234,8 @@ class Switch extends React.Component<Props, State> {
           checked={this.props.checked}
           height={this.props.height}
           secondary={this.props.secondary}
+          checkedColor={this.props.checkedColor}
+          uncheckedColor={this.props.uncheckedColor}
         >
           <InputThumb
             triState={this.props.triState}
@@ -225,6 +243,8 @@ class Switch extends React.Component<Props, State> {
             checked={this.props.checked}
             height={this.props.height}
             secondary={this.props.secondary}
+            checkedColor={this.props.checkedColor}
+            uncheckedColor={this.props.uncheckedColor}
           />
         </InputBackground>
       </InputWrapper>

+ 44 - 5
src/components/molecules/Dropdown/Dropdown.tsx

@@ -107,17 +107,41 @@ const Checkmark = styled.div<any>`
     }
   }
 `
+const getListItemColor = (props: any) => {
+  if (props.disabled) {
+    return Palette.grayscale[3]
+  }
+  if (props.multipleSelected) {
+    return Palette.primary
+  }
+  if (props.selected) {
+    return 'white'
+  }
+  if (props.dim) {
+    return Palette.grayscale[3]
+  }
+  return Palette.grayscale[4]
+}
+const getListBackgroundColor = (props: any) => {
+  if (props.arrowSelected) {
+    return css`background: ${Palette.primary}44;`
+  }
+  if (props.selected) {
+    return css`background: ${Palette.primary};`
+  }
+  return ''
+}
 const ListItem = styled.div<any>`
   position: relative;
   display: flex;
-  color: ${(props: any) => (props.multipleSelected ? Palette.primary : props.selected ? 'white' : props.dim ? Palette.grayscale[3] : Palette.grayscale[4])};
-  ${(props: any) => (props.arrowSelected ? css`background: ${Palette.primary}44;` : '')}
-  ${(props: any) => (props.selected ? css`background: ${Palette.primary};` : '')}
+  color: ${(props: any) => getListItemColor(props)};
+  ${(props: any) => getListBackgroundColor(props)}
   ${(props: any) => (props.selected ? css`font-weight: ${StyleProps.fontWeights.medium};` : '')}
   padding: 8px 16px;
   transition: all ${StyleProps.animations.swift};
   padding-left: ${(props: any) => props.paddingLeft}px;
   word-break: break-word;
+  ${props => (props.disabled ? css`cursor: default;` : '')}
 
   &:first-child {
     border-top-left-radius: ${StyleProps.borderRadius};
@@ -137,7 +161,11 @@ const ListItem = styled.div<any>`
     }
   }
 `
-const DuplicatedLabel = styled.div<any>`
+const SubtitleLabel = styled.div`
+  display: flex;
+  font-size: 11px;
+`
+const DuplicatedLabel = styled.div`
   display: flex;
   font-size: 11px;
   span {
@@ -445,6 +473,12 @@ class Dropdown extends React.Component<Props, State> {
       this.wrapperRef.focus()
       setTimeout(() => { this.ignoreFocusHandler = false }, 100)
     }
+
+    if (item.disabled) {
+      resetFocus()
+      return
+    }
+
     if (!this.props.multipleSelection) {
       this.setState({ showDropdownList: false, firstItemHover: false }, () => {
         resetFocus()
@@ -588,6 +622,7 @@ class Dropdown extends React.Component<Props, State> {
                 dim={this.props.dimFirstItem && i === 0}
                 paddingLeft={this.props.multipleSelection ? 8 : 16}
                 arrowSelected={i === this.state.arrowSelection}
+                disabled={item.disabled}
               >
                 {this.props.multipleSelection ? (
                   <Checkmark
@@ -598,7 +633,11 @@ class Dropdown extends React.Component<Props, State> {
                 ) : null}
                 <Labels>
                   {label === '' ? '\u00A0' : label}
-                  {duplicatedLabel ? <DuplicatedLabel> (<span>{value || ''}</span>)</DuplicatedLabel> : ''}
+                  {item.subtitleLabel ? (
+                    <SubtitleLabel>{item.subtitleLabel}</SubtitleLabel>
+                  ) : null}
+
+                  {duplicatedLabel ? <DuplicatedLabel>(<span>{value || ''}</span>)</DuplicatedLabel> : ''}
                 </Labels>
               </ListItem>
             )

+ 16 - 0
src/components/molecules/Dropdown/story.tsx

@@ -135,3 +135,19 @@ storiesOf('Dropdown', module)
       ]}
     />
   ))
+  .add('subtitle label', () => (
+    <Wrapper items={[
+      {
+        label: 'Item 1',
+        value: 'item-1',
+        subtitleLabel: 'Pool is in UNALLOCATED status instead of being ALLOCATED.',
+        disabled: true,
+      },
+      { label: 'Item 2', value: 'item-2' },
+      { label: 'Item 3', value: 'item-3' },
+      { label: 'Item 4', value: 'item-4' },
+      { separator: true },
+      { label: 'Item 1', value: 'item-1' },
+    ]}
+    />
+  ))

+ 5 - 1
src/components/molecules/FieldInput/FieldInput.tsx

@@ -228,11 +228,15 @@ class FieldInput extends React.Component<Props> {
       return {
         label: typeof e === 'string' ? (useDictionary ? LabelDictionary.get(e) : e) : e.name || e.label,
         value: typeof e === 'string' ? e : e.id || e.value,
+        disabled: typeof e !== 'string' ? Boolean(e.disabled) : false,
+        subtitleLabel: typeof e !== 'string' ? e.subtitleLabel || '' : false,
       }
     })
     if (this.props.addNullValue) {
       items = [
-        { label: 'Choose a value', value: null },
+        {
+          label: 'Choose a value', value: null, disabled: false, subtitleLabel: '',
+        },
         ...items,
       ]
     }

+ 13 - 4
src/components/molecules/MainDetailsTable/MainDetailsTable.tsx

@@ -280,7 +280,10 @@ class MainDetailsTable extends React.Component<Props, State> {
           destinationKey = destinationName as string
           destinationBody = getBody(transferDisk)
         }
-      } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
+      } else if (this.props.item?.type === 'migration' && (
+        this.props.item.last_execution_status === 'RUNNING'
+        || this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
+      )) {
         destinationBody = ['Waiting for migration to finish']
       }
 
@@ -355,7 +358,10 @@ class MainDetailsTable extends React.Component<Props, State> {
             destinationNetworkName = destinationNic.network_name
             destinationBody = getBody(destinationNic)
           }
-        } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
+        } else if (this.props.item?.type === 'migration' && (
+          this.props.item.last_execution_status === 'RUNNING'
+          || this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
+        )) {
           destinationBody = ['Waiting for migration to finish']
         }
 
@@ -389,7 +395,7 @@ class MainDetailsTable extends React.Component<Props, State> {
       && minionPoolMappings[instance.instance_name || instance.id || instance.name]
     if (minionPoolId) {
       const minionPool = this.props.minionPools.find(m => m.id === minionPoolId)
-      sourceBody.push(`Minion Pool: ${minionPool?.pool_name || minionPoolId}`)
+      sourceBody.push(`Minion Pool: ${minionPool?.name || minionPoolId}`)
     }
     let destinationBody: string[] = []
     let destinationName: string = ''
@@ -397,7 +403,10 @@ class MainDetailsTable extends React.Component<Props, State> {
     if (transferResult) {
       destinationName = transferResult.instance_name || transferResult.name
       destinationBody = getBody(transferResult)
-    } else if (this.props.item?.type === 'migration' && this.props.item.last_execution_status === 'RUNNING') {
+    } else if (this.props.item?.type === 'migration' && (
+      this.props.item.last_execution_status === 'RUNNING'
+      || this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
+    )) {
       destinationName = 'Waiting for migration to finish'
     }
     const instanceName = instance.instance_name || instance.id

+ 110 - 0
src/components/molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal.tsx

@@ -0,0 +1,110 @@
+/*
+Copyright (C) 2020  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+
+import Button from '../../atoms/Button'
+
+import KeyboardManager from '../../../utils/KeyboardManager'
+import StatusImage from '../../atoms/StatusImage/StatusImage'
+import FieldInput from '../FieldInput/FieldInput'
+import Modal from '../Modal/Modal'
+
+const Wrapper = styled.div<any>`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 32px 32px 32px;
+`
+const Header = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 32px;
+`
+const Description = styled.div`
+  margin-top: 32px;
+`
+const Form = styled.div<any>`
+  height: 120px;
+`
+const FieldInputStyled = styled(FieldInput)`
+  width: 319px;
+  justify-content: space-between;
+`
+const Buttons = styled.div<any>`
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+`
+type Props = {
+  onCancelClick: () => void,
+  onExecuteClick: (force: boolean) => void,
+}
+type State = {
+  force: boolean,
+}
+@observer
+class MinionPoolConfirmationModal extends React.Component<Props, State> {
+  state: State = {
+    force: false,
+  }
+
+  componentDidMount() {
+    KeyboardManager.onEnter('minion-pool-confirmation', () => { this.props.onExecuteClick(this.state.force) }, 2)
+  }
+
+  componentWillUnmount() {
+    KeyboardManager.removeKeyDown('minion-pool-confirmation')
+  }
+
+  render() {
+    return (
+      <Modal
+        isOpen
+        title="Minion Pool Deallocate Confirmation"
+        onRequestClose={this.props.onCancelClick}
+      >
+        <Wrapper>
+          <Header>
+            <StatusImage status="CONFIRMATION" />
+            <Description>Are you sure you want to deallocate the minion pool?</Description>
+          </Header>
+          <Form>
+            <FieldInputStyled
+              name="force"
+              description="Whether to force the deallocation of the Minion Pool and its machines. This will affect all Migrations/Replicas currently using the pool’s resources."
+              type="boolean"
+              layout="page"
+              value={this.state.force}
+              label="Force"
+              onChange={value => { this.setState({ force: value }) }}
+            />
+          </Form>
+          <Buttons>
+            <Button secondary onClick={this.props.onCancelClick}>Cancel</Button>
+            <Button
+              onClick={() => { this.props.onExecuteClick(this.state.force) }}
+            >Deallocate
+            </Button>
+          </Buttons>
+        </Wrapper>
+      </Modal>
+    )
+  }
+}
+
+export default MinionPoolConfirmationModal

+ 6 - 0
src/components/molecules/MinionPoolConfirmationModal/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "MinionPoolConfirmationModal",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./MinionPoolConfirmationModal.tsx"
+}

+ 10 - 7
src/components/molecules/MinionPoolListItem/MinionPoolListItem.tsx

@@ -106,7 +106,7 @@ type Props = {
 @observer
 class MinionPoolListItem extends React.Component<Props> {
   getStatus() {
-    return this.props.item.pool_status
+    return this.props.item.status
   }
 
   renderCreationDate() {
@@ -135,11 +135,14 @@ class MinionPoolListItem extends React.Component<Props> {
     )
   }
 
-  renderUser() {
+  renderCreatedCount() {
+    const createdCount = this.props.item.minion_machines.filter(m => m.status === 'ALLOCATED' || m.status === 'AVAILABLE').length
+    const totalCount = this.props.item.minion_machines.length
+
     return (
-      <Column style={{ minWidth: '115px', maxWidth: '115px' }}>
+      <Column style={{ minWidth: '150px', maxWidth: '150px' }}>
         <ItemLabel>
-          OS Type
+          Allocated
         </ItemLabel>
         <ItemValue
           style={{
@@ -147,7 +150,7 @@ class MinionPoolListItem extends React.Component<Props> {
             overflow: 'hidden',
           }}
         >
-          {this.props.item.pool_os_type}
+          {createdCount} of {totalCount} machines<br />({this.props.item.maximum_minions} maximum)
         </ItemValue>
       </Column>
     )
@@ -171,7 +174,7 @@ class MinionPoolListItem extends React.Component<Props> {
         <Content onClick={this.props.onClick}>
           <Image />
           <Title>
-            <TitleLabel>{this.props.item.pool_name}</TitleLabel>
+            <TitleLabel>{this.props.item.name}</TitleLabel>
             <StatusWrapper>
               {status ? (
                 <StatusPill
@@ -184,7 +187,7 @@ class MinionPoolListItem extends React.Component<Props> {
           {endpointImage}
           {this.renderCreationDate()}
           {this.renderUpdateDate()}
-          {this.renderUser()}
+          {this.renderCreatedCount()}
         </Content>
       </Wrapper>
     )

+ 1 - 1
src/components/organisms/EditReplica/EditReplica.tsx

@@ -581,7 +581,7 @@ class EditReplica extends React.Component<Props, State> {
       dictionaryKey = `${endpoint.type}-${type}`
     }
     const minionPools = minionPoolStore.minionPools
-      .filter(m => m.pool_platform === type && m.endpoint_id === endpoint.id)
+      .filter(m => m.platform === type && m.endpoint_id === endpoint.id)
     return (
       <WizardOptions
         minionPools={minionPools}

+ 1 - 1
src/components/organisms/Executions/Executions.tsx

@@ -269,7 +269,7 @@ class Executions extends React.Component<Props, State> {
       return null
     }
 
-    if (this.state.selectedExecution.status === 'RUNNING') {
+    if (this.state.selectedExecution.status === 'RUNNING' || this.state.selectedExecution.status === 'AWAITING_MINION_ALLOCATIONS') {
       return (
         <Button
           secondary

+ 31 - 2
src/components/organisms/MainDetails/MainDetails.tsx

@@ -46,6 +46,17 @@ const Wrapper = styled.div<any>`
   flex-direction: column;
   padding-bottom: 48px;
 `
+const WarningWrapper = styled.div`
+  display: flex;
+  background: ${Palette.warning}66;
+  padding: 8px;
+  border-radius: 4px;
+  margin-bottom: 24px;
+  align-items: center;
+`
+const WarningText = styled.div`
+  margin-left: 8px;
+`
 const ColumnsLayout = styled.div<any>`
   display: flex;
 `
@@ -328,7 +339,7 @@ class MainDetails extends React.Component<Props, State> {
               <Field>
                 <Label>Source Minion Pool</Label>
                 {sourceMinionPool ? (
-                  <ValueLink to={`/minion-pools/${sourceMinionPool.id}`}>{sourceMinionPool.pool_name}</ValueLink>
+                  <ValueLink to={`/minion-pools/${sourceMinionPool.id}`}>{sourceMinionPool.name}</ValueLink>
                 ) : (
                   <Value>{this.props.item.origin_minion_pool_id}</Value>
                 )}
@@ -376,7 +387,7 @@ class MainDetails extends React.Component<Props, State> {
               <Field>
                 <Label>Target Minion Pool</Label>
                 {destMinionPool ? (
-                  <ValueLink to={`/minion-pools/${destMinionPool.id}`}>{destMinionPool.pool_name}</ValueLink>
+                  <ValueLink to={`/minion-pools/${destMinionPool.id}`}>{destMinionPool.name}</ValueLink>
                 ) : (
                   <Value>{this.props.item.destination_minion_pool_id}</Value>
                 )}
@@ -408,9 +419,27 @@ class MainDetails extends React.Component<Props, State> {
     )
   }
 
+  renderSpecialError() {
+    if (this.props.item?.last_execution_status !== 'ERROR_ALLOCATING_MINIONS') {
+      return null
+    }
+
+    return (
+      <WarningWrapper>
+        <StatusIcon status="ERROR" />
+        <WarningText>
+          There was an error allocating minion machines for this {this.props.item.type}.
+          Please review the log events for the selected minion pool(s)
+          and the logs of the Coriolis Minion Manager component for full details.
+        </WarningText>
+      </WarningWrapper>
+    )
+  }
+
   render() {
     return (
       <Wrapper>
+        {this.renderSpecialError()}
         {this.renderTable()}
         {this.props.instancesDetailsLoading || this.props.loading ? null : (
           <MainDetailsTable

+ 5 - 2
src/components/organisms/MinionEndpointModal/MinionEndpointModal.tsx

@@ -25,6 +25,7 @@ import { providerTypes } from '../../../constants'
 import EndpointLogos from '../../atoms/EndpointLogos/EndpointLogos'
 import Dropdown from '../../molecules/Dropdown/Dropdown'
 import Button from '../../atoms/Button/Button'
+import Palette from '../../styleUtils/Palette'
 
 const Wrapper = styled.div``
 const LoadingWrapper = styled.div`
@@ -136,11 +137,13 @@ class MinionEndpointModal extends React.Component<Props, State> {
     return (
       <PoolPlatformWrapper>
         <PoolPlatformOptions>
-          <PoolPlatformOption>Source Platform</PoolPlatformOption>
+          <PoolPlatformOption>Source Minion Pool</PoolPlatformOption>
           <SwitchWrapper>
             <Switch
               big
               checked={this.state.platform === 'destination'}
+              checkedColor={Palette.primary}
+              uncheckedColor={Palette.primary}
               onChange={value => {
                 this.setState({
                   platform: value ? 'destination' : 'source',
@@ -148,7 +151,7 @@ class MinionEndpointModal extends React.Component<Props, State> {
               }}
             />
           </SwitchWrapper>
-          <PoolPlatformOption>Destination Platform</PoolPlatformOption>
+          <PoolPlatformOption>Destination Minion Pool</PoolPlatformOption>
         </PoolPlatformOptions>
       </PoolPlatformWrapper>
     )

+ 53 - 49
src/components/organisms/MinionPoolDetailsContent/MinionPoolDetailsContent.tsx

@@ -18,21 +18,26 @@ import { observer } from 'mobx-react'
 
 import Button from '../../atoms/Button/Button'
 import DetailsNavigation from '../../molecules/DetailsNavigation/DetailsNavigation'
-import Executions from '../Executions/Executions'
 import type { Endpoint } from '../../../@types/Endpoint'
-import type { Execution, ExecutionTasks } from '../../../@types/Execution'
 import type { Field } from '../../../@types/Field'
 import StyleProps from '../../styleUtils/StyleProps'
-import { MinionPoolDetails } from '../../../@types/MinionPool'
-import { MinionPoolAction } from '../../../stores/MinionPoolStore'
 import MinionPoolMainDetails from './MinionPoolMainDetails'
 import { ReplicaItem, MigrationItem } from '../../../@types/MainItem'
+import { MinionPoolDetails } from '../../../@types/MinionPool'
+import MinionPoolMachines from './MinionPoolMachines'
+import StatusImage from '../../atoms/StatusImage/StatusImage'
+import MinionPoolEvents from './MinionPoolEvents'
 
 const Wrapper = styled.div<any>`
   display: flex;
   justify-content: center;
 `
-
+const Loading = styled.div<any>`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 200px;
+`
 const Buttons = styled.div<any>`
   display: flex;
   justify-content: space-between;
@@ -58,8 +63,12 @@ const NavigationItems = [
     value: '',
   },
   {
-    label: 'Executions',
-    value: 'executions',
+    label: 'Machines',
+    value: 'machines',
+  },
+  {
+    label: 'Events',
+    value: 'events',
   },
 ]
 
@@ -70,21 +79,15 @@ type Props = {
   endpoints: Endpoint[],
   schema: Field[],
   schemaLoading: boolean,
+  loading: boolean,
   page: string,
-  detailsLoading: boolean,
-  executions: Execution[],
-  executionsLoading: boolean,
-  executionsTasksLoading: boolean,
-  executionsTasks: ExecutionTasks[],
-  onRunAction: (action: MinionPoolAction) => void,
-  onExecutionChange: (executionId: string) => void,
-  onCancelExecutionClick: (execution: Execution | null, force?: boolean) => void,
+  onAllocate: () => void,
   onDeleteMinionPoolClick: () => void,
 }
 @observer
 class MinionPoolDetailsContent extends React.Component<Props> {
   getStatus() {
-    return this.props.item?.pool_status
+    return this.props.item?.status
   }
 
   isEndpointMissing() {
@@ -95,34 +98,27 @@ class MinionPoolDetailsContent extends React.Component<Props> {
   }
 
   renderBottomControls() {
-    const uninitialized = this.props.item?.pool_status === 'UNINITIALIZED'
-    const deallocated = this.props.item?.pool_status === 'DEALLOCATED'
+    const status = this.props.item?.status
+    const deleteEnabled = status === 'DEALLOCATED' || status === 'ERROR'
+    const deallocated = this.props.item?.status === 'DEALLOCATED'
 
     return (
       <Buttons>
         <ButtonColumn>
-          <Button
-            primary
-            hollow
-            disabled={this.isEndpointMissing() || !uninitialized}
-            onClick={() => { this.props.onRunAction('set-up-shared-resources') }}
-          >Setup Shared Resources
-          </Button>
           <Button
             primary
             hollow
             disabled={this.isEndpointMissing() || !deallocated}
-            onClick={() => { this.props.onRunAction('allocate-machines') }}
-          >Allocate Machines
+            onClick={() => { this.props.onAllocate() }}
+          >Allocate
           </Button>
         </ButtonColumn>
         <ButtonColumn>
           <Button
             alert
             hollow
-            disabled={!uninitialized}
+            disabled={!deleteEnabled}
             onClick={this.props.onDeleteMinionPoolClick}
-            data-test-id="rdContent-deleteButton"
           >Delete Minion Pool
           </Button>
         </ButtonColumn>
@@ -130,6 +126,30 @@ class MinionPoolDetailsContent extends React.Component<Props> {
     )
   }
 
+  renderLoading() {
+    return (
+      <Loading>
+        <StatusImage loading />
+      </Loading>
+    )
+  }
+
+  renderMachines() {
+    if (this.props.page !== 'machines') {
+      return null
+    }
+
+    return <MinionPoolMachines item={this.props.item} />
+  }
+
+  renderEvents() {
+    if (this.props.page !== 'events') {
+      return null
+    }
+
+    return <MinionPoolEvents item={this.props.item} />
+  }
+
   renderMainDetails() {
     if (this.props.page !== '') {
       return null
@@ -142,30 +162,12 @@ class MinionPoolDetailsContent extends React.Component<Props> {
         migrations={this.props.migrations}
         schema={this.props.schema}
         schemaLoading={this.props.schemaLoading}
-        loading={this.props.detailsLoading}
         endpoints={this.props.endpoints}
         bottomControls={this.renderBottomControls()}
       />
     )
   }
 
-  renderExecutions() {
-    if (this.props.page !== 'executions') {
-      return null
-    }
-
-    return (
-      <Executions
-        executions={this.props.executions}
-        executionsTasks={this.props.executionsTasks}
-        onCancelExecutionClick={this.props.onCancelExecutionClick}
-        loading={this.props.executionsLoading}
-        onChange={this.props.onExecutionChange}
-        tasksLoading={this.props.executionsTasksLoading}
-      />
-    )
-  }
-
   render() {
     return (
       <Wrapper>
@@ -176,8 +178,10 @@ class MinionPoolDetailsContent extends React.Component<Props> {
           itemType="minion-pool"
         />
         <DetailsBody>
-          {this.renderMainDetails()}
-          {this.renderExecutions()}
+          {!this.props.loading ? this.renderMainDetails() : null}
+          {!this.props.loading ? this.renderMachines() : null}
+          {!this.props.loading ? this.renderEvents() : null}
+          {this.props.loading ? this.renderLoading() : null}
         </DetailsBody>
       </Wrapper>
     )

+ 353 - 0
src/components/organisms/MinionPoolDetailsContent/MinionPoolEvents.tsx

@@ -0,0 +1,353 @@
+/*
+Copyright (C) 2020  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 * as React from 'react'
+import moment from 'moment'
+import styled from 'styled-components'
+import {
+  MinionPoolDetails, MinionPoolEventProgressUpdate,
+} from '../../../@types/MinionPool'
+import Palette from '../../styleUtils/Palette'
+import StyleProps from '../../styleUtils/StyleProps'
+import StatusIcon from '../../atoms/StatusIcon/StatusIcon'
+import Pagination from '../../atoms/Pagination/Pagination'
+import configLoader from '../../../utils/Config'
+import DropdownLink from '../../molecules/DropdownLink/DropdownLink'
+import InfoIcon from '../../atoms/InfoIcon/InfoIcon'
+
+const Wrapper = styled.div``
+const Filters = styled.div`
+  margin-bottom: 24px;
+  display: flex;
+`
+const FilterDropdownWrapper = styled.div`
+  margin-left: 24px;
+`
+const EventsTable = styled.div`
+  background: ${Palette.grayscale[1]};
+  border-radius: ${StyleProps.borderRadius};
+  margin-bottom: 16px;
+`
+const Header = styled.div<any>`
+  display: flex;
+  border-bottom: 1px solid ${Palette.grayscale[5]};
+  padding: 4px 8px;
+`
+type DataDivProps = {
+  width?: string,
+  grow?: boolean
+  secondary?: boolean
+}
+const HeaderData = styled.div<DataDivProps>`
+  ${props => (props.width ? StyleProps.exactWidth(props.width) : '')}
+  ${props => (props.grow ? 'flex-grow: 1;' : '')}
+  font-size: 10px;
+  color: ${Palette.grayscale[5]};
+  font-weight: ${StyleProps.fontWeights.medium};
+  text-transform: uppercase;
+`
+const Body = styled.div``
+const Row = styled.div`
+  display: flex;
+  padding: 8px;
+  border-bottom: 1px solid white;
+`
+const RowData = styled.div<DataDivProps>`
+  ${props => (props.width ? StyleProps.exactWidth(props.width) : '')}
+  ${props => (props.grow ? 'flex-grow: 1;' : '')}
+  ${props => (props.secondary ? `color: ${Palette.grayscale[4]};` : '')}
+`
+const Message = styled.pre`
+  font-family: inherit;
+  white-space: pre-line;
+  margin: inherit;
+`
+const NoData = styled.div`
+  text-align: center;
+`
+type FilterType = 'all' | 'events' | 'progress'
+type EventLevel = 'DEBUG' | 'INFO' | 'ERROR'
+type OrderDir = 'asc' | 'desc'
+type Props = {
+  item?: MinionPoolDetails | null,
+}
+
+type State = {
+  allEvents: MinionPoolEventProgressUpdate[],
+  prevLenghts: number[]
+  currentPage: number
+  filterBy: FilterType
+  eventLevel: EventLevel
+  orderDir: OrderDir
+}
+class MinionPoolEvents extends React.Component<Props, State> {
+  static sortData(data: MinionPoolEventProgressUpdate[]): MinionPoolEventProgressUpdate[] {
+    return data.slice()
+      .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+  }
+
+  state = {
+    allEvents: [] as MinionPoolEventProgressUpdate[],
+    prevLenghts: [0, 0],
+    currentPage: 1,
+    filterBy: 'events' as FilterType,
+    eventLevel: 'INFO' as EventLevel,
+    orderDir: 'desc' as OrderDir,
+  }
+
+  get filteredEventsWithoutPagination() {
+    const shouldFilterByEventType = (event: any): boolean => {
+      if (this.state.filterBy === 'events') {
+        return event.level
+      }
+      if (this.state.filterBy === 'progress') {
+        return event.current_step != null
+      }
+      return true
+    }
+    const shouldFilterByLevel = (event: any): boolean => {
+      if (!event.level) {
+        return true
+      }
+      if (this.state.eventLevel === 'INFO') {
+        return event.level === 'INFO' || event.level === 'ERROR'
+      }
+      if (this.state.eventLevel === 'ERROR') {
+        return event.level === 'ERROR'
+      }
+      return true
+    }
+
+    return this.state.allEvents
+      .filter((event: any) => shouldFilterByEventType(event) && shouldFilterByLevel(event))
+      .sort((a: any, b: any) => {
+        if (a.index && b.index) {
+          return this.state.orderDir === 'asc' ? a.index - b.index : b.index - a.index
+        }
+        const aTime = new Date(a.created_at).getTime()
+        const bTime = new Date(b.created_at).getTime()
+        return this.state.orderDir === 'asc' ? aTime - bTime : bTime - aTime
+      })
+  }
+
+  get filteredEvents() {
+    return this.filteredEventsWithoutPagination
+      .filter((_, i) => {
+        const minI = configLoader.config.maxMinionPoolEventsPerPage * (this.state.currentPage - 1)
+        const maxI = minI + configLoader.config.maxMinionPoolEventsPerPage
+        return i >= minI && i < maxI
+      })
+  }
+
+  static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null {
+    if (!props.item) {
+      return null
+    }
+    const events = props.item?.events || []
+    const progressUpdates = props.item?.progress_updates || []
+    if (events.length === state.prevLenghts[0] && progressUpdates.length === state.prevLenghts[1]) {
+      return null
+    }
+
+    return {
+      allEvents: events.concat(progressUpdates as any),
+      prevLenghts: [events.length, progressUpdates.length],
+    }
+  }
+
+  setOrderDir(orderDir: OrderDir) {
+    this.setState({ orderDir, currentPage: 1 })
+  }
+
+  filterByType(filterBy: FilterType) {
+    this.setState({ filterBy, currentPage: 1 })
+  }
+
+  filterByLevel(eventLevel: EventLevel) {
+    this.setState({ eventLevel, currentPage: 1 })
+  }
+
+  handlePreviousPageClick() {
+    this.setState(state => ({ currentPage: state.currentPage - 1 }))
+  }
+
+  handleNextPageClick() {
+    this.setState(state => ({ currentPage: state.currentPage + 1 }))
+  }
+
+  renderHeader() {
+    return (
+      <Header>
+        <HeaderData grow>
+          Event / Progress Update Message
+        </HeaderData>
+        <HeaderData width="192px">
+          Timestamp
+        </HeaderData>
+      </Header>
+    )
+  }
+
+  renderBody() {
+    return (
+      <Body>
+        {this.filteredEvents.map((event: any) => {
+          let status: string | null = null
+          if (event.level) {
+            if (event.level === 'DEBUG') {
+              status = 'WARNING'
+            }
+            if (event.level === 'INFO') {
+              status = 'UNEXECUTED'
+            }
+            if (event.level === 'ERROR') {
+              status = 'ERROR'
+            }
+          }
+          return (
+            <Row key={event.id}>
+              <RowData
+                grow
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  paddingRight: '8px',
+                }}
+              >
+                {status ? (
+                  <StatusIcon
+                    style={{ marginRight: '8px' }}
+                    status={status}
+                    title="Event"
+                  />
+                ) : null}
+                {event.current_step ? (
+                  <StatusIcon
+                    style={{ marginRight: '8px' }}
+                    status="INFO"
+                    title="Progress Update"
+                    triangle
+                  />
+                ) : null}
+                <Message>{event.message}</Message>
+              </RowData>
+              <RowData width="192px" secondary>{moment(event.created_at).format('YYYY-MM-DD HH:mm:ss')}</RowData>
+            </Row>
+          )
+        })}
+      </Body>
+    )
+  }
+
+  renderPagination() {
+    if (this.filteredEventsWithoutPagination.length
+      <= configLoader.config.maxMinionPoolEventsPerPage) {
+      return null
+    }
+    const totalPages = Math.ceil(this.filteredEventsWithoutPagination.length
+      / configLoader.config.maxMinionPoolEventsPerPage)
+    return (
+      <Pagination
+        previousDisabled={this.state.currentPage === 1}
+        nextDisabled={this.state.currentPage === totalPages}
+        onPreviousClick={() => { this.handlePreviousPageClick() }}
+        onNextClick={() => { this.handleNextPageClick() }}
+        currentPage={this.state.currentPage}
+        totalPages={totalPages}
+      />
+    )
+  }
+
+  renderEventsTable() {
+    return (
+      <EventsTable>
+        {this.renderHeader()}
+        {this.renderBody()}
+      </EventsTable>
+    )
+  }
+
+  renderFilters() {
+    return (
+      <Filters>
+        <FilterDropdownWrapper>
+          <DropdownLink
+            selectedItem={this.state.filterBy}
+            items={[
+              { label: 'Events', value: 'events' },
+              { label: 'Progress Updates', value: 'progress' },
+              { label: 'Events & Progress Updates', value: 'all' },
+            ]}
+            onChange={item => { this.filterByType(item.value as FilterType) }}
+          />
+        </FilterDropdownWrapper>
+        <FilterDropdownWrapper style={{ opacity: this.state.filterBy === 'progress' ? 0.5 : 1 }}>
+          <DropdownLink
+            disabled={this.state.filterBy === 'progress'}
+            selectedItem={this.state.eventLevel}
+            items={[
+              { label: 'DEBUG Event Level', value: 'DEBUG' },
+              { label: 'INFO Event Level', value: 'INFO' },
+              { label: 'ERROR Event Level', value: 'ERROR' },
+            ]}
+            onChange={item => { this.filterByLevel(item.value as EventLevel) }}
+          />
+          <InfoIcon text="The log level only applies to the events. The progress updates are not affected." />
+        </FilterDropdownWrapper>
+        <FilterDropdownWrapper>
+          <DropdownLink
+            selectedItem={this.state.orderDir}
+            items={[
+              { label: 'Ascending Order', value: 'asc' },
+              { label: 'Descending Order', value: 'desc' },
+            ]}
+            onChange={item => { this.setOrderDir(item.value as OrderDir) }}
+          />
+        </FilterDropdownWrapper>
+      </Filters>
+    )
+  }
+
+  renderNoData() {
+    return (
+      <NoData>
+        There are no events or progress updates associated with this minion pool.
+      </NoData>
+    )
+  }
+
+  renderNoDataFound() {
+    return (
+      <NoData>
+        No events found
+      </NoData>
+    )
+  }
+
+  render() {
+    const isNoData = this.state.allEvents.length === 0
+    const isNoDataFound = this.filteredEvents.length === 0
+    return (
+      <Wrapper>
+        {!isNoData ? this.renderFilters() : null}
+        {!isNoData && !isNoDataFound ? this.renderEventsTable() : null}
+        {!isNoData && !isNoDataFound ? this.renderPagination() : null}
+        {isNoData ? this.renderNoData() : null}
+        {isNoDataFound && !isNoData ? this.renderNoDataFound() : null}
+      </Wrapper>
+    )
+  }
+}
+
+export default MinionPoolEvents

+ 271 - 0
src/components/organisms/MinionPoolDetailsContent/MinionPoolMachines.tsx

@@ -0,0 +1,271 @@
+/*
+Copyright (C) 2020  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+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 * as React from 'react'
+import styled, { createGlobalStyle, css } from 'styled-components'
+import moment from 'moment'
+import { Collapse } from 'react-collapse'
+
+import { MinionMachine, MinionPool } from '../../../@types/MinionPool'
+import DropdownLink from '../../molecules/DropdownLink/DropdownLink'
+import Palette from '../../styleUtils/Palette'
+import StyleProps from '../../styleUtils/StyleProps'
+import Arrow from '../../atoms/Arrow/Arrow'
+
+import networkImage from './images/network.svg'
+import StatusPill from '../../atoms/StatusPill/StatusPill'
+
+const GlobalStyle = createGlobalStyle`
+  .ReactCollapse--collapse {
+    transition: height 0.4s ease-in-out;
+  }
+`
+const Wrapper = styled.div``
+const NoMachines = styled.div`
+  text-align: center;
+`
+const Header = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 32px;
+  margin-left: 20px;
+`
+const ArrowStyled = styled(Arrow)`
+  position: absolute;
+  left: -24px;
+`
+const Row = styled.div<any>`
+  position: relative;
+  padding: 8px 0;
+  border-top: 1px solid white;
+  transition: all ${StyleProps.animations.swift};
+  &:last-child {
+    border-bottom: 0;
+    border-bottom-left-radius: ${StyleProps.borderRadius};
+    border-bottom-right-radius: ${StyleProps.borderRadius};
+  }
+  &:hover {
+    background: ${Palette.grayscale[0]};
+    ${ArrowStyled} {
+      opacity: 1;
+    }
+  }
+  cursor: pointer;
+`
+const RowHeader = styled.div<any>`
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+`
+const RowHeaderColumn = styled.div<any>`
+  display: flex;
+  align-items: center;
+  ${StyleProps.exactWidth('50%')}
+`
+const HeaderName = styled.div<any>`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  ${props => StyleProps.exactWidth(`calc(100% - ${props.source ? 120 : 8}px)`)}
+`
+const HeaderIcon = styled.div<any>`
+  min-width: 16px;
+  min-height: 16px;
+  background: url('${networkImage}') center no-repeat;
+  margin-right: 16px;
+`
+const HeaderFilter = styled.div``
+const HeaderText = styled.div`
+  margin-left: 16px;
+`
+const RowBody = styled.div<any>`
+  display: flex;
+  color: ${Palette.grayscale[5]};
+  padding: 0 16px;
+  margin-top: 4px;
+`
+const RowBodyColumn = styled.div<any>`
+  margin-top: 8px;
+  &:first-child {
+    ${StyleProps.exactWidth('calc(50% - 70px)')}
+    margin-right: 88px;
+  }
+  &:last-child {
+    ${StyleProps.exactWidth('calc(50% - 16px)')}
+  }
+`
+const RowBodyColumnValue = styled.div<any>`
+  overflow-wrap: break-word;
+`
+const MachinesWrapper = styled.div``
+const MachineWrapper = styled.div`
+  background: ${Palette.grayscale[1]};
+  border-radius: ${StyleProps.borderRadius};
+`
+const MachineTitle = styled.div`
+  padding: 16px;
+  border-bottom: 1px solid #7F8795;
+  font-size: 16px;
+`
+const MachineBody = styled.div`
+  padding: 16px;
+`
+const MachineRow = styled.div<{secondary?: boolean}>`
+  ${props => (props.secondary ? css`
+    color: ${Palette.grayscale[5]};
+  ` : '')}
+`
+
+type FilterType = 'all' | 'allocated' | 'not-allocated'
+type Props = {
+  item?: MinionPool | null,
+}
+type State = {
+  filterStatus: FilterType
+  openedRows: string[]
+}
+class MinionPoolMachines extends React.Component<Props, State> {
+  state = {
+    filterStatus: 'all' as FilterType,
+    openedRows: [],
+  }
+
+  get machines() {
+    return this.props.item?.minion_machines || []
+  }
+
+  get filteredMachines() {
+    switch (this.state.filterStatus) {
+      case 'all':
+        return this.machines
+      case 'allocated':
+        return this.machines.filter(m => m.status === 'ALLOCATED' || m.status === 'AVAILABLE')
+      default:
+        return this.machines.filter(m => m.status !== 'ALLOCATED' && m.status !== 'AVAILABLE')
+    }
+  }
+
+  handleRowClick(id: string) {
+    if (this.state.openedRows.find(i => i === id)) {
+      this.setState(prevState => ({
+        openedRows: prevState.openedRows.filter(i => i !== id),
+      }))
+    } else {
+      this.setState(prevState => ({
+        openedRows: [...prevState.openedRows, id],
+      }))
+    }
+  }
+
+  renderNoMachines() {
+    return (
+      <NoMachines>There are no Minion Machines allocated to this Minion Pool</NoMachines>
+    )
+  }
+
+  renderHeader() {
+    const plural = this.machines.length === 1 ? '' : 's'
+    return (
+      <Header>
+        <HeaderFilter>
+          <DropdownLink
+            items={[
+              { label: 'All', value: 'all' },
+              { label: 'Allocated', value: 'allocated' },
+              { label: 'Not Allocated', value: 'not-allocated' },
+            ]}
+            selectedItem={this.state.filterStatus}
+            onChange={item => {
+              this.setState({
+                filterStatus: item.value as FilterType,
+              })
+            }}
+          />
+        </HeaderFilter>
+        <HeaderText>
+          {this.machines.length} minion machine{plural}, {this.machines.filter(m => m.status === 'ALLOCATED' || m.status === 'AVAILABLE').length} allocated
+        </HeaderText>
+      </Header>
+    )
+  }
+
+  renderConnectionInfo(machine: MinionMachine) {
+    const isOpened: boolean = Boolean(this.state.openedRows.find(i => i === machine.id))
+
+    return (
+      <Row onClick={() => { this.handleRowClick(machine.id) }}>
+        <ArrowStyled
+          primary
+          orientation={isOpened ? 'up' : 'down'}
+          opacity={isOpened ? 1 : 0}
+          thick
+        />
+        <RowHeader>
+          <RowHeaderColumn>
+            <HeaderIcon />
+            <HeaderName>Connection Info</HeaderName>
+          </RowHeaderColumn>
+        </RowHeader>
+        <Collapse isOpened={isOpened}>
+          <RowBody>
+            <RowBodyColumn>
+              {Object.keys(machine.connection_info).map(prop => (
+                <RowBodyColumnValue key={prop}>
+                  {prop}: {machine.connection_info[prop]}
+                </RowBodyColumnValue>
+              ))}
+            </RowBodyColumn>
+          </RowBody>
+        </Collapse>
+      </Row>
+    )
+  }
+
+  renderMachines() {
+    if (this.filteredMachines.length === 0) {
+      return (
+        <NoMachines>No Minion Machines found</NoMachines>
+      )
+    }
+
+    return (
+      <MachinesWrapper>
+        {this.filteredMachines.map(machine => (
+          <MachineWrapper key={machine.id}>
+            <MachineTitle>ID: {machine.id}</MachineTitle>
+            <MachineBody>
+              <MachineRow style={{ marginBottom: '8px', display: 'flex' }}>
+                Status: <StatusPill style={{ marginLeft: '8px' }} status={machine.status} />
+              </MachineRow>
+              <MachineRow secondary>Created At: {moment(machine.created_at).format('YYYY-MM-DD HH:mm:ss')}</MachineRow>
+              {machine.updated_at ? <MachineRow secondary>Updated At: {moment(machine.updated_at).format('YYYY-MM-DD HH:mm:ss')}</MachineRow> : null}
+            </MachineBody>
+            {machine.connection_info ? this.renderConnectionInfo(machine) : null}
+          </MachineWrapper>
+        ))}
+        <GlobalStyle />
+      </MachinesWrapper>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.props.item?.minion_machines.length ? this.renderHeader() : this.renderNoMachines()}
+        {this.props.item?.minion_machines.length ? this.renderMachines() : null}
+      </Wrapper>
+    )
+  }
+}
+
+export default MinionPoolMachines

+ 29 - 41
src/components/organisms/MinionPoolDetailsContent/MinionPoolMainDetails.tsx

@@ -20,7 +20,6 @@ import styled, { css } from 'styled-components'
 import EndpointLogos from '../../atoms/EndpointLogos'
 import CopyValue from '../../atoms/CopyValue'
 import StatusIcon from '../../atoms/StatusIcon'
-import StatusImage from '../../atoms/StatusImage'
 import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 
 import type { Endpoint } from '../../../@types/Endpoint'
@@ -33,9 +32,8 @@ import DateUtils from '../../../utils/DateUtils'
 import LabelDictionary from '../../../utils/LabelDictionary'
 import { OptionsSchemaPlugin } from '../../../plugins/endpoint'
 
-import { MinionPoolDetails } from '../../../@types/MinionPool'
-import StatusPill from '../../atoms/StatusPill/StatusPill'
 import { TransferItem, ReplicaItem, MigrationItem } from '../../../@types/MainItem'
+import { MinionPool } from '../../../@types/MinionPool'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -81,12 +79,6 @@ const ValueLink = styled(Link)`
   text-decoration: none;
   cursor: pointer;
 `
-const Loading = styled.div<any>`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 200px;
-`
 const PropertiesTable = styled.div<any>``
 const PropertyRow = styled.div<any>`
   display: flex;
@@ -111,14 +103,13 @@ const PropertyValue = styled.div<any>`
 `
 
 type Props = {
-  item?: MinionPoolDetails | null,
+  item?: MinionPool | null,
   replicas: ReplicaItem[]
   migrations: MigrationItem[]
   schema: FieldType[],
   schemaLoading: boolean,
   endpoints: Endpoint[],
   bottomControls: React.ReactNode,
-  loading: boolean,
 }
 @observer
 class MinionPoolMainDetails extends React.Component<Props> {
@@ -215,22 +206,18 @@ class MinionPoolMainDetails extends React.Component<Props> {
 
   renderUsage(items: TransferItem[]) {
     return items.map(item => (
-      <span>
+      <div>
         <ValueLink
           key={item.id}
           to={`/${item.type}s/${item.id}`}
         >
           {item.instances[0]}
         </ValueLink>
-        <br />
-      </span>
+      </div>
     ))
   }
 
   renderTable() {
-    if (this.props.loading) {
-      return null
-    }
     const endpoint = this.getEndpoint()
     const lastUpdated = this.renderLastExecutionTime()
 
@@ -268,7 +255,7 @@ class MinionPoolMainDetails extends React.Component<Props> {
           <Row>
             <Field>
               <Label>Pool Platform</Label>
-              {this.renderValue(this.props.item?.pool_platform || '-', true)}
+              {this.renderValue(this.props.item?.platform || '-', true)}
             </Field>
           </Row>
           <Row>
@@ -304,12 +291,6 @@ class MinionPoolMainDetails extends React.Component<Props> {
         </Column>
         <Column width="9.5%" />
         <Column width="48%" style={{ flexGrow: 1 }}>
-          <Row>
-            <Field>
-              <Label>Last Execution Status</Label>
-              <Value>{this.props.item?.last_execution_status ? <StatusPill status={this.props.item.last_execution_status} /> : '-'}</Value>
-            </Field>
-          </Row>
           {getPropertyNames().length > 0 ? (
             <Row>
               <Field>
@@ -321,36 +302,43 @@ class MinionPoolMainDetails extends React.Component<Props> {
               </Field>
             </Row>
           ) : null}
+          <Row>
+            <Field>
+              <Label>Minimum Minions</Label>
+              <Value>{this.props.item?.minimum_minions || '1'}</Value>
+            </Field>
+          </Row>
+          <Row>
+            <Field>
+              <Label>Maximum Minions</Label>
+              <Value>{this.props.item?.maximum_minions || '1'}</Value>
+            </Field>
+          </Row>
+          <Row>
+            <Field>
+              <Label>Minion Max Idle Time (s)</Label>
+              <Value>{this.props.item?.minion_max_idle_time || '-'}</Value>
+            </Field>
+          </Row>
+          <Row>
+            <Field>
+              <Label>Minion Retention Strategy</Label>
+              <Value>{this.props.item?.minion_retention_strategy || 'delete'}</Value>
+            </Field>
+          </Row>
         </Column>
       </ColumnsLayout>
     )
   }
 
   renderBottomControls() {
-    if (this.props.loading) {
-      return null
-    }
-
     return this.props.bottomControls
   }
 
-  renderLoading() {
-    if (!this.props.loading) {
-      return null
-    }
-
-    return (
-      <Loading>
-        <StatusImage loading />
-      </Loading>
-    )
-  }
-
   render() {
     return (
       <Wrapper>
         {this.renderTable()}
-        {this.renderLoading()}
         {this.renderBottomControls()}
       </Wrapper>
     )

+ 15 - 0
src/components/organisms/MinionPoolDetailsContent/images/network.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="16px" viewBox="0 0 14 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
+
+    <desc>Created with Sketch.</desc>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Migration/Details/Overview-Closed" transform="translate(-336.000000, -681.000000)" stroke="#0044CA" stroke-width="1.5">
+            <g id="Icon/Network/16-Copy-2" transform="translate(336.000000, 681.000000)">
+                <path d="M0.5,8 L14,8" id="Path-5"></path>
+                <path d="M3,1 L3,2 C3,4.209139 4.790861,6 7,6 L9.96429232,6 L14,6" id="Path-6-Copy" stroke-linejoin="round" transform="translate(8.500000, 3.500000) scale(1, -1) translate(-8.500000, -3.500000) "></path>
+                <path d="M3,10 L3,11 C3,13.209139 4.790861,15 7,15 L9.96429232,15 L14,15" id="Path-6-Copy-2" stroke-linejoin="round"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 19 - 23
src/components/organisms/MinionPoolModal/MinionPoolModal.tsx

@@ -140,7 +140,7 @@ class MinionPoolModal extends React.Component<Props, State> {
       this.setState(prevState => ({
         minionPool: {
           ...prevState.minionPool,
-          pool_platform: props.platform,
+          platform: props.platform,
         },
       }))
     }
@@ -203,40 +203,35 @@ class MinionPoolModal extends React.Component<Props, State> {
       return
     }
     this.setState({ saving: true })
-    if (this.state.minionPool?.id) {
-      await this.update()
-    } else {
-      await this.add()
+    try {
+      if (this.state.minionPool?.id) {
+        await this.update()
+      } else {
+        await this.add()
+      }
+    } catch (err) {
+      this.setState({ saving: false })
     }
   }
 
   async update() {
     const stateMinionPool = { ...this.state.minionPool }
-    await minionPoolStore.loadMinionPools()
     const minionPool = minionPoolStore.minionPools.find(e => e.id === stateMinionPool.id)
     if (!minionPool) {
       throw new Error('Minion pool not found!')
     }
-    try {
-      delete stateMinionPool.pool_platform
-      delete stateMinionPool.endpoint_id
-      await minionPoolStore.update(stateMinionPool)
-      if (this.props.onUpdateComplete) {
-        this.props.onUpdateComplete(`/minion-pools/${stateMinionPool.id}`)
-      }
-    } catch (err) {
-      this.props.onRequestClose()
+    delete stateMinionPool.platform
+    delete stateMinionPool.endpoint_id
+    await minionPoolStore.update(stateMinionPool)
+    if (this.props.onUpdateComplete) {
+      this.props.onUpdateComplete(`/minion-pools/${stateMinionPool.id}`)
     }
   }
 
   async add() {
-    try {
-      await minionPoolStore.add(this.props.endpoint.id, this.state.minionPool)
-      notificationStore.alert('Minion Pool created', 'success')
-      this.props.onRequestClose()
-    } catch (err) {
-      this.props.onRequestClose()
-    }
+    await minionPoolStore.add(this.props.endpoint.id, this.state.minionPool)
+    notificationStore.alert('Minion Pool created', 'success')
+    this.props.onRequestClose()
   }
 
   fillRequiredDefaults() {
@@ -309,6 +304,7 @@ class MinionPoolModal extends React.Component<Props, State> {
       <Content>
         <MinionPoolModalContent
           endpoint={this.props.endpoint}
+          envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
           defaultSchema={minionPoolStore.minionPoolDefaultSchema}
           envSchema={minionPoolStore.minionPoolEnvSchema}
           invalidFields={this.state.invalidFields}
@@ -338,7 +334,7 @@ class MinionPoolModal extends React.Component<Props, State> {
     return (
       <LoadingWrapper>
         <StatusImage loading />
-        <LoadingText>Loading schema ...</LoadingText>
+        <LoadingText>Loading Pool Options ...</LoadingText>
       </LoadingWrapper>
     )
   }

+ 69 - 15
src/components/organisms/MinionPoolModal/MinionPoolModalContent.tsx

@@ -24,6 +24,7 @@ import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 import EndpointLogos from '../../atoms/EndpointLogos/EndpointLogos'
 import { Endpoint } from '../../../@types/Endpoint'
+import ToggleButtonBar from '../../atoms/ToggleButtonBar/ToggleButtonBar'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -37,6 +38,9 @@ const Fields = styled.div<any>`
   flex-direction: column;
   overflow: auto;
 `
+const ToggleButtonBarStyled = styled(ToggleButtonBar)`
+  margin-top: 16px;
+`
 const FieldStyled = styled(FieldInput)`
   min-width: ${props => (props.useTextArea ? '100%' : '224px')};
   max-width: ${StyleProps.inputSizes.large.width}px;
@@ -68,6 +72,7 @@ const EndpointFieldLabelText = styled.span`
 const EndpointFieldValue = styled.div`
   display: flex;
   align-items: center;
+  height: 29px;
 `
 const EndpointFieldValueText = styled.div`
   overflow: hidden;
@@ -76,11 +81,11 @@ const EndpointFieldValueText = styled.div`
   font-size: 12px;
   color: ${Palette.grayscale[4]};
 `
-const PoolPlatformFieldText = styled.div`
+const EndpointFieldValueLabel = styled.div`
   text-transform: capitalize;
 `
 const EndpointFieldValueLogo = styled.div``
-const Group = styled.div<any>`
+const Group = styled.div`
   display: flex;
   flex-direction: column;
   flex-shrink: 0;
@@ -90,6 +95,14 @@ const GroupName = styled.div<any>`
   align-items: center;
   margin: 32px 0 24px 0;
 `
+const DisabledMessage = styled.div`
+  display: flex;
+  align-items: center;
+  width: 340px;
+  margin: 0 auto 32px auto;
+  text-align: center;
+  font-size: 13px;
+`
 const GroupNameText = styled.div<any>`
   margin: 0 32px;
   font-size: 16px;
@@ -105,6 +118,7 @@ const GroupFields = styled.div<any>`
   flex-direction: column;
 `
 type Props = {
+  envOptionsDisabled: boolean
   defaultSchema: Field[],
   envSchema: Field[],
   invalidFields: string[],
@@ -118,7 +132,28 @@ type Props = {
   onCreateClick: () => void
   onCancelClick: () => void
 }
-class MinionPoolModalContent extends React.Component<Props> {
+type State = {
+  useAdvancedOptions: boolean
+}
+class MinionPoolModalContent extends React.Component<Props, State> {
+  state = {
+    useAdvancedOptions: false,
+  }
+
+  componentDidUpdate(_: Props, prevState: State) {
+    if (prevState.useAdvancedOptions !== this.state.useAdvancedOptions) {
+      this.props.onResizeUpdate(0)
+    }
+  }
+
+  filterBySimpleAdvanced(fields: Field[]): Field[] {
+    if (this.state.useAdvancedOptions) {
+      return fields
+    }
+    const exceptions = ['endpoint_id', 'platform', 'os_type']
+    return fields.filter(f => (f.required && f.default == null) || exceptions.indexOf(f.name) > -1)
+  }
+
   renderEndpoint() {
     return (
       <EndpointField>
@@ -142,24 +177,24 @@ class MinionPoolModalContent extends React.Component<Props> {
     )
   }
 
-  renderPoolPlatform() {
+  renderReadOnlyField(field: Field) {
     return (
       <EndpointField>
         <EndpointFieldLabel>
           <EndpointFieldLabelText>
-            Pool Platform
+            {field.title}
           </EndpointFieldLabelText>
         </EndpointFieldLabel>
         <EndpointFieldValue>
-          <PoolPlatformFieldText>
-            {this.props.getFieldValue(this.props.defaultSchema.find(f => f.name === 'pool_platform'))}
-          </PoolPlatformFieldText>
+          <EndpointFieldValueLabel>
+            {this.props.getFieldValue(field)}
+          </EndpointFieldValueLabel>
         </EndpointFieldValue>
       </EndpointField>
     )
   }
 
-  renderFieldSet(customFields: Field[]) {
+  renderFieldSet(customFields: Field[], options?: {disabled?: boolean}) {
     const rows: JSX.Element[] = []
     let lastField: JSX.Element
     let i = 0
@@ -167,19 +202,19 @@ class MinionPoolModalContent extends React.Component<Props> {
       let currentField
       if (field.name === 'endpoint_id') {
         currentField = this.renderEndpoint()
-      } else if (field.name === 'pool_platform') {
-        currentField = this.renderPoolPlatform()
+      } else if (field.name === 'platform' || field.name === 'os_type') {
+        currentField = this.renderReadOnlyField(field)
       } else {
         currentField = (
           <FieldStyled
             {...field}
             label={field.title || LabelDictionary.get(field.name)}
             width={StyleProps.inputSizes.large.width}
-            disabled={this.props.disabled}
+            disabled={this.props.disabled || options?.disabled}
             highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}
             value={this.props.getFieldValue(field)}
             onChange={value => { this.props.onFieldChange(field, value) }}
-            nullableBoolean
+            nullableBoolean={field.nullableBoolean != null ? field.nullableBoolean : true}
           />
         )
       }
@@ -216,7 +251,7 @@ class MinionPoolModalContent extends React.Component<Props> {
       <Fields ref={(ref: HTMLElement) => { this.props.scrollableRef(ref) }}>
         <Group>
           <GroupFields>
-            {this.renderFieldSet(this.props.defaultSchema)}
+            {this.renderFieldSet(this.filterBySimpleAdvanced(this.props.defaultSchema))}
           </GroupFields>
         </Group>
         <Group>
@@ -225,17 +260,36 @@ class MinionPoolModalContent extends React.Component<Props> {
             <GroupNameText>Environment Options</GroupNameText>
             <GroupNameBar />
           </GroupName>
+          {this.props.envOptionsDisabled ? (
+            <DisabledMessage>
+              The environment options are disabled while the minion pool is not deallocated.
+            </DisabledMessage>
+          ) : null}
           <GroupFields>
-            {this.renderFieldSet(this.props.envSchema)}
+            {this.renderFieldSet(
+              this.filterBySimpleAdvanced(this.props.envSchema),
+              { disabled: this.props.envOptionsDisabled },
+            )}
           </GroupFields>
         </Group>
       </Fields>
     )
   }
 
+  renderSimpleAdvancedToggle() {
+    return (
+      <ToggleButtonBarStyled
+        items={[{ label: 'Simple', value: 'simple' }, { label: 'Advanced', value: 'advanced' }]}
+        selectedValue={this.state.useAdvancedOptions ? 'advanced' : 'simple'}
+        onChange={item => { this.setState({ useAdvancedOptions: item.value === 'advanced' }) }}
+      />
+    )
+  }
+
   render() {
     return (
       <Wrapper>
+        {this.renderSimpleAdvancedToggle()}
         {this.renderFields()}
       </Wrapper>
     )

+ 2 - 1
src/components/organisms/PageHeader/PageHeader.tsx

@@ -41,6 +41,7 @@ import StyleProps from '../../styleUtils/StyleProps'
 import { ProviderTypes } from '../../../@types/Providers'
 import MinionEndpointModal from '../MinionEndpointModal/MinionEndpointModal'
 import MinionPoolModal from '../MinionPoolModal'
+import ObjectUtils from '../../../utils/ObjectUtils'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -385,7 +386,7 @@ class PageHeader extends React.Component<Props, State> {
         {this.state.showMinionPoolModal ? (
           <Modal
             isOpen
-            title="New Minion Pool"
+            title={`New ${ObjectUtils.capitalizeFirstLetter(this.state.selectedMinionPoolPlatform)} Minion Pool`}
             onRequestClose={() => { this.handleCloseMinionPoolModalRequest() }}
           >
             <MinionPoolModal

+ 1 - 1
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx

@@ -204,7 +204,7 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
       label: instance.name,
       type: 'string',
       enum: minionPools.map(minionPool => ({
-        name: minionPool.pool_name,
+        name: minionPool.name,
         id: minionPool.id,
       })),
     }))

+ 21 - 18
src/components/organisms/WizardOptions/WizardOptions.tsx

@@ -34,6 +34,7 @@ import Palette from '../../styleUtils/Palette'
 
 import endpointImage from './images/endpoint.svg'
 import { MinionPool } from '../../../@types/MinionPool'
+import { MinionPoolStoreUtils } from '../../../stores/MinionPoolStore'
 
 export const INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS = 'instance_osmorphing_minion_pool_mappings'
 
@@ -195,32 +196,34 @@ class WizardOptions extends React.Component<Props> {
   getDefaultSimpleFieldsSchema() {
     let fieldsSchema: Field[] = []
 
-    if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica') {
-      fieldsSchema.push({ name: 'description', type: 'string' })
-    }
-
-    if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
-      fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'boolean', default: false })
+    if (this.props.minionPools.length) {
+      fieldsSchema.push({
+        name: 'minion_pool_id',
+        label: `${this.props.isSource ? 'Source' : 'Target'} Minion Pool`,
+        type: 'string',
+        enum: this.props.minionPools.map(minionPool => ({
+          label: minionPool.name,
+          value: minionPool.id,
+          disabled: !MinionPoolStoreUtils.isActive(minionPool),
+          subtitleLabel: !MinionPoolStoreUtils.isActive(minionPool) ? `Pool is in ${minionPool.status} status instead of being ALLOCATED.` : '',
+        })),
+      })
     }
 
     if (this.props.showSeparatePerVm) {
       const dictionaryLabel = LabelDictionary.get('separate_vm')
       const label = this.props.wizardType === 'migration' ? dictionaryLabel : dictionaryLabel.replace('Migration', 'Replica')
-      fieldsSchema.unshift({
+      fieldsSchema.push({
         name: 'separate_vm', label, type: 'boolean', default: true,
       })
     }
 
-    if (this.props.minionPools.length) {
-      fieldsSchema.push({
-        name: 'minion_pool_id',
-        label: 'Minion Pool',
-        type: 'string',
-        enum: this.props.minionPools.map(minionPool => ({
-          label: minionPool.pool_name,
-          value: minionPool.id,
-        })),
-      })
+    if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
+      fieldsSchema.push({ name: 'skip_os_morphing', type: 'boolean', default: false })
+    }
+
+    if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica') {
+      fieldsSchema.push({ name: 'description', type: 'string' })
     }
 
     if (this.props.wizardType === 'replica') {
@@ -250,7 +253,7 @@ class WizardOptions extends React.Component<Props> {
         label: instance.name,
         type: 'string',
         enum: this.props.minionPools.map(minionPool => ({
-          name: minionPool.pool_name,
+          name: minionPool.name,
           id: minionPool.id,
         })),
       }))

+ 2 - 2
src/components/organisms/WizardPageContent/WizardPageContent.tsx

@@ -427,7 +427,7 @@ class WizardPageContent extends React.Component<Props, State> {
               || this.props.providerStore.sourceOptionsPrimaryLoading
               || this.props.minionPoolStore.loadingMinionPools}
             minionPools={this.props.minionPoolStore.minionPools
-              .filter(m => m.pool_platform === 'source' && m.endpoint_id === this.props.wizardData.source?.id)}
+              .filter(m => m.platform === 'source' && m.endpoint_id === this.props.wizardData.source?.id)}
             optionsLoading={this.props.providerStore.sourceOptionsSecondaryLoading}
             optionsLoadingSkipFields={getOptionsLoadingSkipFields('source')}
             fields={this.props.providerStore.sourceSchema}
@@ -449,7 +449,7 @@ class WizardPageContent extends React.Component<Props, State> {
               || this.props.providerStore.destinationOptionsPrimaryLoading
               || this.props.minionPoolStore.loadingMinionPools}
             minionPools={this.props.minionPoolStore.minionPools
-              .filter(m => m.pool_platform === 'destination' && m.endpoint_id === this.props.wizardData.target?.id)}
+              .filter(m => m.platform === 'destination' && m.endpoint_id === this.props.wizardData.target?.id)}
             optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading}
             optionsLoadingSkipFields={[
               ...getOptionsLoadingSkipFields('destination'), 'description', 'execute_now',

+ 1 - 1
src/components/organisms/WizardSummary/WizardSummary.tsx

@@ -367,7 +367,7 @@ class WizardSummary extends React.Component<Props> {
 
     const getMinionPoolName = (id: string) => {
       const minionPool = this.props.minionPools.find(m => m.id === id)
-      return minionPool?.pool_name || id
+      return minionPool?.name || id
     }
 
     return (

+ 1 - 1
src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx

@@ -319,7 +319,7 @@ class MigrationDetailsPage extends React.Component<Props, State> {
     const dropdownActions = [
       {
         label: 'Cancel',
-        disabled: this.getStatus() !== 'RUNNING',
+        disabled: this.getStatus() !== 'RUNNING' && this.getStatus() !== 'AWAITING_MINION_ALLOCATIONS',
         hidden: this.getStatus() === 'CANCELLING',
         action: () => { this.handleCancelMigrationClick() },
       },

+ 4 - 2
src/components/pages/MigrationsPage/MigrationsPage.tsx

@@ -127,7 +127,8 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
 
   cancelSelectedMigrations() {
     this.state.selectedMigrations.forEach(migration => {
-      if (this.getStatus(migration.id) === 'RUNNING') {
+      const status = this.getStatus(migration.id)
+      if (status === 'RUNNING' || status === 'AWAITING_MINION_ALLOCATIONS') {
         migrationStore.cancel(migration.id)
       }
     })
@@ -213,7 +214,8 @@ class MigrationsPage extends React.Component<{ history: any }, State> {
   render() {
     let atLeaseOneIsRunning = false
     this.state.selectedMigrations.forEach(migration => {
-      atLeaseOneIsRunning = atLeaseOneIsRunning || this.getStatus(migration.id) === 'RUNNING'
+      const status = this.getStatus(migration.id)
+      atLeaseOneIsRunning = atLeaseOneIsRunning || status === 'RUNNING' || status === 'AWAITING_MINION_ALLOCATIONS'
     })
     const BulkActions = [
       {

+ 89 - 155
src/components/pages/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx

@@ -22,7 +22,6 @@ import DetailsContentHeader from '../../organisms/DetailsContentHeader/DetailsCo
 import Modal from '../../molecules/Modal/Modal'
 import AlertModal from '../../organisms/AlertModal/AlertModal'
 
-import type { Execution } from '../../../@types/Execution'
 import type { Action as DropdownAction } from '../../molecules/ActionDropdown/ActionDropdown'
 
 import userStore from '../../../stores/UserStore'
@@ -33,12 +32,12 @@ import configLoader from '../../../utils/Config'
 
 import minionPoolImage from './images/minion-pool.svg'
 import Palette from '../../styleUtils/Palette'
-import ObjectUtils from '../../../utils/ObjectUtils'
-import minionPoolStore, { MinionPoolAction } from '../../../stores/MinionPoolStore'
+import minionPoolStore from '../../../stores/MinionPoolStore'
 import MinionPoolModal from '../../organisms/MinionPoolModal/MinionPoolModal'
 import MinionPoolDetailsContent from '../../organisms/MinionPoolDetailsContent/MinionPoolDetailsContent'
 import replicaStore from '../../../stores/ReplicaStore'
 import migrationStore from '../../../stores/MigrationStore'
+import MinionPoolConfirmationModal from '../../molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal'
 
 const Wrapper = styled.div<any>``
 
@@ -49,10 +48,8 @@ type Props = {
 type State = {
   showEditModal: boolean,
   showDeleteMinionPoolConfirmation: boolean,
-  showCancelConfirmation: boolean
-  forceCancel: boolean,
-  confirmationExecution: Execution | null,
   pausePolling: boolean,
+  showDeallocateConfirmation: boolean
 }
 
 @observer
@@ -60,10 +57,8 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
   state: State = {
     showEditModal: false,
     showDeleteMinionPoolConfirmation: false,
-    confirmationExecution: null,
-    showCancelConfirmation: false,
     pausePolling: false,
-    forceCancel: false,
+    showDeallocateConfirmation: false,
   }
 
   stopPolling: boolean | null = null
@@ -84,7 +79,6 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
 
   componentWillUnmount() {
     this.stopPolling = true
-    minionPoolStore.clearMinionPoolDetails()
   }
 
   get minionPoolId() {
@@ -94,29 +88,40 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     return this.props.match.params.id
   }
 
+  get minionPool() {
+    return minionPoolStore.minionPoolDetails
+  }
+
   getStatus() {
-    return minionPoolStore.minionPoolDetails?.pool_status
+    return this.minionPool?.status
   }
 
   async loadMinionPool(minionPoolId?: string) {
+    const usableId = minionPoolId || this.minionPoolId
     await Promise.all([
       endpointStore.getEndpoints({ showLoading: true }),
-      minionPoolStore
-        .loadMinionPoolDetails(minionPoolId || this.minionPoolId, { showLoading: true }),
+      minionPoolStore.loadMinionPoolDetails(this.minionPoolId, { showLoading: true }),
     ])
+    const minionPool = this.minionPool
+    if (!minionPool) {
+      notificationStore.alert(`Minion pool with ID '${usableId}' was not found`, 'error')
+      return
+    }
+
     const endpoint = endpointStore.endpoints
-      .find(e => e.id === minionPoolStore.minionPoolDetails?.endpoint_id)
+      .find(e => e.id === minionPool.endpoint_id)
     if (!endpoint) {
+      notificationStore.alert('The endpoint associated to this minion pool was not found', 'error')
       return
     }
     await minionPoolStore.loadMinionPoolSchema(
       endpoint.type,
-      minionPoolStore.minionPoolDetails!.pool_platform,
+      minionPool.platform,
     )
     await minionPoolStore.loadEnvOptions(
       endpoint.id,
       endpoint.type,
-      minionPoolStore.minionPoolDetails!.pool_platform,
+      minionPool.platform,
       { useCache: true },
     )
   }
@@ -130,27 +135,14 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     }
   }
 
-  handleCancelExecutionConfirmation() {
-    if (!minionPoolStore.minionPoolDetails) {
-      return
-    }
-    minionPoolStore.cancelExecution(
-      minionPoolStore.minionPoolDetails.id,
-      this.state.forceCancel,
-      this.state.confirmationExecution?.id,
-    )
-
-    this.handleCloseCancelConfirmation()
-  }
-
   handleDeleteMinionPoolClick() {
     this.setState({ showDeleteMinionPoolConfirmation: true })
   }
 
   handleDeleteMinionPool() {
     this.setState({ showDeleteMinionPoolConfirmation: false })
-    this.props.history.push('/replicas')
-    minionPoolStore.deleteMinionPool(minionPoolStore.minionPoolDetails!.id)
+    this.props.history.push('/minion-pools')
+    minionPoolStore.deleteMinionPool(this.minionPool!.id)
   }
 
   handleCloseDeleteMinionPoolConfirmation() {
@@ -161,38 +153,15 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     this.setState({ showEditModal: true, pausePolling: true })
   }
 
-  handleCancelExecution(confirmationExecution: Execution | null, force?: boolean) {
-    this.setState({
-      showCancelConfirmation: true,
-      confirmationExecution,
-      forceCancel: force || false,
-    })
-  }
-
-  handleCloseCancelConfirmation() {
-    this.setState({
-      showCancelConfirmation: false,
-    })
-  }
-
   async pollData(showLoading: boolean) {
     if (this.state.pausePolling || this.stopPolling) {
       return
     }
 
-    await Promise.all([
-      minionPoolStore.loadMinionPoolDetails(this.minionPoolId, {
-        showLoading, skipLog: true,
-      }),
-      (async () => {
-        if (window.location.pathname.indexOf('executions') > -1) {
-          await minionPoolStore.loadExecutionTasks({
-            minionPoolId: this.minionPoolId,
-            skipLog: true,
-          })
-        }
-      })(),
-    ])
+    await minionPoolStore.loadMinionPoolDetails(this.minionPoolId, {
+      showLoading,
+      skipLog: true,
+    })
 
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
   }
@@ -208,44 +177,41 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
     this.closeEditModal()
   }
 
-  async handleExecutionChange(executionId: string) {
-    await ObjectUtils.waitFor(() => Boolean(minionPoolStore.minionPoolDetails))
-    if (!minionPoolStore.minionPoolDetails?.id) {
+  async handleAllocate() {
+    if (!this.minionPool) {
       return
     }
-    minionPoolStore.loadExecutionTasks(
-      {
-        minionPoolId: minionPoolStore.minionPoolDetails.id,
-        executionId,
-      },
-    )
+    notificationStore.alert('Allocating minion pool...')
+    await minionPoolStore.runAction(this.minionPool.id, 'allocate')
+    await minionPoolStore.loadMinionPoolDetails(this.minionPool.id)
+  }
+
+  handleDeallocate() {
+    this.setState({
+      showDeallocateConfirmation: true,
+    })
   }
 
-  async handleAction(action: MinionPoolAction) {
-    const runAction = async (message: string) => {
-      if (!minionPoolStore.minionPoolDetails) {
-        return
-      }
-      notificationStore.alert(message)
-      await minionPoolStore.runAction(minionPoolStore.minionPoolDetails.id, action)
-      await minionPoolStore.loadMinionPoolDetails(minionPoolStore.minionPoolDetails.id)
+  async handleRefresh() {
+    if (!this.minionPool) {
+      return
     }
+    notificationStore.alert('Refreshing minion pool...')
+    await minionPoolStore.runAction(this.minionPool.id, 'refresh')
+    await minionPoolStore.loadMinionPoolDetails(this.minionPool.id)
+    this.props.history.push(`/minion-pools/${this.minionPool.id}/machines`)
+  }
 
-    switch (action) {
-      case 'set-up-shared-resources':
-        runAction('Setting up shared resources...')
-        break
-      case 'tear-down-shared-resources':
-        runAction('Tearing up shared resources...')
-        break
-      case 'allocate-machines':
-        runAction('Allocating machines...')
-        break
-      case 'deallocate-machines':
-        runAction('Deallocating machines...')
-        break
-      default:
+  async handleDeallocateConfirmation(force: boolean) {
+    this.setState({
+      showDeallocateConfirmation: false,
+    })
+    if (!this.minionPool) {
+      return
     }
+    notificationStore.alert('Deallocating minion pool...')
+    await minionPoolStore.runAction(this.minionPool.id, 'deallocate', { force })
+    await minionPoolStore.loadMinionPoolDetails(this.minionPool.id)
   }
 
   renderEditMinionPool() {
@@ -253,7 +219,7 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
       return null
     }
     const endpoint = endpointStore.endpoints
-      .find(e => e.id === minionPoolStore.minionPoolDetails?.endpoint_id)
+      .find(e => e.id === this.minionPool?.endpoint_id)
     if (!endpoint) {
       return null
     }
@@ -268,8 +234,8 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
           endpoint={endpoint}
           onCancelClick={() => { this.closeEditModal() }}
           onRequestClose={() => { this.closeEditModal() }}
-          minionPool={minionPoolStore.minionPoolDetails}
-          platform={minionPoolStore.minionPoolDetails?.pool_platform || 'source'}
+          minionPool={this.minionPool}
+          platform={this.minionPool?.platform || 'source'}
           onUpdateComplete={r => { this.handleUpdateComplete(r) }}
         />
       </Modal>
@@ -277,63 +243,40 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
   }
 
   render() {
-    const uninitialized = minionPoolStore.minionPoolDetails?.pool_status === 'UNINITIALIZED'
-    const deallocated = minionPoolStore.minionPoolDetails?.pool_status === 'DEALLOCATED'
-    const allocated = minionPoolStore.minionPoolDetails?.pool_status === 'ALLOCATED'
-    const isRunning = minionPoolStore.minionPoolDetails?.pool_status?.indexOf('ING') === ((minionPoolStore.minionPoolDetails?.pool_status?.length || -100) - 3)
+    const status = this.minionPool?.status
+    const deallocated = status === 'DEALLOCATED'
+    const allocated = status === 'ALLOCATED'
+    const error = status === 'ERROR'
 
     const dropdownActions: DropdownAction[] = [
       {
         label: 'Edit',
         action: () => { this.handleMinionPoolEditClick() },
-        disabled: !uninitialized,
-        title: !uninitialized ? 'The minion pool should be uninitialized' : '',
-      },
-      {
-        label: 'Setup shared resources',
-        color: Palette.primary,
-        action: () => {
-          this.handleAction('set-up-shared-resources')
-        },
-        disabled: !uninitialized,
-        title: !uninitialized ? 'The minion pool should be uninitialized' : '',
-      },
-      {
-        label: 'Tear down shared resources',
-        action: () => {
-          this.handleAction('tear-down-shared-resources')
-        },
         disabled: !deallocated,
         title: !deallocated ? 'The minion pool should be deallocated' : '',
       },
       {
-        label: 'Allocate Machines',
+        label: 'Allocate',
         color: Palette.primary,
-        action: () => {
-          this.handleAction('allocate-machines')
-        },
+        action: () => { this.handleAllocate() },
         disabled: !deallocated,
         title: !deallocated ? 'The minion pool should be deallocated' : '',
       },
       {
-        label: 'Deallocate Machines',
+        label: 'Deallocate',
         action: () => {
-          this.handleAction('deallocate-machines')
+          this.handleDeallocate()
         },
-        disabled: !allocated,
-        title: !allocated ? 'The minion pool should be allocated' : '',
+        disabled: !allocated && !error,
+        title: !allocated && !error ? 'The minion pool should be allocated' : '',
       },
       {
-        label: 'Cancel Execution',
+        label: 'Refresh',
         action: () => {
-          this.setState({
-            showCancelConfirmation: true,
-            confirmationExecution: null,
-            forceCancel: false,
-          })
+          this.handleRefresh()
         },
-        disabled: !isRunning,
-        title: !isRunning ? 'The minion pool do not have an active execution' : '',
+        disabled: !allocated,
+        title: !allocated ? 'The minion pool should be allocated' : '',
       },
       {
         label: 'Delete Minion Pool',
@@ -341,8 +284,8 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
         action: () => {
           this.setState({ showDeleteMinionPoolConfirmation: true })
         },
-        disabled: !uninitialized,
-        title: !uninitialized ? 'The minion pool should be uninitialized' : '',
+        disabled: !deallocated && !error,
+        title: (!deallocated && !error) ? 'The minion pool should be deallocated' : '',
       },
     ]
 
@@ -357,8 +300,8 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
 )}
           contentHeaderComponent={(
             <DetailsContentHeader
-              statusPill={minionPoolStore.minionPoolDetails?.pool_status}
-              itemTitle={minionPoolStore.minionPoolDetails?.pool_name}
+              statusPill={this.minionPool?.status}
+              itemTitle={this.minionPool?.name}
               itemType="minion pool"
               dropdownActions={dropdownActions}
               largeDropdownActionItems
@@ -368,28 +311,21 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
 )}
           contentComponent={(
             <MinionPoolDetailsContent
-              item={minionPoolStore.minionPoolDetails}
+              item={this.minionPool}
               replicas={replicaStore.replicas
-                .filter(r => r.origin_minion_pool_id === minionPoolStore.minionPoolDetails?.id
-                  || r.destination_minion_pool_id === minionPoolStore.minionPoolDetails?.id)}
+                .filter(r => r.origin_minion_pool_id === this.minionPool?.id
+                  || r.destination_minion_pool_id === this.minionPool?.id)}
               migrations={migrationStore.migrations
-                .filter(r => r.origin_minion_pool_id === minionPoolStore.minionPoolDetails?.id
-                  || r.destination_minion_pool_id === minionPoolStore.minionPoolDetails?.id)}
+                .filter(r => r.origin_minion_pool_id === this.minionPool?.id
+                  || r.destination_minion_pool_id === this.minionPool?.id)}
               endpoints={endpointStore.endpoints}
-              detailsLoading={minionPoolStore.loadingMinionPoolDetails || endpointStore.loading}
               schema={minionPoolStore.minionPoolCombinedSchema}
               schemaLoading={minionPoolStore.loadingMinionPoolSchema
                 || minionPoolStore.loadingEnvOptions}
-              executionsLoading={minionPoolStore.loadingMinionPoolDetails}
-              onExecutionChange={id => { this.handleExecutionChange(id) }}
-              executions={minionPoolStore.minionPoolDetails?.executions || []}
-              executionsTasksLoading={minionPoolStore.loadingMinionPoolDetails
-                || minionPoolStore.loadingExecutionsTasks}
-              executionsTasks={minionPoolStore.executionsTasks}
               page={this.props.match.params.page || ''}
-              onCancelExecutionClick={(e, f) => { this.handleCancelExecution(e, f) }}
+              loading={minionPoolStore.loadingMinionPoolDetails}
               onDeleteMinionPoolClick={() => { this.handleDeleteMinionPoolClick() }}
-              onRunAction={a => { this.handleAction(a) }}
+              onAllocate={() => { this.handleAllocate() }}
             />
           )}
         />
@@ -403,14 +339,12 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
             onRequestClose={() => { this.setState({ showDeleteMinionPoolConfirmation: false }) }}
           />
         ) : null}
-        <AlertModal
-          isOpen={this.state.showCancelConfirmation}
-          title="Cancel Execution?"
-          message="Are you sure you want to cancel the current execution?"
-          extraMessage=" "
-          onConfirmation={() => { this.handleCancelExecutionConfirmation() }}
-          onRequestClose={() => { this.handleCloseCancelConfirmation() }}
-        />
+        {this.state.showDeallocateConfirmation ? (
+          <MinionPoolConfirmationModal
+            onCancelClick={() => { this.setState({ showDeallocateConfirmation: false }) }}
+            onExecuteClick={force => { this.handleDeallocateConfirmation(force) }}
+          />
+        ) : null}
         {this.renderEditMinionPool()}
       </Wrapper>
     )

+ 84 - 101
src/components/pages/MinionPoolsPage/MinionPoolsPage.tsx

@@ -34,14 +34,16 @@ import { MinionPool } from '../../../@types/MinionPool'
 import emptyListImage from './images/minion-pool-empty-list.svg'
 import providerStore from '../../../stores/ProviderStore'
 import endpointStore from '../../../stores/EndpointStore'
-import minionPoolStore, { MinionPoolAction } from '../../../stores/MinionPoolStore'
+import minionPoolStore from '../../../stores/MinionPoolStore'
 import { Endpoint } from '../../../@types/Endpoint'
 import MinionEndpointModal from '../../organisms/MinionEndpointModal/MinionEndpointModal'
 import MinionPoolModal from '../../organisms/MinionPoolModal/MinionPoolModal'
 import MinionPoolListItem from '../../molecules/MinionPoolListItem/MinionPoolListItem'
 import Palette from '../../styleUtils/Palette'
-import notificationStore from '../../../stores/NotificationStore'
 import AlertModal from '../../organisms/AlertModal/AlertModal'
+import MinionPoolConfirmationModal from '../../molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal'
+import notificationStore from '../../../stores/NotificationStore'
+import ObjectUtils from '../../../utils/ObjectUtils'
 
 const Wrapper = styled.div<any>``
 
@@ -51,8 +53,8 @@ type State = {
   showChooseMinionEndpointModal: boolean,
   showMinionPoolModal: boolean,
   selectedMinionPoolEndpoint: Endpoint | null
-  showCancelExecutionModal: boolean,
   showDeletePoolsModal: boolean,
+  showDeallocateConfirmation: boolean,
   selectedMinionPoolPlatform: 'source' | 'destination'
 }
 
@@ -64,8 +66,8 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
     showChooseMinionEndpointModal: false,
     selectedMinionPoolEndpoint: null,
     showMinionPoolModal: false,
-    showCancelExecutionModal: false,
     showDeletePoolsModal: false,
+    showDeallocateConfirmation: false,
     selectedMinionPoolPlatform: 'source',
   }
 
@@ -90,14 +92,28 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
   getFilterItems() {
     return [
       { label: 'All', value: 'all' },
-      { label: 'Allocating', value: 'ALLOCATING' },
       { label: 'Allocated', value: 'ALLOCATED' },
-      { label: 'Initializing', value: 'INITIALIZING' },
-      { label: 'Initialized', value: 'INITIALIZED' },
+      { label: 'Allocating', value: 'ALLOCATING_MACHINES' },
+      { label: 'Deallocated', value: 'DEALLOCATED' },
+      { label: 'Deallocating', value: 'DEALLOCATING_MACHINES' },
       { label: 'Error', value: 'ERROR' },
     ]
   }
 
+  getMinionsThatCanBe(action: 'allocated' | 'deallocated' | 'refreshed' | 'deleted'): MinionPool[] {
+    const minions = this.state.selectedMinionPools
+    switch (action) {
+      case 'allocated':
+        return minions.filter(minion => minion.status === 'DEALLOCATED')
+      case 'deallocated':
+        return minions.filter(minion => minion.status === 'ALLOCATED' || minion.status === 'ERROR')
+      case 'refreshed':
+        return minions.filter(minion => minion.status === 'ALLOCATED')
+      default:
+        return minions.filter(minion => minion.status === 'DEALLOCATED' || minion.status === 'ERROR')
+    }
+  }
+
   getEndpoint(endpointId: string) {
     return endpointStore.endpoints.find(endpoint => endpoint.id === endpointId)
   }
@@ -118,14 +134,14 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
 
   searchText(item: MinionPool, text?: string | null) {
     const result = false
-    if (item.pool_name.toLowerCase().indexOf(text || '') > -1) {
+    if (item.name.toLowerCase().indexOf(text || '') > -1) {
       return true
     }
     return result
   }
 
   itemFilterFunction(item: MinionPool, filterStatus?: string | null, filterText?: string) {
-    if ((filterStatus !== 'all' && item.pool_status !== filterStatus)
+    if ((filterStatus !== 'all' && item.status !== filterStatus)
       || !this.searchText(item, filterText)
     ) {
       return false
@@ -134,15 +150,9 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
     return true
   }
 
-  cancelExecutions() {
-    this.state.selectedMinionPools.forEach(pool => {
-      minionPoolStore.cancelExecution(pool.id)
-    })
-    this.setState({ showCancelExecutionModal: false })
-  }
-
   deleteSelectedMinionPools() {
-    this.state.selectedMinionPools.forEach(pool => {
+    const pools = this.getMinionsThatCanBe('deleted')
+    pools.forEach(pool => {
       minionPoolStore.deleteMinionPool(pool.id)
     })
     this.setState({ showDeletePoolsModal: false })
@@ -210,92 +220,69 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
     })
   }
 
-  async handleAction(action: MinionPoolAction) {
-    const runAction = async (message: string) => {
-      notificationStore.alert(message)
-      await Promise.all(this.state.selectedMinionPools
-        .map(minionPool => minionPoolStore.runAction(minionPool.id, action)))
-      await minionPoolStore.loadMinionPools()
-    }
+  async handleAllocate() {
+    const pools = this.getMinionsThatCanBe('allocated')
 
-    switch (action) {
-      case 'set-up-shared-resources':
-        runAction('Setting up shared resources...')
-        break
-      case 'tear-down-shared-resources':
-        runAction('Tearing up shared resources...')
-        break
-      case 'allocate-machines':
-        runAction('Allocating machines...')
-        break
-      case 'deallocate-machines':
-        runAction('Deallocating machines...')
-        break
-      default:
-    }
+    const plural = pools.length === 1 ? '' : 's'
+    notificationStore.alert(`Allocating minion pool${plural}...`)
+    await Promise.all(pools.map(minionPool => minionPoolStore.runAction(minionPool.id, 'allocate')))
+    await minionPoolStore.loadMinionPools()
+  }
+
+  handleDeallocate() {
+    this.setState({
+      showDeallocateConfirmation: true,
+    })
+  }
+
+  async handleDeallocateConfirmation(force: boolean) {
+    this.setState({
+      showDeallocateConfirmation: false,
+    })
+    const pools = this.getMinionsThatCanBe('deallocated')
+    const plural = pools.length === 1 ? '' : 's'
+    notificationStore.alert(`Deallocating minion pool${plural}...`)
+    await Promise.all(pools.map(minionPool => minionPoolStore.runAction(minionPool.id, 'deallocate', { force })))
+    await minionPoolStore.loadMinionPools()
+  }
+
+  async handleRefreshAction() {
+    const pools = this.getMinionsThatCanBe('refreshed')
+    const plural = pools.length === 1 ? '' : 's'
+    notificationStore.alert(`Refreshing minion pool${plural}...`)
+    await Promise.all(pools.map(minionPool => minionPoolStore.runAction(minionPool.id, 'refresh')))
+    await minionPoolStore.loadMinionPools()
   }
 
   render() {
-    const allPoolsAreStatus = () => {
-      const statuses: any = {}
-      this.state.selectedMinionPools.forEach(pool => {
-        if (!statuses[pool.pool_status]) {
-          statuses[pool.pool_status] = 0
-        }
-        statuses[pool.pool_status] += 1
-      })
-      if (Object.keys(statuses).length === 1) {
-        return Object.keys(statuses)[0]
-      }
-      return null
-    }
-    const allPoolStatusResult = allPoolsAreStatus()
-    const uninitialized = allPoolStatusResult === 'UNINITIALIZED'
-    const deallocated = allPoolStatusResult === 'DEALLOCATED'
-    const allocated = allPoolStatusResult === 'ALLOCATED'
-    const isRunning = allPoolStatusResult?.indexOf('ING') === ((allPoolStatusResult?.length || -100) - 3)
+    const canBeAllocated = this.getMinionsThatCanBe('allocated').length > 0
+    const canBeDeallocated = this.getMinionsThatCanBe('deallocated').length > 0
+    const canBeRefreshed = this.getMinionsThatCanBe('refreshed').length > 0
+    const canBeDeleted = this.getMinionsThatCanBe('deleted').length > 0
+
     const BulkActions: DropdownAction[] = [
       {
-        label: 'Setup shared resources',
+        label: 'Allocate',
         color: Palette.primary,
-        action: () => {
-          this.handleAction('set-up-shared-resources')
-        },
-        disabled: !uninitialized,
-        title: !uninitialized ? 'The minion pools should be uninitialized' : '',
+        action: () => { this.handleAllocate() },
+        disabled: !canBeAllocated,
+        title: !canBeAllocated ? 'The minion pool should be deallocated' : '',
       },
       {
-        label: 'Tear down shared resources',
-        action: () => {
-          this.handleAction('tear-down-shared-resources')
-        },
-        disabled: !deallocated,
-        title: !deallocated ? 'The minion pools should be deallocated' : '',
-      },
-      {
-        label: 'Allocate Machines',
-        color: Palette.primary,
+        label: 'Deallocate',
         action: () => {
-          this.handleAction('allocate-machines')
+          this.handleDeallocate()
         },
-        disabled: !deallocated,
-        title: !deallocated ? 'The minion pools should be deallocated' : '',
+        disabled: !canBeDeallocated,
+        title: !canBeDeallocated ? 'The minion pool should be allocated' : '',
       },
       {
-        label: 'Deallocate Machines',
+        label: 'Refresh',
         action: () => {
-          this.handleAction('deallocate-machines')
+          this.handleRefreshAction()
         },
-        disabled: !allocated,
-        title: !allocated ? 'The minion pools should be allocated' : '',
-      },
-      {
-        label: 'Cancel Execution',
-        action: () => {
-          this.setState({ showCancelExecutionModal: true })
-        },
-        disabled: !isRunning,
-        title: !isRunning ? 'The minion pools do not have an active execution' : '',
+        disabled: !canBeRefreshed,
+        title: !canBeRefreshed ? 'The minion pool should be allocated' : '',
       },
       {
         label: 'Delete Minion Pools',
@@ -303,8 +290,8 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
         action: () => {
           this.setState({ showDeletePoolsModal: true })
         },
-        disabled: !uninitialized,
-        title: !uninitialized ? 'The minion pools should be uninitialized' : '',
+        disabled: !canBeDeleted,
+        title: !canBeDeleted ? 'The minion pool should be deallocated' : '',
       },
     ]
 
@@ -371,7 +358,7 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
         {this.state.showMinionPoolModal ? (
           <Modal
             isOpen
-            title="New Minion Pool"
+            title={`New ${ObjectUtils.capitalizeFirstLetter(this.state.selectedMinionPoolPlatform)} Minion Pool`}
             onRequestClose={() => { this.handleCloseMinionPoolModalRequest() }}
           >
             <MinionPoolModal
@@ -383,16 +370,6 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
             />
           </Modal>
         ) : null}
-        {this.state.showCancelExecutionModal ? (
-          <AlertModal
-            isOpen
-            title="Cancel Executions?"
-            message="Are you sure you want to cancel the selected Minion Pools executions?"
-            extraMessage=" "
-            onConfirmation={() => { this.cancelExecutions() }}
-            onRequestClose={() => { this.setState({ showCancelExecutionModal: false }) }}
-          />
-        ) : null}
         {this.state.showDeletePoolsModal ? (
           <AlertModal
             isOpen
@@ -403,6 +380,12 @@ class MinionPoolsPage extends React.Component<RouteComponentProps, State> {
             onRequestClose={() => { this.setState({ showDeletePoolsModal: false }) }}
           />
         ) : null}
+        {this.state.showDeallocateConfirmation ? (
+          <MinionPoolConfirmationModal
+            onCancelClick={() => { this.setState({ showDeallocateConfirmation: false }) }}
+            onExecuteClick={force => { this.handleDeallocateConfirmation(force) }}
+          />
+        ) : null}
       </Wrapper>
     )
   }

+ 4 - 3
src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx

@@ -232,8 +232,9 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
     const originEndpoint = endpointStore.endpoints.find(e => e.id === replica.origin_endpoint_id)
     const targetEndpoint = endpointStore.endpoints
       .find(e => e.id === replica.destination_endpoint_id)
+    const status = this.getStatus()
 
-    return Boolean(!originEndpoint || !targetEndpoint || this.getStatus() === 'RUNNING' || this.getStatus() === 'CANCELLING')
+    return Boolean(!originEndpoint || !targetEndpoint || status === 'RUNNING' || status === 'CANCELLING' || status === 'AWAITING_MINION_ALLOCATIONS')
   }
 
   handleUserItemClick(item: { value: string }) {
@@ -516,7 +517,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
       },
       {
         label: 'Cancel',
-        hidden: this.getStatus() !== 'RUNNING',
+        hidden: this.getStatus() !== 'RUNNING' && this.getStatus() !== 'AWAITING_MINION_ALLOCATIONS',
         action: () => { this.handleCancelLastExecutionClick() },
       },
       {
@@ -639,7 +640,7 @@ class ReplicaDetailsPage extends React.Component<Props, State> {
           >
             <ReplicaMigrationOptions
               transferItem={this.replica}
-              minionPools={minionPoolStore.minionPools.filter(m => m.endpoint_id === this.replica?.destination_endpoint_id && m.pool_platform === 'destination')}
+              minionPools={minionPoolStore.minionPools.filter(m => m.endpoint_id === this.replica?.destination_endpoint_id && m.platform === 'destination')}
               loadingInstances={instanceStore.loadingInstancesDetails}
               instances={instanceStore.instancesDetails}
               onCancelClick={() => { this.handleCloseMigrationModal() }}

+ 5 - 3
src/components/pages/ReplicasPage/ReplicasPage.tsx

@@ -184,7 +184,7 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
   cancelExecutions() {
     this.state.selectedReplicas.forEach(replica => {
       const actualReplica = replicaStore.replicas.find(r => r.id === replica.id)
-      if (actualReplica?.last_execution_status === 'RUNNING') {
+      if (actualReplica?.last_execution_status === 'RUNNING' || actualReplica?.last_execution_status === 'AWAITING_MINION_ALLOCATIONS') {
         replicaStore.cancelExecution({ replicaId: replica.id })
       }
     })
@@ -200,7 +200,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
       .find(e => e.id === usableReplica.origin_endpoint_id)
     const targetEndpoint = endpointStore.endpoints
       .find(e => e.id === usableReplica.destination_endpoint_id)
-    return Boolean(originEndpoint && targetEndpoint && this.getStatus(usableReplica) !== 'RUNNING')
+    const status = this.getStatus(usableReplica)
+    return Boolean(originEndpoint && targetEndpoint && status !== 'RUNNING' && status !== 'AWAITING_MINION_ALLOCATIONS')
   }
 
   deleteSelectedReplicas() {
@@ -303,7 +304,8 @@ class ReplicasPage extends React.Component<{ history: any }, State> {
       const storeReplica = replicaStore.replicas.find(r => r.id === replica.id)
       atLeastOneHasExecuteEnabled = atLeastOneHasExecuteEnabled
         || this.isExecuteEnabled(storeReplica)
-      atLeaseOneIsRunning = atLeaseOneIsRunning || this.getStatus(storeReplica) === 'RUNNING'
+      const status = this.getStatus(storeReplica)
+      atLeaseOneIsRunning = atLeaseOneIsRunning || status === 'RUNNING' || status === 'AWAITING_MINION_ALLOCATIONS'
     })
 
     const BulkActions: DropdownAction[] = [{

+ 1 - 1
src/plugins/endpoint/default/OptionsSchemaPlugin.ts

@@ -81,7 +81,7 @@ export const defaultGetDestinationEnv = (
   oldOptions?: { [prop: string]: any } | null,
 ): any => {
   const env: any = {}
-  const specialOptions = ['execute_now', 'execute_now_options', 'separate_vm', 'skip_os_morphing', 'description']
+  const specialOptions = ['execute_now', 'execute_now_options', 'separate_vm', 'skip_os_morphing', 'description', 'minion_pool_id']
     .concat(migrationFields.map(f => f.name))
     .concat(executionOptions.map(o => o.name))
     .concat(migrationImageOsTypes)

+ 4 - 4
src/sources/MigrationSource.ts

@@ -186,11 +186,11 @@ class MigrationSource {
     }
     const updatedSourceEnv = opts.updatedSourceEnv
       ? sourceParser.getDestinationEnv(opts.updatedSourceEnv) : {}
-    const sourceMinionPoolId = updatedSourceEnv.minion_pool_id || migration.origin_minion_pool_id
+    const sourceMinionPoolId = opts?.updatedSourceEnv?.minion_pool_id
+      || migration.origin_minion_pool_id
     if (sourceMinionPoolId) {
       payload.migration.origin_minion_pool_id = sourceMinionPoolId
     }
-    delete updatedSourceEnv.minion_pool_id
     payload.migration.source_environment = {
       ...sourceEnv,
       ...updatedSourceEnv,
@@ -201,11 +201,11 @@ class MigrationSource {
     }
     const updatedDestEnv = opts.updatedDestEnv
       ? sourceParser.getDestinationEnv(opts.updatedDestEnv) : {}
-    const destMinionPoolId = updatedDestEnv.minion_pool_id || migration.destination_minion_pool_id
+    const destMinionPoolId = opts?.updatedDestEnv?.minion_pool_id
+      || migration.destination_minion_pool_id
     if (destMinionPoolId) {
       payload.migration.destination_minion_pool_id = destMinionPoolId
     }
-    delete updatedDestEnv.minion_pool_id
 
     const updatedDestEnvMappings = updatedDestEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {}
     const oldMappings = migration[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS] || {}

+ 71 - 81
src/sources/MinionPoolSource.ts

@@ -12,8 +12,6 @@ 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 moment from 'moment'
-
 import Api from '../utils/ApiCaller'
 
 import configLoader from '../utils/Config'
@@ -24,9 +22,8 @@ import { providerTypes } from '../constants'
 import { SchemaParser } from './Schemas'
 import { OptionValues } from '../@types/Endpoint'
 import { MinionPoolAction } from '../stores/MinionPoolStore'
-import { Execution, ExecutionTasks } from '../@types/Execution'
-import { sortTasks } from './ReplicaSource'
-import { ProgressUpdate } from '../@types/Task'
+import { Execution } from '../@types/Execution'
+import DomUtils from '../utils/DomUtils'
 
 const transformFieldsToPayload = (schema: Field[], data: any) => {
   const payload: any = {}
@@ -51,34 +48,60 @@ class MinionPoolSource {
         type: 'string',
       },
       {
-        name: 'pool_platform',
+        name: 'platform',
         type: 'string',
+        title: 'Pool Platform',
       },
       {
-        name: 'pool_name',
+        name: 'name',
         type: 'string',
         required: true,
+        title: 'Pool Name',
       },
       {
-        name: 'pool_os_type',
+        name: 'os_type',
         type: 'string',
         required: true,
+        title: 'Pool OS Type',
+        default: 'linux',
+      },
+      {
+        name: 'minimum_minions',
+        type: 'integer',
+        minimum: 1,
+        default: 1,
+      },
+      {
+        name: 'maximum_minions',
+        type: 'integer',
+        minimum: 1,
+        default: 1,
+      },
+      {
+        name: 'minion_max_idle_time',
+        type: 'integer',
+        minimum: 0,
+        default: 3600,
+      },
+      {
+        name: 'minion_retention_strategy',
+        type: 'string',
+        default: 'delete',
         enum: [
           {
-            value: 'linux',
-            label: 'Linux',
+            value: 'delete',
+            label: 'Delete',
           },
           {
-            value: 'windows',
-            label: 'Windows',
+            value: 'poweroff',
+            label: 'Power Off',
           },
         ],
       },
       {
-        name: 'minimum_minions',
-        type: 'integer',
-        minimum: 1,
-        default: 1,
+        name: 'skip_allocation',
+        type: 'boolean',
+        nullableBoolean: false,
       },
       {
         name: 'notes',
@@ -92,12 +115,27 @@ class MinionPoolSource {
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools`,
       skipLog: options?.skipLog,
     })
-    return response.data.minion_pools
+    const minionPools: MinionPool[] = response.data.minion_pools
+    minionPools.sort((a, b) => new Date(b.updated_at || b.created_at || '').getTime()
+      - new Date(a.updated_at || a.created_at || '').getTime())
+    return minionPools
+  }
+
+  async loadMinionPoolDetails(
+    id: string,
+    options?: { skipLog?: boolean },
+  ): Promise<MinionPoolDetails> {
+    const response = await Api.send({
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${id}`,
+      skipLog: options?.skipLog,
+    })
+    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 })
     const response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/endpoints/${endpointId}/${platform}-minion-pool-options`,
+      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/endpoints/${endpointId}/${platform}-minion-pool-options?env=${env}`,
       cache: useCache,
     })
     return response.data[`${platform}_minion_pool_options`]
@@ -115,6 +153,8 @@ class MinionPoolSource {
       if (schema) {
         fields = SchemaParser.optionsSchemaToFields(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)
@@ -129,6 +169,7 @@ class MinionPoolSource {
         endpoint_id: endpointId,
         environment_options: {
           ...transformFieldsToPayload(envSchema, data),
+          list_all_destination_networks: true,
         },
       },
     }
@@ -146,6 +187,7 @@ class MinionPoolSource {
         ...transformFieldsToPayload(defaultSchema, data),
         environment_options: {
           ...transformFieldsToPayload(envSchema, data),
+          list_all_destination_networks: true,
         },
       },
     }
@@ -157,9 +199,18 @@ class MinionPoolSource {
     return response.data.minion_pool
   }
 
-  async runAction(minionPoolId: string, minionPoolAction: MinionPoolAction): Promise<Execution> {
+  async runAction(
+    minionPoolId: string,
+    minionPoolAction: MinionPoolAction,
+    actionOptions?: any,
+  ): Promise<Execution> {
     const payload: any = {}
-    payload[minionPoolAction] = null
+
+    if (actionOptions) {
+      payload[minionPoolAction] = { ...actionOptions }
+    } else {
+      payload[minionPoolAction] = null
+    }
 
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${minionPoolId}/actions`,
@@ -169,41 +220,6 @@ class MinionPoolSource {
     return response.data.execution
   }
 
-  async getMinionPoolDetails(
-    minionPoolId: string,
-    options?: { skipLog?: boolean },
-  ): Promise<MinionPoolDetails> {
-    const response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${minionPoolId}`,
-      skipLog: options?.skipLog,
-    })
-    const minionPool: MinionPoolDetails = response.data.minion_pool
-    minionPool.executions.sort((a, b) => a.number - b.number)
-    return minionPool
-  }
-
-  async cancelExecution(minionPoolId: string, force?: boolean, executionId?: string) {
-    let usableExecutionId = executionId
-    if (!usableExecutionId) {
-      const details = await this.getMinionPoolDetails(minionPoolId)
-      const lastExecution = details.executions[details.executions.length - 1]
-
-      if (!lastExecution) {
-        return null
-      }
-      usableExecutionId = lastExecution.id
-    }
-
-    const payload: any = { cancel: { force: force || false } }
-
-    await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${minionPoolId}/executions/${usableExecutionId}/actions`,
-      method: 'POST',
-      data: payload,
-    })
-    return null
-  }
-
   async deleteMinionPool(minionPoolId: string) {
     const response = await Api.send({
       url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${minionPoolId}`,
@@ -211,32 +227,6 @@ class MinionPoolSource {
     })
     return response.data.execution
   }
-
-  async getExecutionTasks(options: {
-    minionPoolId: string,
-    executionId?: string,
-    skipLog?: boolean,
-  }): Promise<ExecutionTasks> {
-    const {
-      minionPoolId, executionId, skipLog,
-    } = options
-
-    const response = await Api.send({
-      url: `${configLoader.config.servicesUrls.coriolis}/${Api.projectId}/minion_pools/${minionPoolId}/executions/${executionId}`,
-      skipLog,
-      quietError: true,
-    })
-    const execution: ExecutionTasks = response.data.execution
-    const sortTaskUpdates = (updates: ProgressUpdate[]) => {
-      if (!updates) {
-        return
-      }
-      updates.sort((a, b) => moment(a.created_at)
-        .toDate().getTime() - moment(b.created_at).toDate().getTime())
-    }
-    sortTasks(execution.tasks, sortTaskUpdates)
-    return execution
-  }
 }
 
 export default new MinionPoolSource()

+ 4 - 6
src/sources/ReplicaSource.ts

@@ -229,9 +229,8 @@ class ReplicaSource {
     }
     if (Object.keys(updateData.source).length > 0) {
       const sourceEnv = parser.getDestinationEnv(updateData.source, replica.source_environment)
-      if (sourceEnv.minion_pool_id) {
-        payload.replica.origin_minion_pool_id = sourceEnv.minion_pool_id
-        delete sourceEnv.minion_pool_id
+      if (updateData.source.minion_pool_id !== undefined) {
+        payload.replica.origin_minion_pool_id = updateData.source.minion_pool_id
       }
       if (Object.keys(sourceEnv).length) {
         payload.replica.source_environment = sourceEnv
@@ -250,9 +249,8 @@ class ReplicaSource {
       }
       delete destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
 
-      if (destEnv.minion_pool_id) {
-        payload.replica.destination_minion_pool_id = destEnv.minion_pool_id
-        delete destEnv.minion_pool_id
+      if (updateData.destination.minion_pool_id !== undefined) {
+        payload.replica.destination_minion_pool_id = updateData.destination.minion_pool_id
       }
       if (Object.keys(destEnv).length) {
         payload.replica.destination_environment = destEnv

+ 15 - 72
src/stores/MinionPoolStore.ts

@@ -20,9 +20,12 @@ import MinionPoolSource from '../sources/MinionPoolSource'
 import { Field } from '../@types/Field'
 import { ProviderTypes } from '../@types/Providers'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
-import { ExecutionTasks } from '../@types/Execution'
 
-export type MinionPoolAction = 'set-up-shared-resources' | 'allocate-machines' | 'deallocate-machines' | 'tear-down-shared-resources'
+export type MinionPoolAction = 'allocate' | 'deallocate' | 'refresh'
+
+export const MinionPoolStoreUtils = {
+  isActive: (minionPool: MinionPool) => minionPool.status === 'ALLOCATED' || minionPool.status === 'SCALING' || minionPool.status === 'RESCALING',
+}
 
 class MinionPoolStore {
   @observable
@@ -32,29 +35,23 @@ class MinionPoolStore {
   minionPools: MinionPool[] = []
 
   @observable
-  loadingMinionPoolSchema: boolean = false
+  loadingMinionPoolDetails: boolean = false
 
   @observable
-  minionPoolDefaultSchema: Field[] = []
+  minionPoolDetails: MinionPoolDetails | null = null
 
   @observable
-  minionPoolEnvSchema: Field[] = []
+  loadingMinionPoolSchema: boolean = false
 
   @observable
-  loadingMinionPoolDetails: boolean = false
+  minionPoolDefaultSchema: Field[] = []
 
   @observable
-  minionPoolDetails: MinionPoolDetails | null = null
+  minionPoolEnvSchema: Field[] = []
 
   @observable
   loadingEnvOptions: boolean = false
 
-  @observable
-  executionsTasks: ExecutionTasks[] = []
-
-  @observable
-  loadingExecutionsTasks: boolean = false
-
   @computed
   get minionPoolCombinedSchema() {
     return this.minionPoolDefaultSchema.concat(this.minionPoolEnvSchema)
@@ -84,14 +81,15 @@ class MinionPoolStore {
     if (options?.showLoading) {
       this.loadingMinionPoolDetails = true
     }
+
     try {
-      const minionPoolDetails = await MinionPoolSource.getMinionPoolDetails(
+      const minionPool = await MinionPoolSource.loadMinionPoolDetails(
         id,
         { skipLog: options?.skipLog },
       )
 
       runInAction(() => {
-        this.minionPoolDetails = minionPoolDetails
+        this.minionPoolDetails = minionPool
       })
     } finally {
       runInAction(() => {
@@ -100,11 +98,6 @@ class MinionPoolStore {
     }
   }
 
-  @action
-  clearMinionPoolDetails() {
-    this.minionPoolDetails = null
-  }
-
   @action
   async loadMinionPoolSchema(provider: ProviderTypes, platform: 'source' | 'destination') {
     this.loadingMinionPoolSchema = true
@@ -169,63 +162,13 @@ class MinionPoolStore {
   }
 
   @action
-  async runAction(minionPoolId: string, minionPoolAction: MinionPoolAction) {
-    return MinionPoolSource.runAction(minionPoolId, minionPoolAction)
-  }
-
-  @action
-  async cancelExecution(minionPoolId: string, force?: boolean, executionId?: string) {
-    return MinionPoolSource.cancelExecution(minionPoolId, force, executionId)
+  async runAction(minionPoolId: string, minionPoolAction: MinionPoolAction, actionOptions?: any) {
+    return MinionPoolSource.runAction(minionPoolId, minionPoolAction, actionOptions)
   }
 
   async deleteMinionPool(minionPoolId: string) {
     return MinionPoolSource.deleteMinionPool(minionPoolId)
   }
-
-  private currentlyLoadingExecution: string = ''
-
-  @action
-  async loadExecutionTasks(
-    options: {
-      minionPoolId: string,
-      executionId?: string,
-      skipLog?: boolean,
-    },
-  ) {
-    const {
-      minionPoolId, executionId, skipLog,
-    } = options
-
-    if (!skipLog && this.currentlyLoadingExecution === executionId) {
-      return
-    }
-    this.currentlyLoadingExecution = skipLog ? this.currentlyLoadingExecution : executionId || ''
-    if (!this.currentlyLoadingExecution) {
-      return
-    }
-
-    if (!this.executionsTasks.find(e => e.id === this.currentlyLoadingExecution)) {
-      this.loadingExecutionsTasks = true
-    }
-
-    try {
-      const executionTasks = await MinionPoolSource.getExecutionTasks({
-        minionPoolId, executionId: this.currentlyLoadingExecution, skipLog,
-      })
-      runInAction(() => {
-        this.executionsTasks = [
-          ...this.executionsTasks.filter(e => e.id !== this.currentlyLoadingExecution),
-          executionTasks,
-        ]
-      })
-    } catch (err) {
-      console.error(err)
-    } finally {
-      runInAction(() => {
-        this.loadingExecutionsTasks = false
-      })
-    }
-  }
 }
 
 export default new MinionPoolStore()

+ 4 - 0
src/utils/ObjectUtils.ts

@@ -97,6 +97,10 @@ class ObjectUtils {
       || fieldName.toLowerCase().indexOf('password') > -1
     return typeof value === 'string' && !isPassword ? value.trim() : value
   }
+
+  static capitalizeFirstLetter(value: string): string {
+    return value.charAt(0).toUpperCase() + value.slice(1)
+  }
 }
 
 export default ObjectUtils