Преглед изворни кода

Merge pull request #596 from smiclea/minion-pools

Implement Minion Pool feature redesign CORWEB-247
Nashwan Azhari пре 5 година
родитељ
комит
3bd66af630
57 измењених фајлова са 1828 додато и 913 уклоњено
  1. 1 1
      README.md
  2. 2 0
      config.ts
  3. 1 0
      src/@types/Config.ts
  4. 8 1
      src/@types/Field.ts
  5. 35 8
      src/@types/MinionPool.ts
  6. 8 2
      src/components/atoms/StatusIcon/StatusIcon.tsx
  7. 82 0
      src/components/atoms/StatusIcon/images/warning-hollow.svg
  8. 5 0
      src/components/atoms/StatusIcon/story.tsx
  9. 47 8
      src/components/atoms/StatusPill/StatusPill.tsx
  10. 4 0
      src/components/atoms/StatusPill/story.tsx
  11. 20 0
      src/components/atoms/Switch/Switch.tsx
  12. 44 5
      src/components/molecules/Dropdown/Dropdown.tsx
  13. 16 0
      src/components/molecules/Dropdown/story.tsx
  14. 5 1
      src/components/molecules/FieldInput/FieldInput.tsx
  15. 13 4
      src/components/molecules/MainDetailsTable/MainDetailsTable.tsx
  16. 110 0
      src/components/molecules/MinionPoolConfirmationModal/MinionPoolConfirmationModal.tsx
  17. 6 0
      src/components/molecules/MinionPoolConfirmationModal/package.json
  18. 11 8
      src/components/molecules/MinionPoolListItem/MinionPoolListItem.tsx
  19. 19 56
      src/components/molecules/MinionPoolListItem/images/minion-pool-list-item.svg
  20. 1 1
      src/components/molecules/NewItemDropdown/NewItemDropdown.tsx
  21. 63 0
      src/components/molecules/NewItemDropdown/images/minion-pool.svg
  22. 0 70
      src/components/molecules/NewItemDropdown/images/minionPool.svg
  23. 1 1
      src/components/organisms/EditReplica/EditReplica.tsx
  24. 1 1
      src/components/organisms/Executions/Executions.tsx
  25. 31 2
      src/components/organisms/MainDetails/MainDetails.tsx
  26. 5 2
      src/components/organisms/MinionEndpointModal/MinionEndpointModal.tsx
  27. 60 49
      src/components/organisms/MinionPoolDetailsContent/MinionPoolDetailsContent.tsx
  28. 338 0
      src/components/organisms/MinionPoolDetailsContent/MinionPoolEvents.tsx
  29. 312 0
      src/components/organisms/MinionPoolDetailsContent/MinionPoolMachines.tsx
  30. 35 41
      src/components/organisms/MinionPoolDetailsContent/MinionPoolMainDetails.tsx
  31. 15 0
      src/components/organisms/MinionPoolDetailsContent/images/network.svg
  32. 20 23
      src/components/organisms/MinionPoolModal/MinionPoolModal.tsx
  33. 70 15
      src/components/organisms/MinionPoolModal/MinionPoolModalContent.tsx
  34. 14 42
      src/components/organisms/MinionPoolModal/images/minion-pool.svg
  35. 1 1
      src/components/organisms/Navigation/Navigation.tsx
  36. 61 0
      src/components/organisms/Navigation/images/minion-pool-menu.svg
  37. 0 15
      src/components/organisms/Navigation/images/minion-pools-menu.svg
  38. 2 1
      src/components/organisms/PageHeader/PageHeader.tsx
  39. 1 1
      src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.tsx
  40. 21 18
      src/components/organisms/WizardOptions/WizardOptions.tsx
  41. 2 2
      src/components/organisms/WizardPageContent/WizardPageContent.tsx
  42. 1 1
      src/components/organisms/WizardSummary/WizardSummary.tsx
  43. 1 1
      src/components/pages/MigrationDetailsPage/MigrationDetailsPage.tsx
  44. 4 2
      src/components/pages/MigrationsPage/MigrationsPage.tsx
  45. 93 151
      src/components/pages/MinionPoolDetailsPage/MinionPoolDetailsPage.tsx
  46. 19 75
      src/components/pages/MinionPoolDetailsPage/images/minion-pool.svg
  47. 84 101
      src/components/pages/MinionPoolsPage/MinionPoolsPage.tsx
  48. 17 34
      src/components/pages/MinionPoolsPage/images/minion-pool-empty-list.svg
  49. 4 3
      src/components/pages/ReplicaDetailsPage/ReplicaDetailsPage.tsx
  50. 5 3
      src/components/pages/ReplicasPage/ReplicasPage.tsx
  51. 1 1
      src/plugins/endpoint/default/OptionsSchemaPlugin.ts
  52. 4 4
      src/sources/MigrationSource.ts
  53. 76 77
      src/sources/MinionPoolSource.ts
  54. 4 6
      src/sources/ReplicaSource.ts
  55. 4 6
      src/sources/WizardSource.ts
  56. 16 69
      src/stores/MinionPoolStore.ts
  57. 4 0
      src/utils/ObjectUtils.ts

