Просмотр исходного кода

Make assessment data editable and store it locally

Implement editable target endpoint, location and resource group.

Store all the newly updated-able fields locally so there are the same
every time the assessment is loaded.

Includes logic to make sure the VM sizes are compatible with target
endpoint.

Add total machines label to assessment list item.

Store instances and networks in `localStorage` for a faster page load
if the source endpoint has been used previously.

Clicking the bottom `Refresh` button forces data to be loaded from the
server instead of loading it locally.

Add migration options to azure migrate popup.

Update the assessment loading animations.

Allow the use of endpoints without barbican secret.

Don't show canceled requests as errors in production.
Sergiu Miclea 7 лет назад
Родитель
Сommit
a1433d9d33

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