Parcourir la source

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 il y a 7 ans
Parent
commit
a1433d9d33

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

@@ -26,12 +26,13 @@ const Wrapper = styled.div`
   height: 16px;
   height: 16px;
   background: url('${props => props.warning ? warningImage : questionImage}') center no-repeat;
   background: url('${props => props.warning ? warningImage : questionImage}') center no-repeat;
   display: inline-block;
   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 = {
 type Props = {
   text: string,
   text: string,
   marginLeft?: number,
   marginLeft?: number,
+  marginBottom?: number,
   className?: string,
   className?: string,
   marginLeft?: number,
   marginLeft?: number,
   warning?: boolean,
   warning?: boolean,
@@ -43,6 +44,7 @@ class InfoIcon extends React.Component<Props> {
       <Wrapper
       <Wrapper
         data-tip={this.props.text}
         data-tip={this.props.text}
         marginLeft={this.props.marginLeft}
         marginLeft={this.props.marginLeft}
+        marginBottom={this.props.marginBottom}
         className={this.props.className}
         className={this.props.className}
         warning={this.props.warning}
         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 Checkbox from '../../atoms/Checkbox'
 import InfoIcon from '../../atoms/InfoIcon'
 import InfoIcon from '../../atoms/InfoIcon'
 import DropdownLink from '../../molecules/DropdownLink'
 import DropdownLink from '../../molecules/DropdownLink'
-import type { VmItem, VmSize } from '../../../types/Assessment'
+import type { VmItem } from '../../../types/Assessment'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 
 
@@ -68,9 +68,9 @@ type Props = {
   onSelectedChange: (item: VmItem, isChecked: boolean) => void,
   onSelectedChange: (item: VmItem, isChecked: boolean) => void,
   disabled: boolean,
   disabled: boolean,
   loadingVmSizes: boolean,
   loadingVmSizes: boolean,
-  vmSizes: VmSize[],
-  onVmSizeChange: (size: VmSize) => void,
-  selectedVmSize: ?VmSize,
+  vmSizes: string[],
+  onVmSizeChange: (size: string) => void,
+  selectedVmSize: ?string,
   recommendedVmSize: string,
   recommendedVmSize: string,
 }
 }
 @observer
 @observer
@@ -118,11 +118,12 @@ class AssessedVmListItem extends React.Component<Props> {
             <DropdownLink
             <DropdownLink
               searchable
               searchable
               width="208px"
               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"
               listWidth="200px"
-              onChange={item => { this.props.onVmSizeChange(item.size) }}
+              onChange={item => { this.props.onVmSizeChange(item.value) }}
               disabled={this.props.disabled}
               disabled={this.props.disabled}
               highlightedItem={this.props.recommendedVmSize}
               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 React from 'react'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 import styled from 'styled-components'
-import moment from 'moment'
 
 
 import StatusPill from '../../atoms/StatusPill'
 import StatusPill from '../../atoms/StatusPill'
 
 
@@ -85,12 +84,13 @@ const AssessmentLabel = styled.div`
   color: ${Palette.grayscale[4]};
   color: ${Palette.grayscale[4]};
   width: 64px;
   width: 64px;
 `
 `
-const Project = styled.div`
-  min-width: 96px;
+const TotalVms = styled.div`
+  ${StyleProps.exactWidth('96px')}
   margin-right: 48px;
   margin-right: 48px;
 `
 `
-const Updated = styled.div`
-  min-width: 175px;
+const Project = styled.div`
+  ${StyleProps.exactWidth('132px')}
+  margin-right: 48px;
 `
 `
 const ItemLabel = styled.div`
 const ItemLabel = styled.div`
   color: ${Palette.grayscale[4]};
   color: ${Palette.grayscale[4]};
@@ -133,12 +133,12 @@ class AssessmentListItem extends React.Component<Props> {
               {this.props.item.project.name}
               {this.props.item.project.name}
             </ItemValue>
             </ItemValue>
           </Project>
           </Project>
-          <Updated>
-            <ItemLabel>Updated</ItemLabel>
+          <TotalVms>
+            <ItemLabel>Instances</ItemLabel>
             <ItemValue>
             <ItemValue>
-              {moment(this.props.item.properties.updatedTimestamp).format('DD MMMM YYYY, HH:mm')}
+              {this.props.item.properties.numberOfMachines}
             </ItemValue>
             </ItemValue>
-          </Updated>
+          </TotalVms>
         </Content>
         </Content>
       </Wrapper>
       </Wrapper>
     )
     )

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

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

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