+ 1 - 1
README.md

@@ -26,7 +26,7 @@ Your server will be running at `http://localhost:3000/` (the port is configurabl
 ## Development mode
 
 - set env. variable `NODE_MODE='development'`
-- run `yarn ui-dev` to start local development server (starts on port 3001)
+- run `yarn client-dev` to start local development server (starts on port 3001)
 - run `yarn server-dev` to start the express server in development mode
 
 To debug the client code using VS Code, simply run the project's launch configuration from the 'Run' menu (Ctrl+Shift+D).

+ 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 = (

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

@@ -12,23 +12,50 @@ 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
+  allocation_status: string
+  connection_info?: any
+  power_status: string
+  provider_properties: any
+  last_used_at?: string
+  allocated_action: string | null
+}
+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: 'linux' | 'windows'
+  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[]
 }

+ 8 - 2
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 warningHollowImage from './images/warning-hollow.svg'
 
 type Props = {
   status: string,
@@ -33,7 +34,9 @@ type Props = {
   hollow?: boolean,
   secondary?: boolean,
   style?: React.CSSProperties
+  outlined?: boolean
   onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
+  title?: string
 }
 
 const getSpinnerUrl = (
@@ -48,7 +51,7 @@ const getRunningImageUrl = (props: Props) => {
 
 const getWarningUrl = (background: string) => `url('data:image/svg+xml;utf8,${encodeURIComponent(warningImage(background))}')`
 
-const statuses = (status: any, props: any) => {
+const statuses = (status: any, props: Props) => {
   switch (status) {
     case 'COMPLETED':
       return css`
@@ -57,6 +60,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 +78,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}');
       `
@@ -90,10 +95,11 @@ const statuses = (status: any, props: any) => {
       return css`
         background-image: ${getWarningUrl('#424242')};
       `
+    case 'INFO':
     case 'UNSCHEDULED':
     case 'UNEXECUTED':
       return css`
-        background-image: ${getWarningUrl(Palette.grayscale[2])};
+        background-image: ${props.hollow ? `url('${warningHollowImage}')` : getWarningUrl(Palette.grayscale[2])};
       `
     default:
       return null

+ 82 - 0
src/components/atoms/StatusIcon/images/warning-hollow.svg

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="warning-hollow.svg"
+   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+   id="svg8"
+   version="1.1"
+   viewBox="0 0 4.2333332 4.2333335"
+   height="16"
+   width="16">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     inkscape:window-maximized="0"
+     inkscape:window-y="0"
+     inkscape:window-x="147"
+     inkscape:window-height="1040"
+     inkscape:window-width="1274"
+     units="px"
+     showgrid="false"
+     inkscape:document-rotation="0"
+     inkscape:current-layer="Icon/Warning-8"
+     inkscape:document-units="mm"
+     inkscape:cy="4.0021626"
+     inkscape:cx="-0.059492155"
+     inkscape:zoom="22.4"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <g
+       transform="matrix(0.26428837,0,0,0.26428837,0.00235971,0.00235971)"
+       id="Icon/Warning-8"
+       style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1">
+      <circle
+         style="fill:none;stroke:#c8ccd7;stroke-width:1.451;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="circle833-0"
+         r="7.2834306"
+         cy="8"
+         cx="8"
+         fill-rule="evenodd"
+         fill="#0000ff" />
+      <path
+         style="stroke:#c8ccd7;stroke-opacity:1"
+         stroke-linecap="round"
+         stroke-width="2"
+         stroke="#ffffff"
+         id="Line-Copy-5"
+         d="M 8,8 V 4" />
+      <path
+         style="fill:#c8ccd7;fill-opacity:1"
+         fill-rule="evenodd"
+         fill="#ffffff"
+         id="Oval-3-5"
+         d="m 8,12.5 c 0.6903559,0 1.25,-0.559644 1.25,-1.25 C 9.25,10.559644 8.6903559,10 8,10 7.3096441,10 6.75,10.559644 6.75,11.25 6.75,11.940356 7.3096441,12.5 8,12.5 Z" />
+    </g>
+  </g>
+</svg>

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

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

+ 47 - 8
src/components/atoms/StatusPill/StatusPill.tsx

@@ -29,12 +29,31 @@ 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',
+  // Minion Machine power statuses
+  POWERING_OFF: 'POWERING OFF',
+  POWERING_ON: 'POWERING ON',
+  POWERED_ON: 'POWERED ON',
+  POWERED_OFF: 'POWERED OFF',
+  POWER_ERROR: 'ERROR',
 }
 
 const statuses = (status: any) => {
   switch (status) {
     case 'COMPLETED':
-    case 'ALLOCATED':
+    case 'ALLOCATED': // Minion Pool status
+    case 'POWERED_ON': // Minion Machine status
+    case 'AVAILABLE': // Minion Pool status
       return css`
         background: ${Palette.success};
         color: white;
@@ -43,6 +62,8 @@ const statuses = (status: any) => {
     case 'FAILED_TO_SCHEDULE':
     case 'FAILED_TO_CANCEL':
     case 'ERROR':
+    case 'ERROR_ALLOCATING_MINIONS':
+    case 'POWER_ERROR': // Minion Machine power status
       return css`
         background: ${Palette.alert};
         color: white;
@@ -66,9 +87,19 @@ 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 Machine status
+    case 'POWERING_ON': // Minion Machine power status
       return css`
         background: url('${runningImage}');
         animation: bgMotion 1s infinite linear;
@@ -79,10 +110,15 @@ const statuses = (status: any) => {
           100% { background-position: 0 -1px; }
         }
       `
+
     case 'CANCELLING':
     case 'UNINITIALIZING':
-    case 'DEALLOCATING':
+    case 'DEALLOCATING': // Minion Machine status
+    case 'POWERING_OFF': // Minion Machine power status
     case 'CANCELLING_AFTER_COMPLETION':
+    case 'IN_MAINTENANCE': // Minion Pool status
+    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 +137,11 @@ const statuses = (status: any) => {
         border-color: transparent;
       `
     case 'UNSCHEDULED':
-    case 'UNINITIALIZED':
-    case 'DEALLOCATED':
-    case 'INITIALIZED':
+    case 'UNKNOWN': // Minion Pool/Machine status
+    case 'UNINITIALIZED': // Minion Pool/Machine status
+    case 'DEALLOCATED': // Minion Pool status
+    case 'INITIALIZED': // Minion Pool status
+    case 'POWERED_OFF': // Minion Machine status
       return css`
         background: ${Palette.grayscale[2]};
         color: ${Palette.black};
@@ -156,6 +194,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"
+}

+ 11 - 8
src/components/molecules/MinionPoolListItem/MinionPoolListItem.tsx

@@ -59,7 +59,7 @@ const Wrapper = styled.div<any>`
   }
 `
 
-const Image = styled.div<any>`
+const Image = styled.div`
   min-width: 48px;
   height: 48px;
   background: url('${itemImage}') no-repeat center;
@@ -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.allocation_status === 'IN_USE' || m.allocation_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>
     )

Разлика између датотеке није приказан због своје велике величине
+ 19 - 56
src/components/molecules/MinionPoolListItem/images/minion-pool-list-item.svg


+ 1 - 1
src/components/molecules/NewItemDropdown/NewItemDropdown.tsx

@@ -30,7 +30,7 @@ import replicaImage from './images/replica.svg'
 import endpointImage from './images/endpoint.svg'
 import userImage from './images/user.svg'
 import projectImage from './images/project.svg'
-import minionPoolImage from './images/minionPool.svg'
+import minionPoolImage from './images/minion-pool.svg'
 
 import { navigationMenu } from '../../../constants'
 

Разлика између датотеке није приказан због своје велике величине
+ 63 - 0
src/components/molecules/NewItemDropdown/images/minion-pool.svg


+ 0 - 70
src/components/molecules/NewItemDropdown/images/minionPool.svg

@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   sodipodi:docname="minionPool.svg"
-   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
-   id="svg858"
-   version="1.1"
-   viewBox="0 0 12.170833 12.170834"
-   height="46"
-   width="46">
-  <defs
-     id="defs852">
-    <linearGradient
-       osb:paint="solid"
-       id="linearGradient1457">
-      <stop
-         id="stop1455"
-         offset="0"
-         style="stop-color:#0044ca;stop-opacity:1;" />
-    </linearGradient>
-  </defs>
-  <sodipodi:namedview
-     inkscape:window-maximized="0"
-     inkscape:window-y="0"
-     inkscape:window-x="403"
-     inkscape:window-height="1040"
-     inkscape:window-width="1274"
-     units="px"
-     showgrid="false"
-     inkscape:document-rotation="0"
-     inkscape:current-layer="layer1"
-     inkscape:document-units="mm"
-     inkscape:cy="6.7745742"
-     inkscape:cx="9.8898401"
-     inkscape:zoom="5.6"
-     inkscape:pageshadow="2"
-     inkscape:pageopacity="0.0"
-     borderopacity="1.0"
-     bordercolor="#666666"
-     pagecolor="#ffffff"
-     id="base" />
-  <metadata
-     id="metadata855">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     id="layer1"
-     inkscape:groupmode="layer"
-     inkscape:label="Layer 1">
-    <path
-       d="M 7.5316674,2.8742546 V 1.4229441 H 4.628978 V 2.8742627 Z M 1.0921959,4.3702397 v 5.8499833 c 0,0.324205 0.2652822,0.589487 0.5894863,0.589487 h 8.7972798 c 0.324206,0 0.589485,-0.265282 0.589485,-0.589487 V 4.3702397 c 0,-0.324205 -0.265279,-0.5894871 -0.589485,-0.5894871 H 1.6816822 c -0.3242041,0 -0.5894863,0.2652821 -0.5894863,0.5894871 z m 9.7038161,-1.495977 c 0.654305,0 1.178927,0.5246217 1.178927,1.1789256 V 10.53728 c 0,0.654302 -0.524622,1.178926 -1.178927,1.178926 H 1.3646085 c -0.65430385,0 -1.17892609,-0.524624 -1.17892609,-1.178926 l 0.005892,-6.4840917 c 0,-0.6543039 0.5187271,-1.1789256 1.17303099,-1.1789256 h 2.357851 V 1.695337 c 0,-0.6543033 0.5246217,-1.17892568 1.1789258,-1.17892568 h 2.357851 c 0.6543032,0 1.1789251,0.52462238 1.1789251,1.17892568 v 1.1789257 z"
-       style="fill:none;fill-opacity:1;stroke:#0044ca;stroke-width:0.371364;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       id="path10" />
-  </g>
-</svg>

+ 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>
     )

+ 60 - 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,33 +63,32 @@ const NavigationItems = [
     value: '',
   },
   {
-    label: 'Executions',
-    value: 'executions',
+    label: 'Machines',
+    value: 'machines',
+  },
+  {
+    label: 'Events',
+    value: 'events',
   },
 ]
 
 type Props = {
   item?: MinionPoolDetails | null,
+  itemId: string
   replicas: ReplicaItem[],
   migrations: MigrationItem[]
   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 +99,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,38 +127,50 @@ class MinionPoolDetailsContent extends React.Component<Props> {
     )
   }
 
-  renderMainDetails() {
-    if (this.props.page !== '') {
+  renderLoading() {
+    return (
+      <Loading>
+        <StatusImage loading />
+      </Loading>
+    )
+  }
+
+  renderMachines() {
+    if (this.props.page !== 'machines') {
       return null
     }
 
     return (
-      <MinionPoolMainDetails
+      <MinionPoolMachines
         item={this.props.item}
         replicas={this.props.replicas}
         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') {
+  renderEvents() {
+    if (this.props.page !== 'events') {
+      return null
+    }
+
+    return <MinionPoolEvents item={this.props.item} />
+  }
+
+  renderMainDetails() {
+    if (this.props.page !== '') {
       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}
+      <MinionPoolMainDetails
+        item={this.props.item}
+        replicas={this.props.replicas}
+        migrations={this.props.migrations}
+        schema={this.props.schema}
+        schemaLoading={this.props.schemaLoading}
+        endpoints={this.props.endpoints}
+        bottomControls={this.renderBottomControls()}
       />
     )
   }
@@ -172,12 +181,14 @@ class MinionPoolDetailsContent extends React.Component<Props> {
         <DetailsNavigation
           items={NavigationItems}
           selectedValue={this.props.page}
-          itemId={this.props.item ? this.props.item.id : ''}
+          itemId={this.props.itemId}
           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>
     )

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

@@ -0,0 +1,338 @@
+/*
+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`
+  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 && this.state.filterBy !== 'all') {
+          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 = 'INFO'
+          status = event.level || status
+          if (event.level === 'DEBUG') {
+            status = 'WARNING'
+          }
+          const title = event.current_step ? 'Progress Update' : 'Event'
+          return (
+            <Row key={event.id}>
+              <RowData
+                grow
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  paddingRight: '8px',
+                }}
+              >
+                <StatusIcon
+                  style={{ marginRight: '8px' }}
+                  status={status}
+                  title={title}
+                  hollow={event.current_step != 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

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

@@ -0,0 +1,312 @@
+/*
+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 { Link } from 'react-router-dom'
+import { MinionMachine, MinionPool } from '../../../@types/MinionPool'
+import DropdownLink from '../../molecules/DropdownLink/DropdownLink'
+import { ItemReplicaBadge } from '../../molecules/NotificationDropdown'
+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'
+import { MigrationItem, ReplicaItem, TransferItem } from '../../../@types/MainItem'
+
+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 }>`
+  display: flex;
+  margin-bottom: 8px;
+  align-items: center;
+  ${props => (props.secondary ? css`
+    color: ${Palette.grayscale[5]};
+    margin-bottom: 4px;
+  ` : '')}
+`
+const ValueLink = styled(Link)`
+  display: flex;
+  color: ${Palette.primary};
+  text-decoration: none;
+  cursor: pointer;
+`
+
+type FilterType = 'all' | 'allocated' | 'not-allocated'
+type Props = {
+  item?: MinionPool | null,
+  replicas: ReplicaItem[]
+  migrations: MigrationItem[]
+}
+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.allocation_status === 'ALLOCATED' || m.allocation_status === 'AVAILABLE')
+      default:
+        return this.machines.filter(m => m.allocation_status !== 'ALLOCATED' && m.allocation_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.allocation_status === 'ALLOCATED' || m.allocation_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 => {
+          const findTransferItem = (transferItems: TransferItem[]) => transferItems
+            .find(i => i.id === machine.allocated_action)
+          const allocatedAction = machine.allocated_action ? (
+            findTransferItem(this.props.replicas) || findTransferItem(this.props.migrations)
+          ) : null
+          return (
+            <MachineWrapper key={machine.id}>
+              <MachineTitle>ID: {machine.id}</MachineTitle>
+              <MachineBody>
+                <MachineRow>
+                  Allocation Status: <StatusPill style={{ marginLeft: '8px' }} status={machine.allocation_status} />
+                </MachineRow>
+                <MachineRow style={{ marginBottom: '16px' }}>
+                  <span style={{ width: '114px' }}>Power Status:</span> <StatusPill style={{ marginLeft: '8px' }} status={machine.power_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}
+                {machine.last_used_at ? <MachineRow secondary>Last Used At: {moment(machine.last_used_at).format('YYYY-MM-DD HH:mm:ss')}</MachineRow> : null}
+                {machine.allocated_action ? (
+                  <MachineRow secondary>
+                    Allocated Action:
+                    {allocatedAction ? (
+                      <>
+                        <ItemReplicaBadge style={{ margin: '0px 4px 0 5px' }}>{allocatedAction.type === 'replica' ? 'RE' : 'MI'}</ItemReplicaBadge>
+                        <ValueLink
+                          to={`/${allocatedAction.type}s/${allocatedAction.id}`}
+                        >
+                          {allocatedAction.instances[0]}
+                        </ValueLink>
+                      </>
+                    ) : <span>&nbsp;{machine.allocated_action}</span>}
+                  </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

+ 35 - 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,13 @@ 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>
+            <Field>
+              <Label>Pool OS Type</Label>
+              {this.renderValue(this.props.item?.os_type || '-', true)}
             </Field>
           </Row>
           <Row>
@@ -304,12 +297,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 +308,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>

+ 20 - 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,8 @@ class MinionPoolModal extends React.Component<Props, State> {
       <Content>
         <MinionPoolModalContent
           endpoint={this.props.endpoint}
+          platform={this.props.platform}
+          envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
           defaultSchema={minionPoolStore.minionPoolDefaultSchema}
           envSchema={minionPoolStore.minionPoolEnvSchema}
           invalidFields={this.state.invalidFields}
@@ -338,7 +335,7 @@ class MinionPoolModal extends React.Component<Props, State> {
     return (
       <LoadingWrapper>
         <StatusImage loading />
-        <LoadingText>Loading schema ...</LoadingText>
+        <LoadingText>Loading Pool Options ...</LoadingText>
       </LoadingWrapper>
     )
   }

+ 70 - 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,10 +118,12 @@ const GroupFields = styled.div<any>`
   flex-direction: column;
 `
 type Props = {
+  envOptionsDisabled: boolean
   defaultSchema: Field[],
   envSchema: Field[],
   invalidFields: string[],
   endpoint: Endpoint
+  platform: 'source' | 'destination'
   getFieldValue: (field: Field | null | undefined) => any,
   onFieldChange: (field: Field | null, value: any) => void,
   disabled: boolean,
@@ -118,7 +133,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 +178,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 +203,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' && this.props.platform === 'source')) {
+        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 +252,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 +261,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>
     )

Разлика између датотеке није приказан због своје велике величине
+ 14 - 42
src/components/organisms/MinionPoolModal/images/minion-pool.svg


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

@@ -37,7 +37,7 @@ import projectImage from './images/project-menu.svg'
 import userImage from './images/user-menu.svg'
 import logsImage from './images/logs-menu.svg'
 import dashboardImage from './images/dashboard-menu.svg'
-import minionPoolsImage from './images/minion-pools-menu.svg'
+import minionPoolsImage from './images/minion-pool-menu.svg'
 
 const isCollapsed = (props: any) => props.collapsed
   || (window.outerWidth <= StyleProps.mobileMaxWidth)

Разлика између датотеке није приказан због своје велике величине
+ 61 - 0
src/components/organisms/Navigation/images/minion-pool-menu.svg


+ 0 - 15
src/components/organisms/Navigation/images/minion-pools-menu.svg

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns="http://www.w3.org/2000/svg"
-   viewBox="0 0 6.3499999 6.3500002"
-   height="24"
-   width="24">
-  <g
-     transform="scale(0.89) translate(0.5 0.2)"
-     >
-    <path
-       id="path833"
-       style="fill:#feffff;fill-opacity:1;stroke-width:0.3175"
-       d="M 3.9567384,1.4287455 V 0.64703203 H 2.3932744 V 1.42875 Z M 0.48827166,2.234522 v 3.1509535 c 0,0.1746256 0.1428877,0.3175127 0.31751267,0.3175127 H 5.5442279 c 0.174625,0 0.3175127,-0.1428871 0.3175127,-0.3175127 V 2.234522 c 0,-0.1746251 -0.1428877,-0.3175128 -0.3175127,-0.3175128 H 0.80578433 c -0.17462497,0 -0.31751267,0.1428877 -0.31751267,0.3175128 z M 5.715,1.42875 c 0.352425,0 0.635,0.282575 0.635,0.635 v 3.4925001 c 0,0.3524247 -0.282575,0.635 -0.635,0.635 H 0.63500003 C 0.28257503,6.1912501 0,5.9086748 0,5.5562501 L 0.00317498,2.06375 c 0,-0.352425 0.27940005,-0.635 0.63182505,-0.635 H 1.9050001 V 0.79375013 C 1.9050001,0.44132511 2.1875751,0.15875 2.5400002,0.15875 H 3.8100003 C 4.162425,0.15875 4.445,0.44132511 4.445,0.79375013 V 1.42875 Z" />
-  </g>
-</svg>

+ 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 = [
       {

+ 93 - 151
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
@@ -83,8 +78,8 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
   }
 
   componentWillUnmount() {
-    this.stopPolling = true
     minionPoolStore.clearMinionPoolDetails()
+    this.stopPolling = true
   }
 
   get minionPoolId() {
@@ -94,29 +89,42 @@ 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 }),
+      replicaStore.getReplicas(),
+      migrationStore.getMigrations(),
     ])
+    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 +138,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,20 +156,6 @@ 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
@@ -182,16 +163,11 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
 
     await Promise.all([
       minionPoolStore.loadMinionPoolDetails(this.minionPoolId, {
-        showLoading, skipLog: true,
+        showLoading,
+        skipLog: true,
       }),
-      (async () => {
-        if (window.location.pathname.indexOf('executions') > -1) {
-          await minionPoolStore.loadExecutionTasks({
-            minionPoolId: this.minionPoolId,
-            skipLog: true,
-          })
-        }
-      })(),
+      replicaStore.getReplicas(),
+      migrationStore.getMigrations(),
     ])
 
     setTimeout(() => { this.pollData(false) }, configLoader.config.requestPollTimeout)
@@ -208,44 +184,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 +226,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 +241,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 +250,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 +291,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 +307,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 +318,22 @@ class MinionPoolDetailsPage extends React.Component<Props, State> {
 )}
           contentComponent={(
             <MinionPoolDetailsContent
-              item={minionPoolStore.minionPoolDetails}
+              item={this.minionPool}
+              itemId={this.minionPoolId}
               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 +347,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>
     )

Разлика између датотеке није приказан због своје велике величине
+ 19 - 75
src/components/pages/MinionPoolDetailsPage/images/minion-pool.svg


+ 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>
     )
   }

Разлика између датотеке није приказан због своје велике величине
+ 17 - 34
src/components/pages/MinionPoolsPage/images/minion-pool-empty-list.svg


+ 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] || {}

+ 76 - 77
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,26 +48,29 @@ 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',
         enum: [
           {
-            value: 'linux',
             label: 'Linux',
-          },
-          {
-            value: 'windows',
+            value: 'linux',
+          }, {
             label: 'Windows',
+            value: 'windows',
           },
         ],
       },
@@ -80,6 +80,38 @@ class MinionPoolSource {
         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: 'delete',
+            label: 'Delete',
+          },
+          {
+            value: 'poweroff',
+            label: 'Power Off',
+          },
+        ],
+      },
+      {
+        name: 'skip_allocation',
+        type: 'boolean',
+        nullableBoolean: false,
+      },
       {
         name: 'notes',
         type: 'string',
@@ -92,12 +124,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 +162,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 +178,7 @@ class MinionPoolSource {
         endpoint_id: endpointId,
         environment_options: {
           ...transformFieldsToPayload(envSchema, data),
+          list_all_destination_networks: true,
         },
       },
     }
@@ -146,6 +196,7 @@ class MinionPoolSource {
         ...transformFieldsToPayload(defaultSchema, data),
         environment_options: {
           ...transformFieldsToPayload(envSchema, data),
+          list_all_destination_networks: true,
         },
       },
     }
@@ -157,9 +208,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 +229,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 +236,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

+ 4 - 6
src/sources/WizardSource.ts

@@ -53,17 +53,15 @@ class WizardSource {
 
     if (data.sourceOptions) {
       const sourceEnv = sourceParser.getDestinationEnv(data.sourceOptions)
-      if (sourceEnv.minion_pool_id) {
-        payload[type].origin_minion_pool_id = sourceEnv.minion_pool_id
-        delete sourceEnv.minion_pool_id
+      if (data.sourceOptions.minion_pool_id) {
+        payload[type].origin_minion_pool_id = data.sourceOptions.minion_pool_id
       }
       payload[type].source_environment = sourceEnv
     }
 
     const destEnv = destParser.getDestinationEnv(data.destOptions)
-    if (destEnv.minion_pool_id) {
-      payload[type].destination_minion_pool_id = destEnv.minion_pool_id
-      delete destEnv.minion_pool_id
+    if (data.destOptions?.minion_pool_id) {
+      payload[type].destination_minion_pool_id = data.destOptions.minion_pool_id
     }
 
     const poolMappings = destEnv[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]

+ 16 - 69
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,8 +98,7 @@ class MinionPoolStore {
     }
   }
 
-  @action
-  clearMinionPoolDetails() {
+  @action clearMinionPoolDetails() {
     this.minionPoolDetails = null
   }
 
@@ -169,63 +166,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

Неке датотеке нису приказане због велике количине промена