Переглянути джерело

Merge pull request #289 from smiclea/azure-migrate

Make assessment data editable and store it locally
Dorin Paslaru 7 роки тому
батько
коміт
0ec3b0a481

+ 4 - 2
src/components/atoms/InfoIcon/InfoIcon.jsx

@@ -26,12 +26,13 @@ const Wrapper = styled.div`
   height: 16px;
   background: url('${props => props.warning ? warningImage : questionImage}') center no-repeat;
   display: inline-block;
-  margin-bottom: -4px;
-  margin-left: ${props => props.marginLeft ? `${props.marginLeft}px` : '4px'};
+  margin-left: ${props => props.marginLeft != null ? `${props.marginLeft}px` : '4px'};
+  margin-bottom: ${props => props.marginBottom != null ? `${props.marginBottom}px` : '-4px'};
 `
 type Props = {
   text: string,
   marginLeft?: number,
+  marginBottom?: number,
   className?: string,
   marginLeft?: number,
   warning?: boolean,
@@ -43,6 +44,7 @@ class InfoIcon extends React.Component<Props> {
       <Wrapper
         data-tip={this.props.text}
         marginLeft={this.props.marginLeft}
+        marginBottom={this.props.marginBottom}
         className={this.props.className}
         warning={this.props.warning}
       />

+ 114 - 0
src/components/atoms/SmallLoading/SmallLoading.jsx

@@ -0,0 +1,114 @@
+/*
+Copyright (C) 2018  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/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+
+import StyleProps from '../../styleUtils/StyleProps'
+import Palette from '../../styleUtils/Palette'
+
+const Wrapper = styled.div`
+  position: relative;
+  ${StyleProps.exactSize('28px')}
+  background-repeat: no-repeat;
+  background-position: center;
+`
+const ProgressSvgWrapper = styled.svg`
+  ${StyleProps.exactSize('100%')}
+  transform: rotate(-90deg);
+  ${props => props.spinning ? css`animation: rotate 1s linear infinite;` : ''}
+  @keyframes rotate {
+    0% {transform: rotate(0deg);}
+    100% {transform: rotate(360deg);}
+  }
+`
+const ProgressText = styled.div`
+  color: ${Palette.primary};
+  font-size: 9px;
+  font-weight: ${StyleProps.fontWeights.medium};
+  top: 9px;
+  position: absolute;
+  width: 100%;
+  text-align: center;
+`
+const CircleProgressBar = styled.circle`
+  transition: stroke-dashoffset ${StyleProps.animations.swift};
+`
+
+type Props = {
+  loadingProgress: number,
+}
+
+@observer
+class SmallLoading extends React.Component<Props> {
+  renderProgressImage() {
+    let progress = this.props.loadingProgress > -1 ? this.props.loadingProgress : 25
+
+    return (
+      <ProgressSvgWrapper
+        id="svg"
+        width="28"
+        height="28"
+        viewPort="0 0 28 28"
+        spinning={this.props.loadingProgress === -1}
+        version="1.1"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <g strokeWidth="2">
+          <circle
+            r="13"
+            cx="14"
+            cy="14"
+            fill="transparent"
+            stroke={Palette.grayscale[2]}
+          />
+          <CircleProgressBar
+            data-test-id="statusImage-progressBar"
+            r="13"
+            cx="14"
+            cy="14"
+            fill="transparent"
+            stroke={Palette.primary}
+            strokeDasharray="100 100"
+            strokeDashoffset={300 - ((progress / 100) * 82)}
+          />
+        </g>
+      </ProgressSvgWrapper>
+    )
+  }
+
+  renderProgressText() {
+    if (this.props.loadingProgress === -1) {
+      return null
+    }
+
+    return (
+      <ProgressText>{this.props.loadingProgress ? this.props.loadingProgress.toFixed(0) : 0}%</ProgressText>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        {this.renderProgressImage()}
+        {this.renderProgressText()}
+      </Wrapper>
+    )
+  }
+}
+
+export default SmallLoading

+ 6 - 0
src/components/atoms/SmallLoading/package.json

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

+ 27 - 0
src/components/atoms/SmallLoading/story.jsx

@@ -0,0 +1,27 @@
+/*
+Copyright (C) 2018  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/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import SmallLoading from '.'
+
+storiesOf('SmallLoading', module)
+  .add('default', () => (
+    <SmallLoading loadingProgress={75} />
+  ))
+  .add('spinning', () => (
+    <SmallLoading loadingProgress={-1} />
+  ))

+ 9 - 8
src/components/molecules/AssessedVmListItem/AssessedVmListItem.jsx

@@ -21,7 +21,7 @@ import styled, { css } from 'styled-components'
 import Checkbox from '../../atoms/Checkbox'
 import InfoIcon from '../../atoms/InfoIcon'
 import DropdownLink from '../../molecules/DropdownLink'
-import type { VmItem, VmSize } from '../../../types/Assessment'
+import type { VmItem } from '../../../types/Assessment'
 
 import Palette from '../../styleUtils/Palette'
 
@@ -68,9 +68,9 @@ type Props = {
   onSelectedChange: (item: VmItem, isChecked: boolean) => void,
   disabled: boolean,
   loadingVmSizes: boolean,
-  vmSizes: VmSize[],
-  onVmSizeChange: (size: VmSize) => void,
-  selectedVmSize: ?VmSize,
+  vmSizes: string[],
+  onVmSizeChange: (size: string) => void,
+  selectedVmSize: ?string,
   recommendedVmSize: string,
 }
 @observer
@@ -118,11 +118,12 @@ class AssessedVmListItem extends React.Component<Props> {
             <DropdownLink
               searchable
               width="208px"
-              noItemsLabel="Loading..."
-              items={this.props.loadingVmSizes ? [] : this.props.vmSizes.map(s => ({ value: s.name, label: s.name, size: s }))}
-              selectedItem={this.props.selectedVmSize ? this.props.selectedVmSize.name : ''}
+              noItemsLabel={this.props.loadingVmSizes ? 'Loading...' : 'No data'}
+              selectItemLabel="Auto Determined"
+              items={this.props.loadingVmSizes ? [] : this.props.vmSizes.map(s => ({ value: s, label: s }))}
+              selectedItem={this.props.selectedVmSize || ''}
               listWidth="200px"
-              onChange={item => { this.props.onVmSizeChange(item.size) }}
+              onChange={item => { this.props.onVmSizeChange(item.value) }}
               disabled={this.props.disabled}
               highlightedItem={this.props.recommendedVmSize}
             />

+ 9 - 9
src/components/molecules/AssessmentListItem/AssessmentListItem.jsx

@@ -17,7 +17,6 @@ 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 moment from 'moment'
 
 import StatusPill from '../../atoms/StatusPill'
 
@@ -85,12 +84,13 @@ const AssessmentLabel = styled.div`
   color: ${Palette.grayscale[4]};
   width: 64px;
 `
-const Project = styled.div`
-  min-width: 96px;
+const TotalVms = styled.div`
+  ${StyleProps.exactWidth('96px')}
   margin-right: 48px;
 `
-const Updated = styled.div`
-  min-width: 175px;
+const Project = styled.div`
+  ${StyleProps.exactWidth('132px')}
+  margin-right: 48px;
 `
 const ItemLabel = styled.div`
   color: ${Palette.grayscale[4]};
@@ -133,12 +133,12 @@ class AssessmentListItem extends React.Component<Props> {
               {this.props.item.project.name}
             </ItemValue>
           </Project>
-          <Updated>
-            <ItemLabel>Updated</ItemLabel>
+          <TotalVms>
+            <ItemLabel>Instances</ItemLabel>
             <ItemValue>
-              {moment(this.props.item.properties.updatedTimestamp).format('DD MMMM YYYY, HH:mm')}
+              {this.props.item.properties.numberOfMachines}
             </ItemValue>
-          </Updated>
+          </TotalVms>
         </Content>
       </Wrapper>
     )

+ 34 - 5
src/components/molecules/EndpointField/EndpointField.jsx

@@ -25,6 +25,9 @@ import InfoIcon from '../../atoms/InfoIcon'
 import Dropdown from '../../molecules/Dropdown'
 import DropdownInput from '../../molecules/DropdownInput'
 import TextArea from '../../atoms/TextArea'
+import PropertiesTable from '../../molecules/PropertiesTable'
+
+import type { Field as FieldType } from '../../../types/Field'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -58,7 +61,8 @@ type Props = {
   name: string,
   type: string,
   value: any,
-  onChange?: (value: any) => void,
+  onChange?: (value: any, fieldName?: string) => void,
+  valueCallback?: (fieldName: string) => void,
   getFieldValue?: (fieldName: string) => string,
   onFieldChange?: (fieldName: string, fieldValue: string) => void,
   className?: string,
@@ -68,6 +72,7 @@ type Props = {
   required?: boolean,
   large?: boolean,
   highlight?: boolean,
+  properties?: FieldType[],
   disabled?: boolean,
   // $FlowIssue
   enum?: string[] | { label: string, value: string }[],
@@ -80,12 +85,13 @@ type Props = {
 }
 @observer
 class Field extends React.Component<Props> {
-  renderSwitch() {
+  renderSwitch(propss: { triState: boolean }) {
     return (
       <Switch
         data-test-id={`endpointField-switch-${this.props.name}`}
         disabled={this.props.disabled}
-        checked={this.props.value || false}
+        triState={propss.triState}
+        checked={this.props.value}
         onChange={checked => { if (this.props.onChange) this.props.onChange(checked) }}
       />
     )
@@ -106,6 +112,25 @@ class Field extends React.Component<Props> {
     )
   }
 
+  renderObjectTable() {
+    if (!this.props.properties || !this.props.properties.length) {
+      return null
+    }
+
+    return (
+      <PropertiesTable
+        properties={this.props.properties}
+        valueCallback={field => { if (this.props.valueCallback) { this.props.valueCallback(field.name) } }}
+        onChange={(field, value) => {
+          let fieldName = field.name.substr(field.name.lastIndexOf('/') + 1)
+          if (this.props.onChange) {
+            this.props.onChange(value, fieldName)
+          }
+        }}
+      />
+    )
+  }
+
   renderTextArea() {
     return (
       <TextArea
@@ -239,7 +264,9 @@ class Field extends React.Component<Props> {
       case 'input-choice':
         return this.renderDropdownInput()
       case 'boolean':
-        return this.renderSwitch()
+        return this.renderSwitch({ triState: false })
+      case 'optional-boolean':
+        return this.renderSwitch({ triState: true })
       case 'string':
         if (this.props.enum) {
           return this.renderEnumDropdown()
@@ -257,6 +284,8 @@ class Field extends React.Component<Props> {
         return this.renderRadioInput()
       case 'array':
         return this.renderArrayDropdown()
+      case 'object':
+        return this.renderObjectTable()
       default:
         return null
     }
@@ -270,7 +299,7 @@ class Field extends React.Component<Props> {
     let description = LabelDictionary.getDescription(this.props.name)
     let infoIcon = null
     if (description) {
-      infoIcon = <InfoIcon text={description} marginLeft={-20} />
+      infoIcon = <InfoIcon text={description} marginLeft={-20} marginBottom={0} />
     }
 
     return (

+ 8 - 1
src/components/molecules/Table/Table.jsx

@@ -105,6 +105,8 @@ type Props = {
   className?: string,
   useSecondaryStyle?: boolean,
   noItemsLabel?: string,
+  noItemsComponent?: React.Node,
+  noItemsStyle?: any,
   bodyStyle?: any,
   headerStyle?: any,
   'data-test-id'?: string,
@@ -139,7 +141,12 @@ class Table extends React.Component<Props> {
       return null
     }
 
-    return <NoItems secondary={this.props.useSecondaryStyle} data-test-id="table-noItems">{this.props.noItemsLabel}</NoItems>
+    return (
+      <NoItems
+        secondary={this.props.useSecondaryStyle}
+        data-test-id="table-noItems"
+        style={this.props.noItemsStyle}
+      >{this.props.noItemsComponent || this.props.noItemsLabel}</NoItems>)
   }
 
   renderItems() {

+ 0 - 1
src/components/molecules/WizardOptionsField/WizardOptionsField.jsx

@@ -58,7 +58,6 @@ const Asterisk = styled.div`
   margin-bottom: -3px;
   margin-left: ${props => props.marginLeft || '0px'};
 `
-
 type Props = {
   type: 'replica' | 'migration',
   name: string,

+ 122 - 98
src/components/organisms/AssessmentDetailsContent/AssessmentDetailsContent.jsx

@@ -28,10 +28,11 @@ import AssessedVmListItem from '../../molecules/AssessedVmListItem'
 import DropdownFilter from '../../molecules/DropdownFilter'
 import Tooltip from '../../atoms/Tooltip'
 import Checkbox from '../../atoms/Checkbox'
+import SmallLoading from '../../atoms/SmallLoading'
 
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
-import type { Assessment, VmItem, VmSize } from '../../../types/Assessment'
+import type { Assessment, VmItem, Location } from '../../../types/Assessment'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Instance, Nic } from '../../../types/Instance'
 import type { Network, NetworkMap } from '../../../types/Network'
@@ -97,6 +98,15 @@ const LoadingText = styled.div`
   font-size: 18px;
   margin-top: 32px;
 `
+const SmallLoadingWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`
+const SmallLoadingText = styled.div`
+  font-size: 14px;
+  margin-left: 16px;
+`
 const TableStyled = styled(Table)`
   margin-top: 62px;
   ${props => props.addWidthPadding ? css`
@@ -157,25 +167,35 @@ const NavigationItems = [
 
 type Props = {
   item: ?Assessment,
-  targetEndpoint: Endpoint,
   detailsLoading: boolean,
   instancesDetailsLoading: boolean,
   instancesLoading: boolean,
   networksLoading: boolean,
   instancesDetailsProgress: ?number,
+  targetEndpoint: Endpoint,
+  targetEndpoints: Endpoint[],
+  onTargetEndpointChange: (endpoint: Endpoint) => void,
+  targetEndpointsLoading: boolean,
   sourceEndpoints: Endpoint[],
+  sourceEndpoint: ?Endpoint,
   sourceEndpointsLoading: boolean,
+  locations: Location[],
+  selectedLocation: ?string,
+  onLocationChange: (locationName: string) => void,
+  selectedResourceGroup: string,
+  resourceGroups: string[],
+  onResourceGroupChange: (resourceGroupName: string) => void,
+  targetOptionsLoading: boolean,
   assessedVmsCount: number,
   filteredAssessedVms: VmItem[],
-  selectedVms: VmItem[],
+  selectedVms: string[],
   instancesDetails: Instance[],
   instances: Instance[],
   loadingVmSizes: boolean,
-  vmSizes: VmSize[],
-  onVmSizeChange: (vm: VmItem, size: { name: string }) => void,
-  onGetVmSize: (vm: VmItem) =>?VmSize,
+  vmSizes: string[],
+  onVmSizeChange: (vmId: string, size: string) => void,
+  onGetSelectedVmSize: (vm: VmItem) =>?string,
   networks: Network[],
-  sourceEndpoint: ?Endpoint,
   page: string,
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onVmSearchValueChange: (value: string) => void,
@@ -223,39 +243,19 @@ class AssessmentDetailsContent extends React.Component<Props> {
     )
   }
 
-  renderSourceDropdown() {
-    return (
-      <DropdownLink
-        selectedItem={this.props.sourceEndpoint ? this.props.sourceEndpoint.id : ''}
-        items={this.props.sourceEndpoints.map(endpoint => ({ label: endpoint.name, value: endpoint.id, endpoint }))}
-        onChange={item => { this.props.onSourceEndpointChange(item.endpoint) }}
-        selectItemLabel="Select Endpoint"
-        noItemsLabel={this.props.sourceEndpointsLoading ? 'Loading ....' : 'No matching endpoints'}
-      />
-    )
-  }
-
   renderMainDetails() {
-    if (this.props.detailsLoading) {
-      return null
-    }
-
     if (this.props.page !== '' || !this.props.item || !this.props.item.id) {
       return null
     }
 
     let status = this.props.item ?
-      this.props.item.properties.status === 'Completed' ? 'Ready' : this.props.item.properties.status : ''
+      this.props.item.properties.status === 'Completed' ? 'Ready for Migration' : this.props.item.properties.status : ''
+
+    let locationItem: ?Location = this.props.locations.find(l => l.id.toLowerCase() === (this.props.selectedLocation ? this.props.selectedLocation.toLowerCase() : null))
 
     return (
       <Columns>
         <Column>
-          <Row>
-            <Field>
-              <Label>Type</Label>
-              <Value>Azure Migrate</Value>
-            </Field>
-          </Row>
           <Row>
             <AzureMigrateLogo />
           </Row>
@@ -263,52 +263,82 @@ class AssessmentDetailsContent extends React.Component<Props> {
             <Field>
               <Label>Last Update</Label>
               <Value>
-                {moment(this.props.item.properties.updatedTimestamp).format('YYYY-MM-DD HH:mm:ss')}
+                {this.props.item ? moment(this.props.item.properties.updatedTimestamp).format('YYYY-MM-DD HH:mm:ss') : '-'}
               </Value>
             </Field>
           </Row>
           <Row>
             <Field>
-              <Label>Status</Label>
-              <Value>{status}</Value>
+              <Label>Migration Project</Label>
+              <Value>{this.props.item ? this.props.item.projectName : ''}</Value>
             </Field>
           </Row>
           <Row>
             <Field>
-              <Label>Source Endpoint</Label>
-              <Value>{this.renderSourceDropdown()}</Value>
+              <Label>VM Group</Label>
+              <Value>{this.props.item ? this.props.item.groupName : ''}</Value>
             </Field>
           </Row>
-        </Column>
-        <Column>
           <Row>
             <Field>
-              <Label>Project</Label>
-              <Value>{this.props.item ? this.props.item.projectName : ''}</Value>
+              <Label>Status</Label>
+              <Value>{status}</Value>
             </Field>
           </Row>
+        </Column>
+        <Column>
           <Row>
             <Field>
-              <Label>Location</Label>
-              <Value>{this.props.item ? this.props.item.properties.azureLocation : ''}</Value>
+              <Label>Source Endpoint</Label>
+              <Value>
+                <DropdownLink
+                  selectedItem={this.props.sourceEndpoint ? this.props.sourceEndpoint.id : ''}
+                  items={this.props.sourceEndpoints.map(endpoint => ({ label: endpoint.name, value: endpoint.id, endpoint }))}
+                  onChange={item => { this.props.onSourceEndpointChange(item.endpoint) }}
+                  selectItemLabel="Select Endpoint"
+                  noItemsLabel={this.props.sourceEndpointsLoading ? 'Loading ....' : 'No matching endpoints'}
+                />
+              </Value>
             </Field>
           </Row>
           <Row>
             <Field>
-              <Label>Resource Group</Label>
-              <Value>{this.props.item ? this.props.item.resourceGroupName : ''}</Value>
+              <Label>Target endpoint</Label>
+              <Value>
+                <DropdownLink
+                  selectedItem={this.props.targetEndpoint ? this.props.targetEndpoint.id : ''}
+                  items={this.props.targetEndpoints.map(endpoint => ({ label: endpoint.name, value: endpoint.id, endpoint }))}
+                  onChange={item => { this.props.onTargetEndpointChange(item.endpoint) }}
+                  selectItemLabel="Select Endpoint"
+                  noItemsLabel={this.props.targetEndpointsLoading ? 'Loading ....' : 'No Azure endpoints'}
+                />
+              </Value>
             </Field>
           </Row>
           <Row>
             <Field>
-              <Label>VM Group</Label>
-              <Value>{this.props.item ? this.props.item.groupName : ''}</Value>
+              <Label>Resource Group</Label>
+              <Value>
+                <DropdownLink
+                  selectedItem={this.props.selectedResourceGroup}
+                  items={this.props.resourceGroups.map(group => ({ label: group, value: group }))}
+                  onChange={item => { this.props.onResourceGroupChange(item.value) }}
+                  noItemsLabel={this.props.targetOptionsLoading ? 'Loading ....' : 'No Resource Groups found'}
+                />
+              </Value>
             </Field>
           </Row>
           <Row>
             <Field>
-              <Label>Target endpoint</Label>
-              <Value>{this.props.targetEndpoint.name}</Value>
+              <Label>Location</Label>
+              <Value>
+                <DropdownLink
+                  selectedItem={locationItem ? locationItem.id : ''}
+                  items={this.props.locations.map(location => ({ label: location.name, value: location.id }))}
+                  onChange={item => { this.props.onLocationChange(item.value) }}
+                  noItemsLabel={this.props.targetOptionsLoading ? 'Loading ....' : 'No Locations found'}
+                />
+              </Value>
             </Field>
           </Row>
         </Column>
@@ -317,22 +347,20 @@ class AssessmentDetailsContent extends React.Component<Props> {
   }
 
   renderVmsTable() {
-    if (this.props.detailsLoading || this.props.sourceEndpointsLoading || this.props.instancesLoading) {
-      return null
-    }
+    let loading = this.props.instancesLoading
 
     let items = this.props.filteredAssessedVms.map(vm => {
       return (
         <AssessedVmListItem
           item={vm}
-          selected={this.props.selectedVms.filter(m => m.id === vm.id).length > 0}
+          selected={this.props.selectedVms.filter(m => m === vm.properties.datacenterMachineId).length > 0}
           onSelectedChange={(vm, selected) => { this.props.onVmSelectedChange(vm, selected) }}
           disabled={!this.doesVmMatchSource(vm)}
           loadingVmSizes={this.props.loadingVmSizes}
           recommendedVmSize={vm.properties.recommendedSize}
           vmSizes={this.props.vmSizes}
-          selectedVmSize={this.props.onGetVmSize(vm)}
-          onVmSizeChange={size => { this.props.onVmSizeChange(vm, size) }}
+          selectedVmSize={this.props.onGetSelectedVmSize(vm)}
+          onVmSizeChange={size => { this.props.onVmSizeChange(vm.properties.datacenterMachineId, size) }}
         />
       )
     })
@@ -341,7 +369,7 @@ class AssessmentDetailsContent extends React.Component<Props> {
       `${this.props.filteredAssessedVms.length} OUT OF ${this.props.assessedVmsCount}`})`
     let vmHeaderItem = (
       <VmHeaderItem>
-        <Checkbox checked={this.props.selectAllVmsChecked} onChange={checked => { this.props.onSelectAllVmsChange(checked) }} />
+        {loading ? null : <Checkbox checked={this.props.selectAllVmsChecked} onChange={checked => { this.props.onSelectAllVmsChange(checked) }} />}
         <VmHeaderItemLabel>Virtual Machine {vmCountLabel}</VmHeaderItemLabel>
         <DropdownFilter
           searchPlaceholder="Filter Virtual Machines"
@@ -351,22 +379,33 @@ class AssessmentDetailsContent extends React.Component<Props> {
       </VmHeaderItem>
     )
 
+
     return (
       <TableStyled
         addWidthPadding
-        items={items}
+        items={loading ? [] : items}
         bodyStyle={TableBodyStyle}
         headerStyle={TableHeaderStyle}
         header={[vmHeaderItem, 'OS', 'Target Disk Type', 'Azure VM Size']}
         useSecondaryStyle
-        noItemsLabel="No VMs found!"
+        noItemsComponent={this.renderLoading('Loading instances, please wait ...')}
       />
     )
   }
 
   renderNetworkTable() {
-    if (this.props.detailsLoading || this.props.sourceEndpointsLoading || this.props.instancesDetailsLoading || this.props.networksLoading || this.props.instancesLoading) {
-      return null
+    let loading = this.props.networksLoading || this.props.instancesDetailsLoading
+
+    if (loading) {
+      return (
+        <TableStyled
+          items={[]}
+          header={['Source Network', '', '', 'Target Network']}
+          useSecondaryStyle
+          noItemsStyle={{ marginLeft: 0 }}
+          noItemsComponent={this.renderNetworksLoading()}
+        />
+      )
     }
 
     let nics = []
@@ -413,18 +452,32 @@ class AssessmentDetailsContent extends React.Component<Props> {
     })
     return (
       <TableStyled
-        items={items}
+        items={loading ? [] : items}
         header={['Source Network', '', '', 'Target Network']}
         useSecondaryStyle
+        noItemsStyle={{ marginLeft: 0 }}
+        noItemsComponent={this.renderNetworksLoading()}
       />
     )
   }
 
-  renderButtons() {
-    if (this.props.detailsLoading) {
-      return null
+  renderNetworksLoading() {
+    let loadingProgress = -1
+    if (this.props.instancesDetailsLoading) {
+      if (this.props.instancesDetailsProgress != null) {
+        loadingProgress = Math.round(this.props.instancesDetailsProgress * 100)
+      }
     }
 
+    return (
+      <SmallLoadingWrapper>
+        <SmallLoading loadingProgress={loadingProgress} />
+        <SmallLoadingText>Loading networks, please wait ...</SmallLoadingText>
+      </SmallLoadingWrapper>
+    )
+  }
+
+  renderButtons() {
     return (
       <Buttons>
         <Button secondary onClick={this.props.onRefresh}>Refresh</Button>
@@ -436,39 +489,10 @@ class AssessmentDetailsContent extends React.Component<Props> {
     )
   }
 
-  renderLoading() {
-    let message = ''
-    let loadingProgress = -1
-    if (!this.props.detailsLoading && !this.props.sourceEndpointsLoading && !this.props.instancesDetailsLoading && !this.props.networksLoading && !this.props.instancesLoading) {
-      return null
-    }
-
-    if (this.props.instancesDetailsLoading) {
-      if (this.props.instancesDetailsProgress != null) {
-        loadingProgress = Math.round(this.props.instancesDetailsProgress * 100)
-      }
-      message = 'Loading instances details, please wait ...'
-    }
-
-    if (this.props.instancesLoading) {
-      message = 'Loading instances ...'
-    }
-
-    if (this.props.networksLoading) {
-      message = 'Loading networks ...'
-    }
-
-    if (this.props.sourceEndpointsLoading) {
-      message = 'Loading source endpoints ...'
-    }
-
-    if (this.props.detailsLoading) {
-      message = 'Loading assessment ...'
-    }
-
+  renderLoading(message: string) {
     return (
       <LoadingWrapper>
-        <StatusImage loading loadingProgress={loadingProgress} />
+        <StatusImage loading />
         <LoadingText>{message}</LoadingText>
       </LoadingWrapper>
     )
@@ -484,11 +508,11 @@ class AssessmentDetailsContent extends React.Component<Props> {
           customHref={() => null}
         />
         <DetailsBody>
-          {this.renderMainDetails()}
-          {this.renderVmsTable()}
-          {this.renderNetworkTable()}
-          {this.renderLoading()}
-          {this.renderButtons()}
+          {this.props.detailsLoading ? null : this.renderMainDetails()}
+          {this.props.detailsLoading ? this.renderLoading('Loading assessment...') : null}
+          {this.props.detailsLoading ? null : this.renderVmsTable()}
+          {this.props.detailsLoading || this.props.instancesLoading ? null : this.renderNetworkTable()}
+          {this.props.detailsLoading ? null : this.renderButtons()}
           <Tooltip />
         </DetailsBody>
       </Wrapper>

+ 173 - 61
src/components/organisms/AssessmentMigrationOptions/AssessmentMigrationOptions.jsx

@@ -14,16 +14,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import React from 'react'
+import * as React from 'react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 
 import Button from '../../atoms/Button'
-import WizardOptionsField from '../../molecules/WizardOptionsField'
+import EndpointField from '../../molecules/EndpointField'
+import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
+import Tooltip from '../../atoms/Tooltip'
 
-import LabelDictionary from '../../../utils/LabelDictionary'
 import type { Field } from '../../../types/Field'
 
+import StyleProps from '../../styleUtils/StyleProps'
+
 import assessmentImage from './images/assessment.svg'
 
 const Wrapper = styled.div`
@@ -31,124 +34,233 @@ const Wrapper = styled.div`
   display: flex;
   flex-direction: column;
   align-items: center;
+  min-height: 0;
 `
 const Image = styled.div`
   width: 96px;
   height: 96px;
   background: url('${assessmentImage}') center no-repeat;
 `
+const ToggleButtonBarStyled = styled(ToggleButtonBar)`
+  margin-top: 48px;
+`
 const Fields = styled.div`
-  margin-top: 64px;
+  display: flex;
+  margin-top: 32px;
+  flex-direction: column;
+  overflow: auto;
+  width: 100%;
+  min-height: 0;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField)`
-  width: 319px;
+const FieldStyled = styled(EndpointField)`
+  ${StyleProps.exactWidth('224px')}
+  margin-bottom: 16px;
+`
+const Row = styled.div`
+  display: flex;
+  flex-shrink: 0;
   justify-content: space-between;
-  margin-bottom: 32px;
-  &:last-child {
-    margin-bottom: 0;
-  }
 `
+
 const Buttons = styled.div`
   display: flex;
   justify-content: space-between;
   width: 100%;
-  margin-top: 48px;
+  margin-top: 32px;
 `
 
 const generalFields = [
   {
     name: 'use_replica',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
   {
     name: 'separate_vm',
-    type: 'boolean',
-    value: true,
+    type: 'strict-boolean',
   },
 ]
 const replicaFields = [
   {
     name: 'shutdown_instances',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
 ]
 const migrationFields = [
   {
     name: 'skip_os_morphing',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
 ]
 
 type Props = {
   onCancelClick: () => void,
-  onExecuteClick: (fields: Field[]) => void,
+  onExecuteClick: (fieldValues: { [string]: any }) => void,
   executeButtonDisabled: boolean,
+  replicaSchema: Field[],
+  migrationSchema: Field[],
+  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
 }
 type State = {
-  generalFields: Field[],
-  migrationFields: Field[],
-  replicaFields: Field[],
+  fieldValues: { [string]: any },
+  showAdvancedOptions: boolean,
 }
 @observer
 class AssessmentMigrationOptions extends React.Component<Props, State> {
   state = {
-    generalFields: [...generalFields],
-    migrationFields: [...migrationFields],
-    replicaFields: [...replicaFields],
+    fieldValues: {
+      separate_vm: true,
+      use_replica: false,
+      shutdown_instances: false,
+      skip_os_morphing: false,
+    },
+    showAdvancedOptions: false,
   }
 
-  handleValueChange(field: Field, value: any) {
-    let mapFields = fields => {
-      let mappedFields = fields.map(f => {
-        if (f.name === field.name) {
-          return { ...f, value }
-        }
-        return { ...f }
-      })
-      return mappedFields
+  scrollableRef: HTMLElement
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    if (prevState.showAdvancedOptions !== this.state.showAdvancedOptions && this.props.onResizeUpdate) {
+      this.props.onResizeUpdate(this.scrollableRef)
     }
-    this.setState({
-      generalFields: mapFields(this.state.generalFields),
-      migrationFields: mapFields(this.state.migrationFields),
-      replicaFields: mapFields(this.state.replicaFields),
-    })
+    Tooltip.rebuild()
   }
 
-  renderField(field: Field) {
-    return (
-      <WizardOptionsFieldStyled
-        key={field.name}
-        name={field.name}
-        type="strict-boolean"
-        value={field.value}
-        label={LabelDictionary.get(field.name)}
-        onChange={value => this.handleValueChange(field, value)}
-      />
-    )
+  handleValueChange(fieldName: string, value: any) {
+    let fieldValues = { ...this.state.fieldValues }
+    if (value != null) {
+      fieldValues[fieldName] = value
+    } else {
+      delete fieldValues[fieldName]
+    }
+    this.setState({ fieldValues })
   }
 
-  render() {
-    let fields = this.state.generalFields
-    let useReplicaField = fields.find(f => f.name === 'use_replica')
+  getFieldValue(fieldName: string) {
+    if (this.state.fieldValues[fieldName] != null) {
+      return this.state.fieldValues[fieldName]
+    }
+    return null
+  }
 
-    if (useReplicaField && useReplicaField.value) {
-      fields = [...fields, ...this.state.replicaFields]
+  getObjectFieldValue(fieldName: string, propName: string) {
+    return this.state.fieldValues[fieldName] && this.state.fieldValues[fieldName][propName]
+  }
+
+  handleObjectValueChange(fieldName: string, propName: string, value: any) {
+    let fieldValues = { ...this.state.fieldValues }
+    if (!fieldValues[fieldName]) {
+      fieldValues[fieldName] = {}
+    }
+    fieldValues[fieldName][propName] = value
+    this.setState({ fieldValues })
+  }
+
+  renderFields() {
+    let fields = generalFields
+    let useReplica = this.getFieldValue('use_replica')
+    let skipFields = ['location', 'resource_group', 'network_map', 'storage_map', 'vm_size', 'worker_size']
+
+    if (useReplica) {
+      fields = [...fields, ...replicaFields]
+      if (this.state.showAdvancedOptions) {
+        fields = [...fields, ...this.props.replicaSchema.filter(f => !skipFields.find(n => n === f.name))]
+      }
     } else {
-      fields = [...fields, ...this.state.migrationFields]
+      fields = [...fields, ...migrationFields]
+      if (this.state.showAdvancedOptions) {
+        fields = [...fields, ...this.props.migrationSchema.filter(f => !skipFields.find(n => n === f.name))]
+      }
+    }
+
+    const sortPriority: any = {
+      'strict-boolean': 1,
+      boolean: 2,
+      string: 3,
+      object: 4,
     }
+    fields.sort((a, b) => {
+      if (sortPriority[a.type] && sortPriority[b.type]) {
+        return sortPriority[a.type] - sortPriority[b.type]
+      }
+      if (sortPriority[a.type]) {
+        return -1
+      }
+      if (sortPriority[b.type]) {
+        return 1
+      }
+      return a.name.localeCompare(b.name)
+    })
+
+    const rows = []
+    let lastField
+    fields.forEach((field, index) => {
+      let additionalProps
+      if (field.type === 'object' && field.properties) {
+        additionalProps = {
+          valueCallback: propName => this.getObjectFieldValue(field.name, propName),
+          onChange: (value, propName) => { this.handleObjectValueChange(field.name, propName, value) },
+          properties: field.properties,
+        }
+      } else {
+        let value = this.getFieldValue(field.name)
+        additionalProps = {
+          value,
+          onChange: value => { this.handleValueChange(field.name, value) },
+          type: field.type === 'strict-boolean' ? 'boolean' : field.type === 'boolean' ? 'optional-boolean' : field.type,
+        }
+      }
+
+      const currentField = (
+        <FieldStyled
+          large
+          {...field}
+          {...additionalProps}
+        />
+      )
+      const pushRow = (field1: React.Node, field2?: React.Node) => {
+        rows.push((
+          <Row key={field.name}>
+            {field1}
+            {field2}
+          </Row>
+        ))
+      }
+      if (index === fields.length - 1 && index % 2 === 0) {
+        pushRow(currentField)
+      } else if (index % 2 !== 0) {
+        pushRow(lastField, currentField)
+      } else {
+        lastField = currentField
+      }
+    })
+
+    return (
+      <Fields innerRef={ref => { this.scrollableRef = ref }}>
+        {rows}
+        <Tooltip />
+      </Fields>
+    )
+  }
 
+  render() {
     return (
       <Wrapper>
         <Image />
-        <Fields>
-          {fields.map(field => {
-            return this.renderField(field)
-          })}
-        </Fields>
+        <ToggleButtonBarStyled
+          items={[{ label: 'Basic', value: 'basic' }, { label: 'Advanced', value: 'advanced' }]}
+          selectedValue={this.state.showAdvancedOptions ? 'advanced' : 'basic'}
+          onChange={item => { this.setState({ showAdvancedOptions: item.value === 'advanced' }) }}
+        />
+        {this.renderFields()}
         <Buttons>
-          <Button secondary onClick={() => { this.props.onCancelClick() }}>Cancel</Button>
           <Button
-            onClick={() => { this.props.onExecuteClick(fields) }}
+            large
+            secondary
+            onClick={() => { this.props.onCancelClick() }}
+          >Cancel</Button>
+          <Button
+            large
+            onClick={() => { this.props.onExecuteClick(this.state.fieldValues) }}
             disabled={this.props.executeButtonDisabled}
           >Execute</Button>
         </Buttons>

+ 226 - 108
src/components/pages/AssessmentDetailsPage/AssessmentDetailsPage.jsx

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import styled from 'styled-components'
+import cookie from 'js-cookie'
 import { observer } from 'mobx-react'
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
@@ -26,11 +27,12 @@ import Modal from '../../molecules/Modal'
 import AssessmentMigrationOptions from '../../organisms/AssessmentMigrationOptions'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Nic } from '../../../types/Instance'
-import type { VmItem, VmSize } from '../../../types/Assessment'
+import type { VmItem } from '../../../types/Assessment'
 import type { Field } from '../../../types/Field'
 import type { Network, NetworkMap } from '../../../types/Network'
 
 import azureStore from '../../../stores/AzureStore'
+import type { LocalData } from '../../../stores/AzureStore'
 import endpointStore from '../../../stores/EndpointStore'
 import notificationStore from '../../../stores/NotificationStore'
 import replicaStore from '../../../stores/ReplicaStore'
@@ -38,6 +40,7 @@ import instanceStore from '../../../stores/InstanceStore'
 import networkStore from '../../../stores/NetworkStore'
 import userStore from '../../../stores/UserStore'
 import assessmentStore from '../../../stores/AssessmentStore'
+import providerStore from '../../../stores/ProviderStore'
 
 import assessmentImage from './images/assessment.svg'
 
@@ -47,29 +50,32 @@ type Props = {
   match: any,
 }
 type State = {
-  sourceEndpoint: ?Endpoint,
-  selectedVms: VmItem[],
   selectedNetworks: NetworkMap[],
   showMigrationOptions: boolean,
   executeButtonDisabled: boolean,
-  vmSizes: { [string]: VmSize },
   vmSearchValue: string,
+  loadingTargetVmSizes: boolean,
+  replicaSchema: Field[],
+  migrationSchema: Field[],
 }
 @observer
 class AssessmentDetailsPage extends React.Component<Props, State> {
   state = {
-    sourceEndpoint: null,
-    selectedVms: [],
     selectedNetworks: [],
     showMigrationOptions: false,
     executeButtonDisabled: false,
-    vmSizes: {},
     vmSearchValue: '',
+    loadingTargetVmSizes: false,
+    replicaSchema: [],
+    migrationSchema: [],
   }
 
   componentWillMount() {
     document.title = 'Assessment Details'
-
+    let urlData: LocalData = JSON.parse(atob(decodeURIComponent(this.props.match.params.info)))
+    if (!azureStore.loadLocalData(urlData.assessmentName)) {
+      azureStore.setLocalData(urlData)
+    }
     this.azureAuthenticate()
   }
 
@@ -79,12 +85,17 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     instanceStore.clearInstancesDetails()
   }
 
+  getLocalData(): LocalData {
+    // at this point we know for sure that at least URL data is there
+    let data: any = azureStore.localData
+    return data
+  }
+
   getUrlInfo() {
-    let urlInfo = JSON.parse(atob(decodeURIComponent(this.props.match.params.info)))
-    return urlInfo
+    return JSON.parse(atob(decodeURIComponent(this.props.match.params.info)))
   }
 
-  getEndpoints() {
+  getSourceEndpoints() {
     let vms = azureStore.assessedVms
     let connectionsInfo = endpointStore.connectionsInfo
 
@@ -98,6 +109,11 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     return endpoints
   }
 
+  getTargetEndpoints() {
+    let endpoints = endpointStore.endpoints
+    return endpoints.filter(e => e.type === 'azure')
+  }
+
   getInstancesDetailsProgress() {
     let count = instanceStore.instancesDetailsCount
     if (count < 5) {
@@ -117,7 +133,8 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   }
 
   getSourceEndpointId() {
-    return this.state.sourceEndpoint ? this.state.sourceEndpoint.id : ''
+    let localData = this.getLocalData()
+    return localData.sourceEndpoint ? localData.sourceEndpoint.id : ''
   }
 
   getEnabledVms() {
@@ -143,31 +160,61 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     if (this.getFilteredAssessedVms().length === 0 || this.getEnabledVms().length === 0) {
       return false
     }
-
-    return this.state.selectedVms.length === this.getFilteredAssessedVms(this.getEnabledVms()).length
+    let selectedVms = this.getLocalData().selectedVms
+    return selectedVms.length === this.getFilteredAssessedVms(this.getEnabledVms()).length
   }
 
   handleVmSelectedChange(vm: VmItem, selected: boolean) {
+    let selectedVms = this.getLocalData().selectedVms
     if (selected) {
-      this.setState({ selectedVms: [...this.state.selectedVms, vm] }, () => { this.loadInstancesDetails() })
+      selectedVms = [
+        ...selectedVms,
+        vm.properties.datacenterMachineId,
+      ]
+      azureStore.updateSelectedVms(selectedVms)
+      this.loadInstancesDetails()
     } else {
-      this.setState({ selectedVms: this.state.selectedVms.filter(m => m.id !== vm.id) }, () => { this.loadInstancesDetails() })
+      selectedVms = this.getLocalData().selectedVms.filter(m => m !== vm.properties.datacenterMachineId)
     }
+    azureStore.updateSelectedVms(selectedVms)
+    this.loadInstancesDetails()
   }
 
   handleSelectAllVmsChange(selected: boolean) {
     let selectedVms = selected ? [...this.getFilteredAssessedVms(this.getEnabledVms())] : []
-    this.setState({ selectedVms }, () => { this.loadInstancesDetails() })
+    azureStore.updateSelectedVms(selectedVms.map(v => v.properties.datacenterMachineId))
+    this.loadInstancesDetails()
   }
 
   handleSourceEndpointChange(sourceEndpoint: ?Endpoint) {
-    this.setState({ sourceEndpoint, selectedVms: [], selectedNetworks: [] })
-    instanceStore.loadInstances(this.getSourceEndpointId(), true, true).then(() => {
+    this.setState({ selectedNetworks: [] })
+    azureStore.updateSourceEndpoint(sourceEndpoint)
+    instanceStore.loadInstances(this.getSourceEndpointId(), true, true, true).then(() => {
       this.initSelectedVms()
       this.loadInstancesDetails()
     })
   }
 
+  handleResourceGroupChange(resourceGroupName: string) {
+    azureStore.updateResourceGroup(resourceGroupName)
+    this.loadNetworks()
+    this.loadTargetVmSizes()
+  }
+
+  handleLocationChange(locationName: string) {
+    azureStore.updateLocation(locationName)
+    this.loadNetworks()
+    this.loadTargetVmSizes()
+  }
+
+  handleTargetEndpointChange(endpoint: Endpoint) {
+    azureStore.updateTargetEndpoint(endpoint)
+    this.loadTargetOptions().then(() => {
+      this.loadTargetVmSizes()
+      this.loadNetworks()
+    })
+  }
+
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'signout':
@@ -193,13 +240,21 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   }
 
   handleRefresh() {
-    let urlInfo = this.getUrlInfo()
-    azureStore.getAssessmentDetails({ ...urlInfo })
-    azureStore.getAssessedVms({ ...urlInfo })
-    this.loadInstancesDetails()
+    localStorage.removeItem('instances')
+    localStorage.removeItem(`assessments-${cookie.get('projectId') || ''}`)
+    localStorage.removeItem('instancesDetails')
+    localStorage.removeItem('networks')
+    location.reload()
   }
 
   handleMigrateClick() {
+    let endpointType = this.getLocalData().endpoint.type
+    providerStore.loadOptionsSchema(endpointType, 'replica').then(() => {
+      this.setState({ replicaSchema: providerStore.optionsSchema })
+      return providerStore.loadOptionsSchema(endpointType, 'migration')
+    }).then(() => {
+      this.setState({ migrationSchema: providerStore.optionsSchema })
+    })
     this.setState({ showMigrationOptions: true })
   }
 
@@ -207,53 +262,12 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     this.setState({ showMigrationOptions: false })
   }
 
-  handleMigrationExecute(options: Field[]) {
-    let selectedInstances = instanceStore.instancesDetails
-      .filter(i => this.state.selectedVms.find(m => i.id === `${m.properties.datacenterMachineId}`))
-    let vmSizes = {}
-    selectedInstances.forEach(i => {
-      let vm = this.state.selectedVms.find(m => i.id === `${m.properties.datacenterMachineId}`)
-      vmSizes[i.instance_name] = vm ? this.state.vmSizes[vm.id].name : ''
-    })
-
-    this.setState({ executeButtonDisabled: true })
-
-    assessmentStore.migrate({
-      source: this.state.sourceEndpoint,
-      target: this.getUrlInfo().endpoint,
-      networks: [...this.state.selectedNetworks],
-      options: [...options],
-      destinationEnv: {
-        resource_group: this.getUrlInfo().resourceGroupName,
-        location: azureStore.assessmentDetails ? azureStore.assessmentDetails.properties.azureLocation : '',
-      },
-      vmSizes,
-      selectedInstances,
-    }).then(() => {
-      this.setState({ showMigrationOptions: false })
-      let useReplicaOption = options.find(o => o.name === 'use_replica')
-      let type = useReplicaOption && useReplicaOption.value ? 'Replica' : 'Migration'
-      notificationStore.alert(`${type} was succesfully created`, 'success')
-
-      if (type === 'Replica') {
-        assessmentStore.migrations.forEach(replica => {
-          replicaStore.execute(replica.id, options)
-        })
-      }
-
-      window.location.href = `/#/${type.toLowerCase()}s`
-    })
-  }
-
-  handleVmSizeChange(vm: VmItem, vmSize: VmSize) {
-    let vmSizes = this.state.vmSizes
-    vmSizes[vm.id] = vmSize
-
-    this.setState({ vmSizes })
+  handleVmSizeChange(vmId: string, vmSize: string) {
+    azureStore.updateVmSize(vmId, vmSize)
   }
 
-  handleGetVmSize(vm: VmItem): VmSize {
-    return this.state.vmSizes[vm.id]
+  handleGetVmSize(vm: VmItem): string {
+    return this.getLocalData().selectedVmSizes[vm.properties.datacenterMachineId]
   }
 
   handleVmSearchValueChange(vmSearchValue: string) {
@@ -267,62 +281,156 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     })
   }
 
-  loadSourceEndpoints() {
+  loadEndpoints() {
     endpointStore.getEndpoints({ showLoading: true }).then(() => {
-      endpointStore.getConnectionsInfo(endpointStore.endpoints.filter(e => e.type === 'vmware_vsphere')).then(() => {
-        let endpoints = this.getEndpoints()
-        let sourceEndpoint = endpoints.find(e => e.id === this.getSourceEndpointId())
-        if (sourceEndpoint) {
-          this.handleSourceEndpointChange(sourceEndpoint)
-        } else if (endpoints.length > 0) {
-          this.handleSourceEndpointChange(endpoints[0])
-        } else {
-          this.handleSourceEndpointChange(null)
-        }
-      })
+      this.loadSourceEndpointsInfo()
+    })
+  }
+
+  loadSourceEndpointsInfo() {
+    endpointStore.getConnectionsInfo(endpointStore.endpoints.filter(e => e.type === 'vmware_vsphere')).then(() => {
+      let endpoints = this.getSourceEndpoints()
+      let sourceEndpoint = endpoints.find(e => e.id === this.getSourceEndpointId())
+      if (sourceEndpoint) {
+        this.handleSourceEndpointChange(sourceEndpoint)
+      } else if (endpoints.length > 0) {
+        this.handleSourceEndpointChange(endpoints[0])
+      } else {
+        this.handleSourceEndpointChange(null)
+      }
     })
   }
 
   loadAssessmentDetails() {
     let urlInfo = this.getUrlInfo()
     azureStore.getAssessmentDetails({ ...urlInfo }).then(() => {
-      azureStore.getVmSizes({ ...urlInfo, location: azureStore.assessmentDetails ? azureStore.assessmentDetails.properties.azureLocation : '' })
+      let location = azureStore.assessmentDetails ? azureStore.assessmentDetails.properties.azureLocation : ''
+      azureStore.setLocation(location)
+      // azureStore.getVmSizes({ ...urlInfo, location })
       this.loadNetworks()
+      this.loadTargetOptions()
+      this.loadTargetVmSizes()
     })
+
     azureStore.getAssessedVms({ ...urlInfo }).then(() => {
       this.initVmSizes()
-      this.loadSourceEndpoints()
+      this.loadEndpoints()
+    })
+  }
+
+  loadTargetOptions(): Promise<void> {
+    let localData = this.getLocalData()
+    return providerStore.getDestinationOptions(localData.endpoint.id, localData.endpoint.type).then(options => {
+      let locations = options.find(o => o.name === 'location')
+      if (locations && locations.values) {
+        let localDataFind = locations.values.find(l => l.id === localData.locationName)
+        if (!localDataFind) {
+          azureStore.updateLocation(locations.values[0].id)
+        }
+
+        azureStore.saveLocations(locations.values)
+      }
+      let resourceGroups = options.find(o => o.name === 'resource_group')
+      if (resourceGroups && resourceGroups.values) {
+        let localDataFind = resourceGroups.values.find(g => g === localData.resourceGroupName)
+        if (!localDataFind) {
+          azureStore.updateResourceGroup(resourceGroups.values[0])
+        }
+        azureStore.saveResourceGroups(resourceGroups.values)
+      }
+    })
+  }
+
+  loadTargetVmSizes() {
+    let localData = this.getLocalData()
+    this.setState({ loadingTargetVmSizes: true })
+    providerStore.getDestinationOptions(localData.endpoint.id, localData.endpoint.type, {
+      location: localData.locationName,
+      resource_group: localData.resourceGroupName,
+    }).then(options => {
+      let vmSizes = options.find(o => o.name === 'vm_size')
+      if (vmSizes && vmSizes.values) {
+        azureStore.saveTargetVmSizes(vmSizes.values)
+      }
+      this.setState({ loadingTargetVmSizes: false })
     })
   }
 
   initSelectedVms() {
-    this.setState({ selectedVms: this.getEnabledVms() })
+    let localData = this.getLocalData()
+    let enabledVms = this.getEnabledVms().map(vm => vm.properties.datacenterMachineId)
+    if (localData.selectedVms.length === 0) {
+      azureStore.updateSelectedVms(enabledVms)
+    } else {
+      azureStore.updateSelectedVms(enabledVms.filter(id => localData.selectedVms.find(i => i === id)))
+    }
   }
 
   initVmSizes() {
     let vmSizes = {}
     let vms = azureStore.assessedVms
+    let localData = this.getLocalData()
 
     vms.forEach(vm => {
-      vmSizes[vm.id] = { name: vm.properties.recommendedSize }
+      vmSizes[vm.properties.datacenterMachineId] = localData.selectedVmSizes[vm.properties.datacenterMachineId] || vm.properties.recommendedSize || 'auto'
     })
-
-    this.setState({ vmSizes })
+    azureStore.updateVmSizes(vmSizes)
   }
 
   loadNetworks() {
+    let localData = this.getLocalData()
     this.setState({ selectedNetworks: [] })
-    let details = azureStore.assessmentDetails
-    networkStore.loadNetworks(this.getUrlInfo().endpoint.id, {
-      location: details ? details.properties.azureLocation : '',
-      resource_group: this.getUrlInfo().resourceGroupName,
-    })
+    networkStore.loadNetworks(localData.endpoint.id, {
+      location: localData.locationName,
+      resource_group: localData.resourceGroupName,
+    }, true)
   }
 
   loadInstancesDetails() {
-    let instances = instanceStore.instances.filter(i => this.state.selectedVms.find(m => i.id === `${m.properties.datacenterMachineId}`))
+    let selectedVms = this.getLocalData().selectedVms
+    let instances = instanceStore.instances.filter(i => selectedVms.find(m => i.id === m))
     instanceStore.clearInstancesDetails()
-    instanceStore.loadInstancesDetails(this.getSourceEndpointId(), instances)
+    instanceStore.loadInstancesDetails(this.getSourceEndpointId(), instances, true)
+  }
+
+  handleMigrationExecute(fieldValues: { [string]: any }) {
+    let selectedVms = this.getLocalData().selectedVms
+    let selectedInstances = instanceStore.instancesDetails.filter(i => selectedVms.find(m => i.id === m))
+    let vmSizes = {}
+    let localData = this.getLocalData()
+    selectedInstances.forEach(i => {
+      let vm = selectedVms.find(m => i.id === m)
+      let selectedVmSize = localData.selectedVmSizes[i.id]
+      if (vm && azureStore.vmSizes.find(s => s === selectedVmSize)) {
+        vmSizes[i.instance_name] = selectedVmSize
+      }
+    })
+
+    this.setState({ executeButtonDisabled: true })
+
+    fieldValues.resource_group = localData.resourceGroupName
+    fieldValues.location = localData.locationName
+
+    assessmentStore.migrate({
+      source: localData.sourceEndpoint,
+      target: localData.endpoint,
+      networks: [...this.state.selectedNetworks],
+      fieldValues,
+      vmSizes,
+      selectedInstances,
+    }).then(() => {
+      this.setState({ showMigrationOptions: false })
+      let type = fieldValues.use_replica ? 'Replica' : 'Migration'
+      notificationStore.alert(`${type} was succesfully created`, 'success')
+
+      if (type === 'Replica') {
+        assessmentStore.migrations.forEach(replica => {
+          replicaStore.execute(replica.id, [{ name: 'shutdown_instances', value: fieldValues.shutdown_instances || false }])
+        })
+      }
+
+      window.location.href = `/#/${type.toLowerCase()}s`
+    })
   }
 
   render() {
@@ -331,6 +439,7 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     let endpointsLoading = endpointStore.connectionsInfoLoading || endpointStore.loading
     let status = details ? details.properties.status.toUpperCase() : ''
     let statusLabel = status === 'COMPLETED' ? 'READY' : status
+    let localData = this.getLocalData()
 
     return (
       <Wrapper>
@@ -356,34 +465,41 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
             <AssessmentDetailsContent
               item={details}
               detailsLoading={loading}
-              targetEndpoint={this.getUrlInfo().endpoint}
-              sourceEndpoints={this.getEndpoints()}
+              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
+              instancesDetailsProgress={this.getInstancesDetailsProgress()}
+              instancesLoading={instanceStore.instancesLoading}
+              networksLoading={networkStore.loading}
+              targetEndpointsLoading={endpointStore.loading}
+              loadingVmSizes={this.state.loadingTargetVmSizes}
               sourceEndpointsLoading={endpointsLoading}
-              sourceEndpoint={this.state.sourceEndpoint}
+              targetOptionsLoading={providerStore.destinationOptionsLoading}
+              targetEndpoints={this.getTargetEndpoints()}
+              targetEndpoint={localData.endpoint}
+              onTargetEndpointChange={endpoint => { this.handleTargetEndpointChange(endpoint) }}
+              sourceEndpoints={this.getSourceEndpoints()}
+              sourceEndpoint={localData.sourceEndpoint}
+              locations={azureStore.locations}
+              selectedLocation={localData.locationName}
+              onLocationChange={locationName => { this.handleLocationChange(locationName) }}
+              selectedResourceGroup={localData.resourceGroupName}
+              resourceGroups={azureStore.coriolisResourceGroups}
+              onResourceGroupChange={resourceGroupName => { this.handleResourceGroupChange(resourceGroupName) }}
               assessedVmsCount={azureStore.assessedVms.length}
               filteredAssessedVms={this.getFilteredAssessedVms()}
               onSourceEndpointChange={endpoint => this.handleSourceEndpointChange(endpoint)}
-              selectedVms={this.state.selectedVms}
+              selectedVms={localData.selectedVms}
               onVmSelectedChange={(vm, selected) => { this.handleVmSelectedChange(vm, selected) }}
               selectAllVmsChecked={this.getSelectAllVmsChecked()}
               onSelectAllVmsChange={checked => { this.handleSelectAllVmsChange(checked) }}
               instances={instanceStore.instances}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
-              instancesLoading={instanceStore.instancesLoading}
-              networksLoading={networkStore.loading}
-              instancesDetailsProgress={this.getInstancesDetailsProgress()}
               networks={networkStore.networks}
               selectedNetworks={this.state.selectedNetworks}
-              loadingVmSizes={azureStore.loadingVmSizes}
               vmSizes={azureStore.vmSizes}
-              onVmSizeChange={(vm, size) => {
-                // $FlowIgnore
-                this.handleVmSizeChange(vm, size)
-              }}
+              onVmSizeChange={(vmId, size) => { this.handleVmSizeChange(vmId, size) }}
+              onGetSelectedVmSize={vm => this.handleGetVmSize(vm)}
               vmSearchValue={this.state.vmSearchValue}
               onVmSearchValueChange={value => { this.handleVmSearchValueChange(value) }}
-              onGetVmSize={vm => this.handleGetVmSize(vm)}
               onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
               onRefresh={() => this.handleRefresh()}
               onMigrateClick={() => { this.handleMigrateClick() }}
@@ -397,7 +513,9 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
         >
           <AssessmentMigrationOptions
             onCancelClick={() => { this.handleCloseMigrationOptions() }}
-            onExecuteClick={options => { this.handleMigrationExecute(options) }}
+            onExecuteClick={fieldValues => { this.handleMigrationExecute(fieldValues) }}
+            replicaSchema={this.state.replicaSchema}
+            migrationSchema={this.state.migrationSchema}
             executeButtonDisabled={this.state.executeButtonDisabled}
           />
         </Modal>

+ 2 - 2
src/components/pages/AssessmentsPage/AssessmentsPage.jsx

@@ -88,7 +88,7 @@ class AssessmentsPage extends React.Component<Props, State> {
   }
 
   getResourceGroupsDropdownConfig() {
-    let groups = azureStore.resourceGroups
+    let groups = azureStore.assessmentResourceGroups
     return {
       key: 'resource-group',
       selectedItem: assessmentStore.selectedResourceGroup ? assessmentStore.selectedResourceGroup.id : '',
@@ -227,7 +227,7 @@ class AssessmentsPage extends React.Component<Props, State> {
       azureStore.authenticate(connectionInfo.user_credentials.username, connectionInfo.user_credentials.password).then(() => {
         // $FlowIgnore
         azureStore.getResourceGroups(connectionInfo.subscription_id).then(() => {
-          let groups = azureStore.resourceGroups
+          let groups = azureStore.assessmentResourceGroups
           let selectedGroup = assessmentStore.selectedResourceGroup
           // $FlowIssue
           if (groups.filter(rg => rg.id === selectedGroup ? selectedGroup.id : '').length > 0) {

+ 18 - 14
src/sources/AssessmentSource.js

@@ -22,23 +22,32 @@ import notificationStore from '../stores/NotificationStore'
 
 class AssessmentSourceUtils {
   static getDestinationEnv(data: MigrationInfo) {
-    let env = { ...data.destinationEnv }
-    env.network_map = {}
+    let env = {}
     if (data.networks && data.networks.length) {
+      env.network_map = {}
       data.networks.forEach(mapping => {
         env.network_map[mapping.sourceNic.network_name] = mapping.targetNetwork.name
       })
     }
-    env.vm_size = data.vmSizes[Object.keys(data.vmSizes).filter(k => k === data.selectedInstances[0].instance_name)[0]]
+    let vmSize = data.vmSizes[Object.keys(data.vmSizes).filter(k => k === data.selectedInstances[0].instance_name)[0]]
+    if (vmSize) {
+      env.vm_size = vmSize
+    }
+    let skipFields = ['use_replica', 'separate_vm', 'shutdown_instances', 'skip_os_morphing']
+    Object.keys(data.fieldValues).filter(f => !skipFields.find(sf => sf === f)).forEach(fieldName => {
+      if (data.fieldValues[fieldName] != null) {
+        env[fieldName] = data.fieldValues[fieldName]
+      }
+    })
+
     return env
   }
 }
 
 class AssessmentSource {
   static migrate(data: MigrationInfo): Promise<MainItem> {
-    let useReplicaField = data.options.find(o => o.name === 'use_replica')
-    let type = useReplicaField && useReplicaField.value ? 'replica' : 'migration'
-    let payload = {}
+    let type = data.fieldValues.use_replica ? 'replica' : 'migration'
+    let payload: any = {}
     payload[type] = {
       origin_endpoint_id: data.source ? data.source.id : 'null',
       destination_endpoint_id: data.target.id,
@@ -48,14 +57,9 @@ class AssessmentSource {
       security_groups: ['testgroup'],
     }
 
-    data.options.forEach(option => {
-      if (option.name === 'use_replica') {
-        return
-      }
-      if (option.value != null) {
-        payload[type][option.name] = option.value
-      }
-    })
+    if (type === 'migration') {
+      payload[type].skip_os_morphing = data.fieldValues.skip_os_morphing
+    }
 
     return Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/${type}s`,

+ 10 - 2
src/sources/EndpointSource.js

@@ -89,6 +89,9 @@ class EdnpointSource {
     return Promise.all(endpoints.map(endpoint => {
       let index = endpoint.connection_info.secret_ref ? endpoint.connection_info.secret_ref.lastIndexOf('/') : ''
       let uuid = endpoint.connection_info.secret_ref && index ? endpoint.connection_info.secret_ref.substr(index + 1) : ''
+      if (!uuid) {
+        return Promise.resolve({ ...endpoint })
+      }
       return Api.send({
         url: `${servicesUrl.barbican}/v1/secrets/${uuid}/payload`,
         responseType: 'text',
@@ -171,7 +174,7 @@ class EdnpointSource {
   }
 
   static add(endpoint: Endpoint, skipSchemaParser: boolean = false): Promise<Endpoint> {
-    let parsedEndpoint = skipSchemaParser ? { ...endpoint } : SchemaParser.fieldsToPayload(endpoint)
+    let parsedEndpoint: any = skipSchemaParser ? { ...endpoint } : SchemaParser.fieldsToPayload(endpoint)
     let newEndpoint: any = {}
     let connectionInfo = {}
     if (useSecret) {
@@ -216,7 +219,12 @@ class EdnpointSource {
     return Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints`,
       method: 'POST',
-      data: { endpoint: parsedEndpoint },
+      data: {
+        endpoint: {
+          ...parsedEndpoint,
+          type: endpoint.type,
+        },
+      },
     }).then(response => {
       return response.data.endpoint
     })

+ 1 - 6
src/stores/AssessmentStore.js

@@ -36,14 +36,9 @@ class AssessmentStore {
   }
 
   @action migrate(data: MigrationInfo): Promise<void> {
-    if (!data.options) {
-      return Promise.resolve()
-    }
-
     this.migrating = true
     this.migrations = []
-    let seperateVmField = data.options.find(o => o.name === 'separate_vm')
-    let separateVm = seperateVmField ? seperateVmField.value : ''
+    let separateVm = data.fieldValues.separate_vm
 
     if (separateVm) {
       return AssessmentSource.migrateMultiple(data).then((items: MainItem[]) => {

+ 153 - 14
src/stores/AzureStore.js

@@ -18,12 +18,47 @@ import { observable, action } from 'mobx'
 import cookie from 'js-cookie'
 
 import AzureSource from '../sources/AzureSource'
-import type { Assessment, VmItem, VmSize } from '../types/Assessment'
+import type { Assessment, VmItem, Location } from '../types/Assessment'
+import type { NetworkMap } from '../types/Network'
+import type { Endpoint } from '../types/Endpoint'
+
+export type LocalData = {
+  endpoint: Endpoint,
+  sourceEndpoint: ?Endpoint,
+  connectionInfo: any,
+  resourceGroupName: string,
+  locationName: string,
+  assessmentName: string,
+  groupName: string,
+  projectName: string,
+  selectedVmSizes: { [string]: string },
+  selectedVms: string[],
+  selectedNetworks: NetworkMap[],
+  [string]: mixed,
+}
+
+class AzureLocalStorage {
+  static loadLocalData(assessmentName: string): ?LocalData {
+    let localDataArray: LocalData[] = JSON.parse(localStorage.getItem(`assessments-${cookie.get('projectId') || ''}`) || '[]')
+    return localDataArray.find(a => a.assessmentName === assessmentName)
+  }
+
+  static setLocalData(data: LocalData) {
+    let localDataArray: LocalData[] = JSON.parse(localStorage.getItem(`assessments-${cookie.get('projectId') || ''}`) || '[]')
+    let assessmentIndex = localDataArray.findIndex(a => a.assessmentName === data.assessmentName)
+    if (assessmentIndex > -1) {
+      localDataArray.splice(assessmentIndex, 1)
+    }
+    localDataArray.push(data)
+    localStorage.setItem(`assessments-${cookie.get('projectId') || ''}`, JSON.stringify(localDataArray))
+  }
+}
 
 class AzureStore {
   @observable authenticating: boolean = false
   @observable loadingResourceGroups: boolean = false
-  @observable resourceGroups: $PropertyType<Assessment, 'group'>[] = []
+  @observable assessmentResourceGroups: $PropertyType<Assessment, 'group'>[] = []
+  @observable coriolisResourceGroups: string[] = []
   @observable loadingAssessments: boolean = false
   @observable loadingAssessmentDetails: boolean = false
   @observable assessmentDetails: ?Assessment = null
@@ -31,8 +66,91 @@ class AzureStore {
   @observable loadingAssessedVms: boolean = false
   @observable assessedVms: VmItem[] = []
   @observable loadingVmSizes: boolean = false
-  @observable vmSizes: VmSize[] = []
+  // @observable vmSizes: VmSize[] = []
   @observable assessmentsProjectId: string = ''
+  @observable locations: Location[] = []
+  @observable localData: ?LocalData = null
+  @observable vmSizes: string[] = []
+
+  @action loadLocalData(assessmentName: string): boolean {
+    this.localData = AzureLocalStorage.loadLocalData(assessmentName)
+    return Boolean(this.localData)
+  }
+
+  @action setLocalData(data: LocalData) {
+    data.selectedVmSizes = data.selectedVmSizes || {}
+    data.selectedVms = data.selectedVms || []
+    data.selectedNetworks = data.selectedNetworks || []
+
+    this.localData = data
+    AzureLocalStorage.setLocalData(data)
+  }
+
+  @action updateResourceGroup(resourceGroupName: string) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.resourceGroupName = resourceGroupName
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateNetworkMap(selectedNetworks: NetworkMap[]) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.selectedNetworks = selectedNetworks
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateSourceEndpoint(sourceEndpoint: ?Endpoint) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.sourceEndpoint = sourceEndpoint
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateSelectedVms(selectedVms: string[]) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.selectedVms = selectedVms
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateVmSize(vmId: string, vmSize: string) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.selectedVmSizes[vmId] = vmSize
+    if (this.localData) {
+      AzureLocalStorage.setLocalData(this.localData)
+    }
+  }
+
+  @action updateVmSizes(vmSizes: { [string]: string }) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.selectedVmSizes = vmSizes
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateLocation(locationName: string) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.locationName = locationName
+    AzureLocalStorage.setLocalData(this.localData)
+  }
+
+  @action updateTargetEndpoint(endpoint: Endpoint) {
+    if (!this.localData) {
+      return
+    }
+    this.localData.endpoint = endpoint
+    AzureLocalStorage.setLocalData(this.localData)
+  }
 
   @action authenticate(username: string, password: string): Promise<void> {
     this.authenticating = true
@@ -49,7 +167,7 @@ class AzureStore {
 
     return AzureSource.getResourceGroups(subscriptionId).then((groups: $PropertyType<Assessment, 'group'>[]) => {
       this.loadingResourceGroups = false
-      this.resourceGroups = groups
+      this.assessmentResourceGroups = groups
     }).catch(() => {
       this.loadingResourceGroups = false
     })
@@ -95,6 +213,25 @@ class AzureStore {
     })
   }
 
+  @action saveLocations(locations: Location[]) {
+    this.locations = locations
+  }
+
+  @action saveResourceGroups(resourceGroups: string[]) {
+    this.coriolisResourceGroups = resourceGroups
+  }
+
+  @action saveTargetVmSizes(targetVmSizes: string[]) {
+    this.vmSizes = targetVmSizes
+  }
+
+  @action setLocation(location: string) {
+    if (!this.localData || this.localData.locationName) {
+      return
+    }
+    this.localData.locationName = location
+  }
+
   @action clearAssessmentDetails() {
     this.assessmentDetails = null
     this.assessedVms = []
@@ -111,24 +248,26 @@ class AzureStore {
     })
   }
 
-  @action getVmSizes(info: Assessment): Promise<void> {
-    this.loadingVmSizes = true
+  // @action getVmSizes(info: Assessment): Promise<void> {
+  //   this.loadingVmSizes = true
 
-    return AzureSource.getVmSizes(info).then((sizes: VmSize[]) => {
-      this.loadingVmSizes = false
-      this.vmSizes = sizes
-    }).catch(() => {
-      this.loadingVmSizes = false
-    })
-  }
+  //   return AzureSource.getVmSizes(info).then((sizes: VmSize[]) => {
+  //     this.loadingVmSizes = false
+  //     this.vmSizes = sizes
+  //   }).catch(() => {
+  //     this.loadingVmSizes = false
+  //   })
+  // }
 
   @action clearAssessedVms() {
     this.assessedVms = []
   }
 
   @action clearAssessments() {
-    this.resourceGroups = []
+    this.assessmentResourceGroups = []
     this.assessments = []
+    this.locations = []
+    this.coriolisResourceGroups = []
   }
 }
 

+ 90 - 3
src/stores/InstanceStore.js

@@ -20,6 +20,67 @@ import { wizardConfig } from '../config'
 import type { Instance } from '../types/Instance'
 import InstanceSource from '../sources/InstanceSource'
 
+class InstanceLocalStorage {
+  static saveInstancesToLocalStorage(endpointId: string, instances: Instance[]) {
+    let instancesLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instances') || '[]')
+    let endpointIndex = instancesLocalStorage.findIndex(i => i.endpointId === endpointId)
+    if (endpointIndex > -1) {
+      instancesLocalStorage.splice(endpointIndex, 1)
+    }
+    instancesLocalStorage.push({ endpointId, instances })
+    localStorage.setItem('instances', JSON.stringify(instancesLocalStorage))
+  }
+
+  static loadInstancesFromLocalStorage(endpointId: string): ?Instance[] {
+    let instancesLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instances') || '[]')
+    let endpointInstances = instancesLocalStorage.find(i => i.endpointId === endpointId)
+    if (!endpointInstances) {
+      return null
+    }
+    return endpointInstances.instances
+  }
+
+  static saveDetailsToLocalStorage(endpointId: string, instance: Instance) {
+    let instancesDetailsLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instancesDetails') || '[]')
+    let endpointInstancesIndex = instancesDetailsLocalStorage.findIndex(i => i.endpointId === endpointId)
+    let endpointDetails = { endpointId, instances: [] }
+    if (endpointInstancesIndex > -1) {
+      endpointDetails = instancesDetailsLocalStorage[endpointInstancesIndex]
+      instancesDetailsLocalStorage.splice(endpointInstancesIndex, 1)
+    }
+
+    let localInstanceIndex = endpointDetails.instances.findIndex(i => i.id === instance.id)
+    if (localInstanceIndex > -1) {
+      endpointDetails.instances.splice(localInstanceIndex, 1)
+    }
+    endpointDetails.instances.push(instance)
+    instancesDetailsLocalStorage.push(endpointDetails)
+    localStorage.setItem('instancesDetails', JSON.stringify(instancesDetailsLocalStorage))
+  }
+
+  static loadDetailsFromLocalStorage(endpointId: string, instancesInfo: Instance[]): ?Instance[] {
+    let instancesDetailsLocalStorage: { endpointId: string, instances: Instance[] }[] = JSON.parse(localStorage.getItem('instancesDetails') || '[]')
+    let endpointStorage = instancesDetailsLocalStorage.find(i => i.endpointId === endpointId)
+    if (!endpointStorage || !endpointStorage.instances) {
+      return null
+    }
+    let isValid = true
+    let instances: Instance[] = []
+    instancesInfo.forEach(instance => {
+      let storageInstance = endpointStorage.instances.find(i => instance.id === i.id)
+      if (storageInstance) {
+        instances.push(storageInstance)
+      } else {
+        isValid = false
+      }
+    })
+    if (isValid) {
+      return instances
+    }
+    return null
+  }
+}
+
 class InstanceStoreUtils {
   static hasNextPage(instances) {
     let result = false
@@ -63,7 +124,7 @@ class InstanceStore {
   lastEndpointId: string
   reqId: number
 
-  @action loadInstances(endpointId: string, skipLimit?: boolean, useCache?: boolean): Promise<void> {
+  @action loadInstances(endpointId: string, skipLimit?: boolean, useCache?: boolean, useLocalStorage?: boolean): Promise<void> {
     if (this.cachedInstances.length > 0 && this.lastEndpointId === endpointId && useCache) {
       return Promise.resolve()
     }
@@ -72,6 +133,18 @@ class InstanceStore {
     this.searchNotFound = false
     this.lastEndpointId = endpointId
 
+    if (useLocalStorage) {
+      let endpointInstances = InstanceLocalStorage.loadInstancesFromLocalStorage(endpointId)
+      if (endpointInstances) {
+        this.currentPage = 1
+        this.hasNextPage = false
+        this.instances = endpointInstances
+        this.cachedInstances = endpointInstances
+        this.instancesLoading = false
+        return Promise.resolve()
+      }
+    }
+
     return InstanceSource.loadInstances(endpointId, null, null, skipLimit).then(instances => {
       if (endpointId !== this.lastEndpointId) {
         return
@@ -82,6 +155,8 @@ class InstanceStore {
       this.instances = instances
       this.cachedInstances = instances
       this.instancesLoading = false
+
+      InstanceLocalStorage.saveInstancesToLocalStorage(endpointId, instances)
     }).catch(() => {
       if (endpointId !== this.lastEndpointId) {
         return
@@ -162,7 +237,7 @@ class InstanceStore {
     })
   }
 
-  @action loadInstancesDetails(endpointId: string, instancesInfo: Instance[]): Promise<void> {
+  @action loadInstancesDetails(endpointId: string, instancesInfo: Instance[], useLocalStorage?: boolean): Promise<void> {
     // Use reqId to be able to uniquely identify the request so all but the latest request can be igonred and canceled
     this.reqId = !this.reqId ? 1 : this.reqId + 1
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
@@ -179,7 +254,17 @@ class InstanceStore {
     this.loadingInstancesDetails = true
     this.instancesDetailsCount = count
     this.instancesDetailsRemaining = count
-    this.instancesDetails = []
+
+    if (useLocalStorage) {
+      let storageInstances = InstanceLocalStorage.loadDetailsFromLocalStorage(endpointId, instancesInfo)
+      if (storageInstances) {
+        this.loadingInstancesDetails = false
+        this.instancesDetails = storageInstances
+        this.loadingInstancesDetails = false
+        this.instancesDetailsRemaining = 0
+        return Promise.resolve()
+      }
+    }
 
     return new Promise((resolve) => {
       instancesInfo.forEach(instanceInfo => {
@@ -195,6 +280,8 @@ class InstanceStore {
             this.instancesDetails = this.instancesDetails.filter(i => i.id !== resp.instance.id)
           }
 
+          InstanceLocalStorage.saveDetailsToLocalStorage(endpointId, resp.instance)
+
           this.instancesDetails = [
             ...this.instancesDetails,
             resp.instance,

+ 35 - 1
src/stores/NetworkStore.js

@@ -18,23 +18,57 @@ import { observable, action } from 'mobx'
 import type { Network } from '../types/Network'
 import NetworkSource from '../sources/NetworkSource'
 
+class NetworkLocalStorage {
+  static loadNetworksFromStorage(id: string): ?Network[] {
+    let networkStorage: { id: string, networks: Network[] }[] = JSON.parse(localStorage.getItem('networks') || '[]')
+    let endpointNetworks = networkStorage.find(n => n.id === id)
+    if (!endpointNetworks) {
+      return null
+    }
+    return endpointNetworks.networks
+  }
+
+  static saveNetworksToLocalStorage(id: string, networks: Network[]) {
+    let networkStorage: { id: string, networks: Network[] }[] = JSON.parse(localStorage.getItem('networks') || '[]')
+    let endpointNetworksIndex = networkStorage.findIndex(n => n.id === id)
+    if (endpointNetworksIndex > -1) {
+      networkStorage.splice(endpointNetworksIndex, 1)
+    }
+    networkStorage.push({ id, networks })
+    localStorage.setItem('networks', JSON.stringify(networkStorage))
+  }
+}
+
 class NetworkStore {
   @observable networks: Network[] = []
   @observable loading: boolean = false
 
   cachedId: string = ''
 
-  @action loadNetworks(endpointId: string, environment: ?{ [string]: mixed }): Promise<void> {
+  @action loadNetworks(endpointId: string, environment: ?{ [string]: mixed }, useLocalStorage?: boolean): Promise<void> {
     let id = `${endpointId}-${btoa(JSON.stringify(environment))}`
     if (this.cachedId === id) {
       return Promise.resolve()
     }
 
     this.loading = true
+
+    if (useLocalStorage) {
+      let networkStorage = NetworkLocalStorage.loadNetworksFromStorage(id)
+      if (networkStorage) {
+        this.loading = false
+        this.networks = networkStorage
+        this.cachedId = id
+        return Promise.resolve()
+      }
+    }
+
     return NetworkSource.loadNetworks(endpointId, environment).then((networks: Network[]) => {
       this.loading = false
       this.networks = networks
       this.cachedId = id
+
+      NetworkLocalStorage.saveNetworksToLocalStorage(id, networks)
     }).catch(() => {
       this.loading = false
     })

+ 7 - 3
src/stores/ProviderStore.js

@@ -74,19 +74,23 @@ class ProviderStore {
     })
   }
 
-  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }): Promise<void> {
+  @action getDestinationOptions(endpointId: string, provider: string, envData?: { [string]: mixed }): Promise<DestinationOption[]> {
     let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p === 'string' ? p === provider : p.name === provider)
     if (!providerWithExtraOptions) {
-      return Promise.resolve()
+      return Promise.resolve([])
     }
 
     this.destinationOptionsLoading = true
+    this.destinationOptions = []
+    let destOptions = []
+
     return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
       this.optionsSchema.forEach(field => {
         const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
         parser.fillFieldValues(field, options)
       })
       this.destinationOptions = options
+      destOptions = options
       this.destinationOptionsLoading = false
     }).catch(err => {
       console.error(err)
@@ -97,8 +101,8 @@ class ProviderStore {
       }
       return this.loadOptionsSchema(provider, this.lastOptionsSchemaType)
     }).then(() => {
-      this.destinationOptions = []
       this.destinationOptionsLoading = false
+      return destOptions
     })
   }
 }

+ 13 - 7
src/types/Assessment.js

@@ -14,7 +14,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
-import type { Field } from './Field'
 import type { Endpoint } from './Endpoint'
 import type { Instance } from './Instance'
 import type { NetworkMap } from './Network'
@@ -24,6 +23,16 @@ export type VmSize = {
   size?: string,
 }
 
+export type Location = {
+  id: string,
+  name: string,
+}
+
+export type Group = {
+  id: string,
+  name: string,
+}
+
 export type VmItem = {
   id: string,
   properties: {
@@ -55,24 +64,21 @@ export type Assessment = {
   project: {
     name: string,
   },
-  group: {
-    name: string,
-    id: string,
-  },
+  group: Group,
   properties: {
     status: string,
     updatedTimestamp: string,
     azureLocation: string,
+    numberOfMachines: string,
   },
   connectionInfo: { subscription_id: string } & $PropertyType<Endpoint, 'connection_info'>,
 }
 
 export type MigrationInfo = {
-  options: Field[],
   source: ?Endpoint,
   target: Endpoint,
   selectedInstances: Instance[],
-  destinationEnv: { [string]: mixed },
+  fieldValues: { [string]: any },
   networks: NetworkMap[],
   vmSizes: { [string]: VmSize },
 }

+ 1 - 1
webpack.config.js

@@ -110,7 +110,7 @@ const config = createConfig([
     }),
     splitVendor(),
     addPlugins([
-      new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }),
+      new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, mangle: { keep_fnames: true } }),
     ]),
   ]),
 ])