@@ -105,6 +105,8 @@ type Props = {
   className?: string,
   className?: string,
   useSecondaryStyle?: boolean,
   useSecondaryStyle?: boolean,
   noItemsLabel?: string,
   noItemsLabel?: string,
+  noItemsComponent?: React.Node,
+  noItemsStyle?: any,
   bodyStyle?: any,
   bodyStyle?: any,
   headerStyle?: any,
   headerStyle?: any,
   'data-test-id'?: string,
   'data-test-id'?: string,
@@ -139,7 +141,12 @@ class Table extends React.Component<Props> {
       return null
       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() {
   renderItems() {

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

@@ -58,7 +58,6 @@ const Asterisk = styled.div`
   margin-bottom: -3px;
   margin-bottom: -3px;
   margin-left: ${props => props.marginLeft || '0px'};
   margin-left: ${props => props.marginLeft || '0px'};
 `
 `
-
 type Props = {
 type Props = {
   type: 'replica' | 'migration',
   type: 'replica' | 'migration',
   name: string,
   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 DropdownFilter from '../../molecules/DropdownFilter'
 import Tooltip from '../../atoms/Tooltip'
 import Tooltip from '../../atoms/Tooltip'
 import Checkbox from '../../atoms/Checkbox'
 import Checkbox from '../../atoms/Checkbox'
+import SmallLoading from '../../atoms/SmallLoading'
 
 
 import Palette from '../../styleUtils/Palette'
 import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 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 { Endpoint } from '../../../types/Endpoint'
 import type { Instance, Nic } from '../../../types/Instance'
 import type { Instance, Nic } from '../../../types/Instance'
 import type { Network, NetworkMap } from '../../../types/Network'
 import type { Network, NetworkMap } from '../../../types/Network'
@@ -97,6 +98,15 @@ const LoadingText = styled.div`
   font-size: 18px;
   font-size: 18px;
   margin-top: 32px;
   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)`
 const TableStyled = styled(Table)`
   margin-top: 62px;
   margin-top: 62px;
   ${props => props.addWidthPadding ? css`
   ${props => props.addWidthPadding ? css`
@@ -157,25 +167,35 @@ const NavigationItems = [
 
 
 type Props = {
 type Props = {
   item: ?Assessment,
   item: ?Assessment,
-  targetEndpoint: Endpoint,
   detailsLoading: boolean,
   detailsLoading: boolean,
   instancesDetailsLoading: boolean,
   instancesDetailsLoading: boolean,
   instancesLoading: boolean,
   instancesLoading: boolean,
   networksLoading: boolean,
   networksLoading: boolean,
   instancesDetailsProgress: ?number,
   instancesDetailsProgress: ?number,
+  targetEndpoint: Endpoint,
+  targetEndpoints: Endpoint[],
+  onTargetEndpointChange: (endpoint: Endpoint) => void,
+  targetEndpointsLoading: boolean,
   sourceEndpoints: Endpoint[],
   sourceEndpoints: Endpoint[],
+  sourceEndpoint: ?Endpoint,
   sourceEndpointsLoading: boolean,
   sourceEndpointsLoading: boolean,
+  locations: Location[],
+  selectedLocation: ?string,
+  onLocationChange: (locationName: string) => void,
+  selectedResourceGroup: string,
+  resourceGroups: string[],
+  onResourceGroupChange: (resourceGroupName: string) => void,
+  targetOptionsLoading: boolean,
   assessedVmsCount: number,
   assessedVmsCount: number,
   filteredAssessedVms: VmItem[],
   filteredAssessedVms: VmItem[],
-  selectedVms: VmItem[],
+  selectedVms: string[],
   instancesDetails: Instance[],
   instancesDetails: Instance[],
   instances: Instance[],
   instances: Instance[],
   loadingVmSizes: boolean,
   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[],
   networks: Network[],
-  sourceEndpoint: ?Endpoint,
   page: string,
   page: string,
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onSourceEndpointChange: (endpoint: Endpoint) => void,
   onVmSearchValueChange: (value: string) => 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() {
   renderMainDetails() {
-    if (this.props.detailsLoading) {
-      return null
-    }
-
     if (this.props.page !== '' || !this.props.item || !this.props.item.id) {
     if (this.props.page !== '' || !this.props.item || !this.props.item.id) {
       return null
       return null
     }
     }
 
 
     let status = this.props.item ?
     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 (
     return (
       <Columns>
       <Columns>
         <Column>
         <Column>
-          <Row>
-            <Field>
-              <Label>Type</Label>
-              <Value>Azure Migrate</Value>
-            </Field>
-          </Row>
           <Row>
           <Row>
             <AzureMigrateLogo />
             <AzureMigrateLogo />
           </Row>
           </Row>
@@ -263,52 +263,82 @@ class AssessmentDetailsContent extends React.Component<Props> {
             <Field>
             <Field>
               <Label>Last Update</Label>
               <Label>Last Update</Label>
               <Value>
               <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>
               </Value>
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <Field>
-              <Label>Status</Label>
-              <Value>{status}</Value>
+              <Label>Migration Project</Label>
+              <Value>{this.props.item ? this.props.item.projectName : ''}</Value>
             </Field>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <Field>
-              <Label>Source Endpoint</Label>
-              <Value>{this.renderSourceDropdown()}</Value>
+              <Label>VM Group</Label>
+              <Value>{this.props.item ? this.props.item.groupName : ''}</Value>
             </Field>
             </Field>
           </Row>
           </Row>
-        </Column>
-        <Column>
           <Row>
           <Row>
             <Field>
             <Field>
-              <Label>Project</Label>
-              <Value>{this.props.item ? this.props.item.projectName : ''}</Value>
+              <Label>Status</Label>
+              <Value>{status}</Value>
             </Field>
             </Field>
           </Row>
           </Row>
+        </Column>
+        <Column>
           <Row>
           <Row>
             <Field>
             <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>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <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>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <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>
             </Field>
           </Row>
           </Row>
           <Row>
           <Row>
             <Field>
             <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>
             </Field>
           </Row>
           </Row>
         </Column>
         </Column>
@@ -317,22 +347,20 @@ class AssessmentDetailsContent extends React.Component<Props> {
   }
   }
 
 
   renderVmsTable() {
   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 => {
     let items = this.props.filteredAssessedVms.map(vm => {
       return (
       return (
         <AssessedVmListItem
         <AssessedVmListItem
           item={vm}
           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) }}
           onSelectedChange={(vm, selected) => { this.props.onVmSelectedChange(vm, selected) }}
           disabled={!this.doesVmMatchSource(vm)}
           disabled={!this.doesVmMatchSource(vm)}
           loadingVmSizes={this.props.loadingVmSizes}
           loadingVmSizes={this.props.loadingVmSizes}
           recommendedVmSize={vm.properties.recommendedSize}
           recommendedVmSize={vm.properties.recommendedSize}
           vmSizes={this.props.vmSizes}
           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}`})`
       `${this.props.filteredAssessedVms.length} OUT OF ${this.props.assessedVmsCount}`})`
     let vmHeaderItem = (
     let vmHeaderItem = (
       <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>
         <VmHeaderItemLabel>Virtual Machine {vmCountLabel}</VmHeaderItemLabel>
         <DropdownFilter
         <DropdownFilter
           searchPlaceholder="Filter Virtual Machines"
           searchPlaceholder="Filter Virtual Machines"
@@ -351,22 +379,33 @@ class AssessmentDetailsContent extends React.Component<Props> {
       </VmHeaderItem>
       </VmHeaderItem>
     )
     )
 
 
+
     return (
     return (
       <TableStyled
       <TableStyled
         addWidthPadding
         addWidthPadding
-        items={items}
+        items={loading ? [] : items}
         bodyStyle={TableBodyStyle}
         bodyStyle={TableBodyStyle}
         headerStyle={TableHeaderStyle}
         headerStyle={TableHeaderStyle}
         header={[vmHeaderItem, 'OS', 'Target Disk Type', 'Azure VM Size']}
         header={[vmHeaderItem, 'OS', 'Target Disk Type', 'Azure VM Size']}
         useSecondaryStyle
         useSecondaryStyle
-        noItemsLabel="No VMs found!"
+        noItemsComponent={this.renderLoading('Loading instances, please wait ...')}
       />
       />
     )
     )
   }
   }
 
 
   renderNetworkTable() {
   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 = []
     let nics = []
@@ -413,18 +452,32 @@ class AssessmentDetailsContent extends React.Component<Props> {
     })
     })
     return (
     return (
       <TableStyled
       <TableStyled
-        items={items}
+        items={loading ? [] : items}
         header={['Source Network', '', '', 'Target Network']}
         header={['Source Network', '', '', 'Target Network']}
         useSecondaryStyle
         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 (
     return (
       <Buttons>
       <Buttons>
         <Button secondary onClick={this.props.onRefresh}>Refresh</Button>
         <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 (
     return (
       <LoadingWrapper>
       <LoadingWrapper>
-        <StatusImage loading loadingProgress={loadingProgress} />
+        <StatusImage loading />
         <LoadingText>{message}</LoadingText>
         <LoadingText>{message}</LoadingText>
       </LoadingWrapper>
       </LoadingWrapper>
     )
     )
@@ -484,11 +508,11 @@ class AssessmentDetailsContent extends React.Component<Props> {
           customHref={() => null}
           customHref={() => null}
         />
         />
         <DetailsBody>
         <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 />
           <Tooltip />
         </DetailsBody>
         </DetailsBody>
       </Wrapper>
       </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
 // @flow
 
 
-import React from 'react'
+import * as React from 'react'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 import styled from 'styled-components'
 
 
 import Button from '../../atoms/Button'
 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 type { Field } from '../../../types/Field'
 
 
+import StyleProps from '../../styleUtils/StyleProps'
+
 import assessmentImage from './images/assessment.svg'
 import assessmentImage from './images/assessment.svg'
 
 
 const Wrapper = styled.div`
 const Wrapper = styled.div`
@@ -31,124 +34,233 @@ const Wrapper = styled.div`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
+  min-height: 0;
 `
 `
 const Image = styled.div`
 const Image = styled.div`
   width: 96px;
   width: 96px;
   height: 96px;
   height: 96px;
   background: url('${assessmentImage}') center no-repeat;
   background: url('${assessmentImage}') center no-repeat;
 `
 `
+const ToggleButtonBarStyled = styled(ToggleButtonBar)`
+  margin-top: 48px;
+`
 const Fields = styled.div`
 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;
   justify-content: space-between;
-  margin-bottom: 32px;
-  &:last-child {
-    margin-bottom: 0;
-  }
 `
 `
+
 const Buttons = styled.div`
 const Buttons = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   width: 100%;
   width: 100%;
-  margin-top: 48px;
+  margin-top: 32px;
 `
 `
 
 
 const generalFields = [
 const generalFields = [
   {
   {
     name: 'use_replica',
     name: 'use_replica',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
   },
   {
   {
     name: 'separate_vm',
     name: 'separate_vm',
-    type: 'boolean',
-    value: true,
+    type: 'strict-boolean',
   },
   },
 ]
 ]
 const replicaFields = [
 const replicaFields = [
   {
   {
     name: 'shutdown_instances',
     name: 'shutdown_instances',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
   },
 ]
 ]
 const migrationFields = [
 const migrationFields = [
   {
   {
     name: 'skip_os_morphing',
     name: 'skip_os_morphing',
-    type: 'boolean',
+    type: 'strict-boolean',
   },
   },
 ]
 ]
 
 
 type Props = {
 type Props = {
   onCancelClick: () => void,
   onCancelClick: () => void,
-  onExecuteClick: (fields: Field[]) => void,
+  onExecuteClick: (fieldValues: { [string]: any }) => void,
   executeButtonDisabled: boolean,
   executeButtonDisabled: boolean,
+  replicaSchema: Field[],
+  migrationSchema: Field[],
+  onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
 }
 }
 type State = {
 type State = {
-  generalFields: Field[],
-  migrationFields: Field[],
-  replicaFields: Field[],
+  fieldValues: { [string]: any },
+  showAdvancedOptions: boolean,
 }
 }
 @observer
 @observer
 class AssessmentMigrationOptions extends React.Component<Props, State> {
 class AssessmentMigrationOptions extends React.Component<Props, State> {
   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 {
     } 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 (
     return (
       <Wrapper>
       <Wrapper>
         <Image />
         <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>
         <Buttons>
-          <Button secondary onClick={() => { this.props.onCancelClick() }}>Cancel</Button>
           <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}
             disabled={this.props.executeButtonDisabled}
           >Execute</Button>
           >Execute</Button>
         </Buttons>
         </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 React from 'react'
 import styled from 'styled-components'
 import styled from 'styled-components'
+import cookie from 'js-cookie'
 import { observer } from 'mobx-react'
 import { observer } from 'mobx-react'
 
 
 import DetailsTemplate from '../../templates/DetailsTemplate'
 import DetailsTemplate from '../../templates/DetailsTemplate'
@@ -26,11 +27,12 @@ import Modal from '../../molecules/Modal'
 import AssessmentMigrationOptions from '../../organisms/AssessmentMigrationOptions'
 import AssessmentMigrationOptions from '../../organisms/AssessmentMigrationOptions'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Endpoint } from '../../../types/Endpoint'
 import type { Nic } from '../../../types/Instance'
 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 { Field } from '../../../types/Field'
 import type { Network, NetworkMap } from '../../../types/Network'
 import type { Network, NetworkMap } from '../../../types/Network'
 
 
 import azureStore from '../../../stores/AzureStore'
 import azureStore from '../../../stores/AzureStore'
+import type { LocalData } from '../../../stores/AzureStore'
 import endpointStore from '../../../stores/EndpointStore'
 import endpointStore from '../../../stores/EndpointStore'
 import notificationStore from '../../../stores/NotificationStore'
 import notificationStore from '../../../stores/NotificationStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import replicaStore from '../../../stores/ReplicaStore'
@@ -38,6 +40,7 @@ import instanceStore from '../../../stores/InstanceStore'
 import networkStore from '../../../stores/NetworkStore'
 import networkStore from '../../../stores/NetworkStore'
 import userStore from '../../../stores/UserStore'
 import userStore from '../../../stores/UserStore'
 import assessmentStore from '../../../stores/AssessmentStore'
 import assessmentStore from '../../../stores/AssessmentStore'
+import providerStore from '../../../stores/ProviderStore'
 
 
 import assessmentImage from './images/assessment.svg'
 import assessmentImage from './images/assessment.svg'
 
 
@@ -47,29 +50,32 @@ type Props = {
   match: any,
   match: any,
 }
 }
 type State = {
 type State = {
-  sourceEndpoint: ?Endpoint,
-  selectedVms: VmItem[],
   selectedNetworks: NetworkMap[],
   selectedNetworks: NetworkMap[],
   showMigrationOptions: boolean,
   showMigrationOptions: boolean,
   executeButtonDisabled: boolean,
   executeButtonDisabled: boolean,
-  vmSizes: { [string]: VmSize },
   vmSearchValue: string,
   vmSearchValue: string,
+  loadingTargetVmSizes: boolean,
+  replicaSchema: Field[],
+  migrationSchema: Field[],
 }
 }
 @observer
 @observer
 class AssessmentDetailsPage extends React.Component<Props, State> {
 class AssessmentDetailsPage extends React.Component<Props, State> {
   state = {
   state = {
-    sourceEndpoint: null,
-    selectedVms: [],
     selectedNetworks: [],
     selectedNetworks: [],
     showMigrationOptions: false,
     showMigrationOptions: false,
     executeButtonDisabled: false,
     executeButtonDisabled: false,
-    vmSizes: {},
     vmSearchValue: '',
     vmSearchValue: '',
+    loadingTargetVmSizes: false,
+    replicaSchema: [],
+    migrationSchema: [],
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
     document.title = 'Assessment Details'
     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()
     this.azureAuthenticate()
   }
   }
 
 
@@ -79,12 +85,17 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     instanceStore.clearInstancesDetails()
     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() {
   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 vms = azureStore.assessedVms
     let connectionsInfo = endpointStore.connectionsInfo
     let connectionsInfo = endpointStore.connectionsInfo
 
 
@@ -98,6 +109,11 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     return endpoints
     return endpoints
   }
   }
 
 
+  getTargetEndpoints() {
+    let endpoints = endpointStore.endpoints
+    return endpoints.filter(e => e.type === 'azure')
+  }
+
   getInstancesDetailsProgress() {
   getInstancesDetailsProgress() {
     let count = instanceStore.instancesDetailsCount
     let count = instanceStore.instancesDetailsCount
     if (count < 5) {
     if (count < 5) {
@@ -117,7 +133,8 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   getSourceEndpointId() {
   getSourceEndpointId() {
-    return this.state.sourceEndpoint ? this.state.sourceEndpoint.id : ''
+    let localData = this.getLocalData()
+    return localData.sourceEndpoint ? localData.sourceEndpoint.id : ''
   }
   }
 
 
   getEnabledVms() {
   getEnabledVms() {
@@ -143,31 +160,61 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     if (this.getFilteredAssessedVms().length === 0 || this.getEnabledVms().length === 0) {
     if (this.getFilteredAssessedVms().length === 0 || this.getEnabledVms().length === 0) {
       return false
       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) {
   handleVmSelectedChange(vm: VmItem, selected: boolean) {
+    let selectedVms = this.getLocalData().selectedVms
     if (selected) {
     if (selected) {
-      this.setState({ selectedVms: [...this.state.selectedVms, vm] }, () => { this.loadInstancesDetails() })
+      selectedVms = [
+        ...selectedVms,
+        vm.properties.datacenterMachineId,
+      ]
+      azureStore.updateSelectedVms(selectedVms)
+      this.loadInstancesDetails()
     } else {
     } 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) {
   handleSelectAllVmsChange(selected: boolean) {
     let selectedVms = selected ? [...this.getFilteredAssessedVms(this.getEnabledVms())] : []
     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) {
   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.initSelectedVms()
       this.loadInstancesDetails()
       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 }) {
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
     switch (item.value) {
       case 'signout':
       case 'signout':
@@ -193,13 +240,21 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
   }
   }
 
 
   handleRefresh() {
   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() {
   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 })
     this.setState({ showMigrationOptions: true })
   }
   }
 
 
@@ -207,53 +262,12 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     this.setState({ showMigrationOptions: false })
     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) {
   handleVmSearchValueChange(vmSearchValue: string) {
@@ -267,62 +281,156 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     })
     })
   }
   }
 
 
-  loadSourceEndpoints() {
+  loadEndpoints() {
     endpointStore.getEndpoints({ showLoading: true }).then(() => {
     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() {
   loadAssessmentDetails() {
     let urlInfo = this.getUrlInfo()
     let urlInfo = this.getUrlInfo()
     azureStore.getAssessmentDetails({ ...urlInfo }).then(() => {
     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.loadNetworks()
+      this.loadTargetOptions()
+      this.loadTargetVmSizes()
     })
     })
+
     azureStore.getAssessedVms({ ...urlInfo }).then(() => {
     azureStore.getAssessedVms({ ...urlInfo }).then(() => {
       this.initVmSizes()
       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() {
   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() {
   initVmSizes() {
     let vmSizes = {}
     let vmSizes = {}
     let vms = azureStore.assessedVms
     let vms = azureStore.assessedVms
+    let localData = this.getLocalData()
 
 
     vms.forEach(vm => {
     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() {
   loadNetworks() {
+    let localData = this.getLocalData()
     this.setState({ selectedNetworks: [] })
     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() {
   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.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() {
   render() {
@@ -331,6 +439,7 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
     let endpointsLoading = endpointStore.connectionsInfoLoading || endpointStore.loading
     let endpointsLoading = endpointStore.connectionsInfoLoading || endpointStore.loading
     let status = details ? details.properties.status.toUpperCase() : ''
     let status = details ? details.properties.status.toUpperCase() : ''
     let statusLabel = status === 'COMPLETED' ? 'READY' : status
     let statusLabel = status === 'COMPLETED' ? 'READY' : status
+    let localData = this.getLocalData()
 
 
     return (
     return (
       <Wrapper>
       <Wrapper>
@@ -356,34 +465,41 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
             <AssessmentDetailsContent
             <AssessmentDetailsContent
               item={details}
               item={details}
               detailsLoading={loading}
               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}
               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}
               assessedVmsCount={azureStore.assessedVms.length}
               filteredAssessedVms={this.getFilteredAssessedVms()}
               filteredAssessedVms={this.getFilteredAssessedVms()}
               onSourceEndpointChange={endpoint => this.handleSourceEndpointChange(endpoint)}
               onSourceEndpointChange={endpoint => this.handleSourceEndpointChange(endpoint)}
-              selectedVms={this.state.selectedVms}
+              selectedVms={localData.selectedVms}
               onVmSelectedChange={(vm, selected) => { this.handleVmSelectedChange(vm, selected) }}
               onVmSelectedChange={(vm, selected) => { this.handleVmSelectedChange(vm, selected) }}
               selectAllVmsChecked={this.getSelectAllVmsChecked()}
               selectAllVmsChecked={this.getSelectAllVmsChecked()}
               onSelectAllVmsChange={checked => { this.handleSelectAllVmsChange(checked) }}
               onSelectAllVmsChange={checked => { this.handleSelectAllVmsChange(checked) }}
               instances={instanceStore.instances}
               instances={instanceStore.instances}
               instancesDetails={instanceStore.instancesDetails}
               instancesDetails={instanceStore.instancesDetails}
-              instancesDetailsLoading={instanceStore.loadingInstancesDetails}
-              instancesLoading={instanceStore.instancesLoading}
-              networksLoading={networkStore.loading}
-              instancesDetailsProgress={this.getInstancesDetailsProgress()}
               networks={networkStore.networks}
               networks={networkStore.networks}
               selectedNetworks={this.state.selectedNetworks}
               selectedNetworks={this.state.selectedNetworks}
-              loadingVmSizes={azureStore.loadingVmSizes}
               vmSizes={azureStore.vmSizes}
               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}
               vmSearchValue={this.state.vmSearchValue}
               onVmSearchValueChange={value => { this.handleVmSearchValueChange(value) }}
               onVmSearchValueChange={value => { this.handleVmSearchValueChange(value) }}
-              onGetVmSize={vm => this.handleGetVmSize(vm)}
               onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
               onNetworkChange={(sourceNic, targetNetwork) => { this.handleNetworkChange(sourceNic, targetNetwork) }}
               onRefresh={() => this.handleRefresh()}
               onRefresh={() => this.handleRefresh()}
               onMigrateClick={() => { this.handleMigrateClick() }}
               onMigrateClick={() => { this.handleMigrateClick() }}
@@ -397,7 +513,9 @@ class AssessmentDetailsPage extends React.Component<Props, State> {
         >
         >
           <AssessmentMigrationOptions
           <AssessmentMigrationOptions
             onCancelClick={() => { this.handleCloseMigrationOptions() }}
             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}
             executeButtonDisabled={this.state.executeButtonDisabled}
           />
           />
         </Modal>
         </Modal>

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

@@ -88,7 +88,7 @@ class AssessmentsPage extends React.Component<Props, State> {
   }
   }
 
 
   getResourceGroupsDropdownConfig() {
   getResourceGroupsDropdownConfig() {
-    let groups = azureStore.resourceGroups
+    let groups = azureStore.assessmentResourceGroups
     return {
     return {
       key: 'resource-group',
       key: 'resource-group',
       selectedItem: assessmentStore.selectedResourceGroup ? assessmentStore.selectedResourceGroup.id : '',
       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(() => {
       azureStore.authenticate(connectionInfo.user_credentials.username, connectionInfo.user_credentials.password).then(() => {
         // $FlowIgnore
         // $FlowIgnore
         azureStore.getResourceGroups(connectionInfo.subscription_id).then(() => {
         azureStore.getResourceGroups(connectionInfo.subscription_id).then(() => {
-          let groups = azureStore.resourceGroups
+          let groups = azureStore.assessmentResourceGroups
           let selectedGroup = assessmentStore.selectedResourceGroup
           let selectedGroup = assessmentStore.selectedResourceGroup
           // $FlowIssue
           // $FlowIssue
           if (groups.filter(rg => rg.id === selectedGroup ? selectedGroup.id : '').length > 0) {
           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 {
 class AssessmentSourceUtils {
   static getDestinationEnv(data: MigrationInfo) {
   static getDestinationEnv(data: MigrationInfo) {
-    let env = { ...data.destinationEnv }
-    env.network_map = {}
+    let env = {}
     if (data.networks && data.networks.length) {
     if (data.networks && data.networks.length) {
+      env.network_map = {}
       data.networks.forEach(mapping => {
       data.networks.forEach(mapping => {
         env.network_map[mapping.sourceNic.network_name] = mapping.targetNetwork.name
         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
     return env
   }
   }
 }
 }
 
 
 class AssessmentSource {
 class AssessmentSource {
   static migrate(data: MigrationInfo): Promise<MainItem> {
   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] = {
     payload[type] = {
       origin_endpoint_id: data.source ? data.source.id : 'null',
       origin_endpoint_id: data.source ? data.source.id : 'null',
       destination_endpoint_id: data.target.id,
       destination_endpoint_id: data.target.id,
@@ -48,14 +57,9 @@ class AssessmentSource {
       security_groups: ['testgroup'],
       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({
     return Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/${type}s`,
       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 => {
     return Promise.all(endpoints.map(endpoint => {
       let index = endpoint.connection_info.secret_ref ? endpoint.connection_info.secret_ref.lastIndexOf('/') : ''
       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) : ''
       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({
       return Api.send({
         url: `${servicesUrl.barbican}/v1/secrets/${uuid}/payload`,
         url: `${servicesUrl.barbican}/v1/secrets/${uuid}/payload`,
         responseType: 'text',
         responseType: 'text',
@@ -171,7 +174,7 @@ class EdnpointSource {
   }
   }
 
 
   static add(endpoint: Endpoint, skipSchemaParser: boolean = false): Promise<Endpoint> {
   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 newEndpoint: any = {}
     let connectionInfo = {}
     let connectionInfo = {}
     if (useSecret) {
     if (useSecret) {
@@ -216,7 +219,12 @@ class EdnpointSource {
     return Api.send({
     return Api.send({
       url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints`,
       url: `${servicesUrl.coriolis}/${Api.projectId}/endpoints`,
       method: 'POST',
       method: 'POST',
-      data: { endpoint: parsedEndpoint },
+      data: {
+        endpoint: {
+          ...parsedEndpoint,
+          type: endpoint.type,
+        },
+      },
     }).then(response => {
     }).then(response => {
       return response.data.endpoint
       return response.data.endpoint
     })
     })

+ 1 - 6
src/stores/AssessmentStore.js

@@ -36,14 +36,9 @@ class AssessmentStore {
   }
   }
 
 
   @action migrate(data: MigrationInfo): Promise<void> {
   @action migrate(data: MigrationInfo): Promise<void> {
-    if (!data.options) {
-      return Promise.resolve()
-    }
-
     this.migrating = true
     this.migrating = true
     this.migrations = []
     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) {
     if (separateVm) {
       return AssessmentSource.migrateMultiple(data).then((items: MainItem[]) => {
       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 cookie from 'js-cookie'
 
 
 import AzureSource from '../sources/AzureSource'
 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 {
 class AzureStore {
   @observable authenticating: boolean = false
   @observable authenticating: boolean = false
   @observable loadingResourceGroups: boolean = false
   @observable loadingResourceGroups: boolean = false
-  @observable resourceGroups: $PropertyType<Assessment, 'group'>[] = []
+  @observable assessmentResourceGroups: $PropertyType<Assessment, 'group'>[] = []
+  @observable coriolisResourceGroups: string[] = []
   @observable loadingAssessments: boolean = false
   @observable loadingAssessments: boolean = false
   @observable loadingAssessmentDetails: boolean = false
   @observable loadingAssessmentDetails: boolean = false
   @observable assessmentDetails: ?Assessment = null
   @observable assessmentDetails: ?Assessment = null
@@ -31,8 +66,91 @@ class AzureStore {
   @observable loadingAssessedVms: boolean = false
   @observable loadingAssessedVms: boolean = false
   @observable assessedVms: VmItem[] = []
   @observable assessedVms: VmItem[] = []
   @observable loadingVmSizes: boolean = false
   @observable loadingVmSizes: boolean = false
-  @observable vmSizes: VmSize[] = []
+  // @observable vmSizes: VmSize[] = []
   @observable assessmentsProjectId: string = ''
   @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> {
   @action authenticate(username: string, password: string): Promise<void> {
     this.authenticating = true
     this.authenticating = true
@@ -49,7 +167,7 @@ class AzureStore {
 
 
     return AzureSource.getResourceGroups(subscriptionId).then((groups: $PropertyType<Assessment, 'group'>[]) => {
     return AzureSource.getResourceGroups(subscriptionId).then((groups: $PropertyType<Assessment, 'group'>[]) => {
       this.loadingResourceGroups = false
       this.loadingResourceGroups = false
-      this.resourceGroups = groups
+      this.assessmentResourceGroups = groups
     }).catch(() => {
     }).catch(() => {
       this.loadingResourceGroups = false
       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() {
   @action clearAssessmentDetails() {
     this.assessmentDetails = null
     this.assessmentDetails = null
     this.assessedVms = []
     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() {
   @action clearAssessedVms() {
     this.assessedVms = []
     this.assessedVms = []
   }
   }
 
 
   @action clearAssessments() {
   @action clearAssessments() {
-    this.resourceGroups = []
+    this.assessmentResourceGroups = []
     this.assessments = []
     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 type { Instance } from '../types/Instance'
 import InstanceSource from '../sources/InstanceSource'
 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 {
 class InstanceStoreUtils {
   static hasNextPage(instances) {
   static hasNextPage(instances) {
     let result = false
     let result = false
@@ -63,7 +124,7 @@ class InstanceStore {
   lastEndpointId: string
   lastEndpointId: string
   reqId: number
   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) {
     if (this.cachedInstances.length > 0 && this.lastEndpointId === endpointId && useCache) {
       return Promise.resolve()
       return Promise.resolve()
     }
     }
@@ -72,6 +133,18 @@ class InstanceStore {
     this.searchNotFound = false
     this.searchNotFound = false
     this.lastEndpointId = endpointId
     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 => {
     return InstanceSource.loadInstances(endpointId, null, null, skipLimit).then(instances => {
       if (endpointId !== this.lastEndpointId) {
       if (endpointId !== this.lastEndpointId) {
         return
         return
@@ -82,6 +155,8 @@ class InstanceStore {
       this.instances = instances
       this.instances = instances
       this.cachedInstances = instances
       this.cachedInstances = instances
       this.instancesLoading = false
       this.instancesLoading = false
+
+      InstanceLocalStorage.saveInstancesToLocalStorage(endpointId, instances)
     }).catch(() => {
     }).catch(() => {
       if (endpointId !== this.lastEndpointId) {
       if (endpointId !== this.lastEndpointId) {
         return
         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
     // 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
     this.reqId = !this.reqId ? 1 : this.reqId + 1
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
     InstanceSource.cancelInstancesDetailsRequests(this.reqId - 1)
@@ -179,7 +254,17 @@ class InstanceStore {
     this.loadingInstancesDetails = true
     this.loadingInstancesDetails = true
     this.instancesDetailsCount = count
     this.instancesDetailsCount = count
     this.instancesDetailsRemaining = 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) => {
     return new Promise((resolve) => {
       instancesInfo.forEach(instanceInfo => {
       instancesInfo.forEach(instanceInfo => {
@@ -195,6 +280,8 @@ class InstanceStore {
             this.instancesDetails = this.instancesDetails.filter(i => i.id !== resp.instance.id)
             this.instancesDetails = this.instancesDetails.filter(i => i.id !== resp.instance.id)
           }
           }
 
 
+          InstanceLocalStorage.saveDetailsToLocalStorage(endpointId, resp.instance)
+
           this.instancesDetails = [
           this.instancesDetails = [
             ...this.instancesDetails,
             ...this.instancesDetails,
             resp.instance,
             resp.instance,

+ 35 - 1
src/stores/NetworkStore.js

@@ -18,23 +18,57 @@ import { observable, action } from 'mobx'
 import type { Network } from '../types/Network'
 import type { Network } from '../types/Network'
 import NetworkSource from '../sources/NetworkSource'
 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 {
 class NetworkStore {
   @observable networks: Network[] = []
   @observable networks: Network[] = []
   @observable loading: boolean = false
   @observable loading: boolean = false
 
 
   cachedId: string = ''
   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))}`
     let id = `${endpointId}-${btoa(JSON.stringify(environment))}`
     if (this.cachedId === id) {
     if (this.cachedId === id) {
       return Promise.resolve()
       return Promise.resolve()
     }
     }
 
 
     this.loading = true
     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[]) => {
     return NetworkSource.loadNetworks(endpointId, environment).then((networks: Network[]) => {
       this.loading = false
       this.loading = false
       this.networks = networks
       this.networks = networks
       this.cachedId = id
       this.cachedId = id
+
+      NetworkLocalStorage.saveNetworksToLocalStorage(id, networks)
     }).catch(() => {
     }).catch(() => {
       this.loading = false
       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)
     let providerWithExtraOptions = providersWithExtraOptions.find(p => typeof p === 'string' ? p === provider : p.name === provider)
     if (!providerWithExtraOptions) {
     if (!providerWithExtraOptions) {
-      return Promise.resolve()
+      return Promise.resolve([])
     }
     }
 
 
     this.destinationOptionsLoading = true
     this.destinationOptionsLoading = true
+    this.destinationOptions = []
+    let destOptions = []
+
     return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
     return ProviderSource.getDestinationOptions(endpointId, envData).then(options => {
       this.optionsSchema.forEach(field => {
       this.optionsSchema.forEach(field => {
         const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
         const parser = OptionsSchemaPlugin[provider] || OptionsSchemaPlugin.default
         parser.fillFieldValues(field, options)
         parser.fillFieldValues(field, options)
       })
       })
       this.destinationOptions = options
       this.destinationOptions = options
+      destOptions = options
       this.destinationOptionsLoading = false
       this.destinationOptionsLoading = false
     }).catch(err => {
     }).catch(err => {
       console.error(err)
       console.error(err)
@@ -97,8 +101,8 @@ class ProviderStore {
       }
       }
       return this.loadOptionsSchema(provider, this.lastOptionsSchemaType)
       return this.loadOptionsSchema(provider, this.lastOptionsSchemaType)
     }).then(() => {
     }).then(() => {
-      this.destinationOptions = []
       this.destinationOptionsLoading = false
       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
 // @flow
 
 
-import type { Field } from './Field'
 import type { Endpoint } from './Endpoint'
 import type { Endpoint } from './Endpoint'
 import type { Instance } from './Instance'
 import type { Instance } from './Instance'
 import type { NetworkMap } from './Network'
 import type { NetworkMap } from './Network'
@@ -24,6 +23,16 @@ export type VmSize = {
   size?: string,
   size?: string,
 }
 }
 
 
+export type Location = {
+  id: string,
+  name: string,
+}
+
+export type Group = {
+  id: string,
+  name: string,
+}
+
 export type VmItem = {
 export type VmItem = {
   id: string,
   id: string,
   properties: {
   properties: {
@@ -55,24 +64,21 @@ export type Assessment = {
   project: {
   project: {
     name: string,
     name: string,
   },
   },
-  group: {
-    name: string,
-    id: string,
-  },
+  group: Group,
   properties: {
   properties: {
     status: string,
     status: string,
     updatedTimestamp: string,
     updatedTimestamp: string,
     azureLocation: string,
     azureLocation: string,
+    numberOfMachines: string,
   },
   },
   connectionInfo: { subscription_id: string } & $PropertyType<Endpoint, 'connection_info'>,
   connectionInfo: { subscription_id: string } & $PropertyType<Endpoint, 'connection_info'>,
 }
 }
 
 
 export type MigrationInfo = {
 export type MigrationInfo = {
-  options: Field[],
   source: ?Endpoint,
   source: ?Endpoint,
   target: Endpoint,
   target: Endpoint,
   selectedInstances: Instance[],
   selectedInstances: Instance[],
-  destinationEnv: { [string]: mixed },
+  fieldValues: { [string]: any },
   networks: NetworkMap[],
   networks: NetworkMap[],
   vmSizes: { [string]: VmSize },
   vmSizes: { [string]: VmSize },
 }
 }

+ 1 - 1
webpack.config.js

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