ソースを参照

Show mapping info in replica / migration details

Also includes destination information for migrations that return
`transfer_result`.
Sergiu Miclea 7 年 前
コミット
9e7040ed4e

+ 376 - 0
src/components/molecules/MainDetailsTable/MainDetailsTable.jsx

@@ -0,0 +1,376 @@
+/*
+Copyright (C) 2017  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 styled from 'styled-components'
+import { Collapse } from 'react-collapse'
+
+import Arrow from '../../atoms/Arrow'
+
+import Palette from '../../styleUtils/Palette'
+import StyleProps from '../../styleUtils/StyleProps'
+
+import type { MainItem } from '../../../types/MainItem'
+import type { Instance, Nic, Disk } from '../../../types/Instance'
+
+import instanceIcon from './images/instance.svg'
+import networkIcon from './images/network.svg'
+import storageIcon from './images/storage.svg'
+import arrowIcon from './images/arrow.svg'
+
+const Wrapper = styled.div`
+  margin-top: 24px;
+  margin-bottom: 48px;
+`
+const ArrowStyled = styled(Arrow)`
+  position: absolute;
+  left: -24px;
+`
+const Header = styled.div`
+  display: flex;
+`
+const HeaderLabel = styled.div`
+  font-size: 10px;
+  color: ${Palette.grayscale[3]};
+  font-weight: ${StyleProps.fontWeights.medium};
+  text-transform: uppercase;
+  width: 50%;
+  margin-bottom: 8px;
+  &:last-child { margin-left: 36px; }
+`
+const InstanceInfo = styled.div`
+  background: ${Palette.grayscale[1]};
+  border-radius: ${StyleProps.borderRadius};
+  margin-bottom: 32px;
+  &:last-child { margin-bottom: 0; }
+`
+const InstanceName = styled.div`
+  padding: 16px;
+  border-bottom: 1px solid ${Palette.grayscale[5]};
+  font-size: 16px;
+`
+const InstanceBody = styled.div`
+  font-size: 14px;
+`
+const Row = styled.div`
+  position: relative;
+  padding: 8px 0;
+  border-bottom: 1px solid white;
+  transition: all ${StyleProps.animations.swift};
+  &:last-child {
+    border-bottom: 0;
+    border-bottom-left-radius: ${StyleProps.borderRadius};
+    border-bottom-right-radius: ${StyleProps.borderRadius};
+  }
+  &:hover {
+    background: ${Palette.grayscale[0]};
+    ${ArrowStyled} {
+      opacity: 1;
+    }
+  }
+  cursor: pointer;
+`
+const RowHeader = styled.div`
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+`
+const RowHeaderColumn = styled.div`
+  display: flex;
+  align-items: center;
+  ${StyleProps.exactWidth('50%')}
+  &:last-child { margin-left: 19px; }
+`
+const HeaderName = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  ${props => StyleProps.exactWidth(`calc(100% - ${props.source ? 120 : 8}px)`)}
+`
+const RowBody = styled.div`
+  display: flex;
+  color: ${Palette.grayscale[5]};
+  padding: 0 16px;
+  margin-top: 4px;
+`
+const RowBodyColumn = styled.div`
+  width: 50%;
+  &:first-child { margin-left: 32px; }
+  &:last-child { margin-left: 6px; }
+`
+const getHeaderIcon = (icon: 'instance' | 'network' | 'storage'): string => {
+  switch (icon) {
+    case 'instance':
+      return instanceIcon
+    case 'network':
+      return networkIcon
+    default:
+      return storageIcon
+  }
+}
+const HeaderIcon = styled.div`
+  width: 16px;
+  height: 16px;
+  background: url('${props => getHeaderIcon(props.icon)}') center no-repeat;
+  margin-right: 16px;
+`
+const ArrowIcon = styled.div`
+  width: 32px;
+  height: 16px;
+  background: url('${arrowIcon}') center no-repeat;
+  margin-left: 16px;
+`
+
+type Props = {
+  item: ?MainItem,
+  instancesDetails: Instance[],
+}
+type State = {
+  openedRows: string[],
+}
+
+class MainDetailsTable extends React.Component<Props, State> {
+  state = {
+    openedRows: [],
+  }
+
+  getTransferResult(instance: Instance): ?Instance {
+    if (this.props.item && this.props.item.transfer_result) {
+      let transferInstanceKey = Object.keys(this.props.item.transfer_result).find(i => i.indexOf(instance.name))
+      if (transferInstanceKey && this.props.item && this.props.item.transfer_result) {
+        let result = this.props.item.transfer_result[transferInstanceKey]
+        result.instance_name = transferInstanceKey
+        return result
+      }
+    }
+    return null
+  }
+
+  handleRowClick(id: string) {
+    if (this.state.openedRows.find(i => i === id)) {
+      this.setState({
+        openedRows: this.state.openedRows.filter(i => i !== id),
+      })
+    } else {
+      this.setState({
+        openedRows: [...this.state.openedRows, id],
+      })
+    }
+  }
+
+  renderRow(
+    id: string,
+    icon: 'instance' | 'network' | 'storage',
+    sourceName: string,
+    destinationName: string,
+    sourceBody: string[],
+    destinationBody: string[]
+  ) {
+    let isOpened: boolean = Boolean(this.state.openedRows.find(i => i === id))
+
+    return (
+      <Row key={id} onClick={() => { this.handleRowClick(id) }}>
+        <ArrowStyled
+          primary
+          orientation={isOpened ? 'up' : 'down'}
+          opacity={isOpened ? 1 : 0}
+          thick
+        />
+        <RowHeader>
+          <RowHeaderColumn>
+            <HeaderIcon icon={icon} />
+            <HeaderName source>{sourceName}</HeaderName>
+            {destinationName ? <ArrowIcon /> : null}
+          </RowHeaderColumn>
+          <RowHeaderColumn>
+            <HeaderName>{destinationName}</HeaderName>
+          </RowHeaderColumn>
+        </RowHeader>
+        <Collapse isOpened={isOpened} springConfig={{ stiffness: 100, damping: 20 }}>
+          <RowBody>
+            <RowBodyColumn>{sourceBody.map(l => <div key={l}>{l}</div>)}</RowBodyColumn>
+            <RowBodyColumn>{destinationBody.map(l => <div key={l}>{l}</div>)}</RowBodyColumn>
+          </RowBody>
+        </Collapse>
+      </Row>
+    )
+  }
+
+  renderStorage(instance: Instance) {
+    let storageMapping = this.props.item && this.props.item.storage_mappings
+    let transferResult = this.getTransferResult(instance)
+    let rows = []
+    instance.devices.disks.forEach(disk => {
+      let sourceName = disk.id
+      let mappedDisk = storageMapping && storageMapping.disk_mappings &&
+        storageMapping.disk_mappings.find(m => String(m.disk_id) === String(disk.id))
+      let destinationName: string = (
+        this.props.item && this.props.item.storage_mappings
+        && this.props.item.storage_mappings.default
+      ) || 'Default'
+      if (mappedDisk) {
+        destinationName = mappedDisk.destination
+      }
+      let getBody = (d: Disk): string[] => {
+        let body: string[] = []
+        if (d.size_bytes) {
+          body.push(`Size: ${(d.size_bytes / 1024 / 1024).toFixed(0)} MB`)
+        }
+        if (d.storage_backend_identifier) {
+          body.push(`Backend Identifier: ${d.storage_backend_identifier}`)
+        }
+        if (d.format) {
+          body.push(`Format: ${d.format}`)
+        }
+        if (d.guest_device) {
+          body.push(`Guest Device: ${d.guest_device}`)
+        }
+        return body
+      }
+      let sourceBody = getBody(disk)
+      let destinationBody = []
+      if (transferResult) {
+        let transferDisk = transferResult.devices.disks.find(d => d.storage_backend_identifier === destinationName)
+        if (transferDisk) {
+          destinationName = transferDisk.name || transferDisk.id
+          destinationBody = getBody(transferDisk)
+        }
+      } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+        destinationBody = ['Waiting for migration to finish']
+      }
+
+      rows.push(this.renderRow(
+        `${instance.instance_name}-${sourceName}-${destinationName}`,
+        'storage',
+        sourceName,
+        destinationName,
+        sourceBody,
+        destinationBody
+      ))
+    })
+
+    return rows
+  }
+
+  renderNetworks(instance: Instance) {
+    let destinationNetworkMap = null
+    if (this.props.item && this.props.item.destination_environment.network_map) {
+      destinationNetworkMap = this.props.item.destination_environment.network_map
+    }
+    if (destinationNetworkMap == null) {
+      return null
+    }
+    let transferResult = this.getTransferResult(instance)
+    let rows = []
+    instance.devices.nics.forEach(nic => {
+      if (destinationNetworkMap && destinationNetworkMap[nic.network_name]) {
+        let getBody = (n: Nic): string[] => {
+          let body: string[] = []
+          let ipv4 = n.ip_addresses ? n.ip_addresses.find(ip => /(?:\d+?\.){3}\d+/g.exec(ip)) : null
+          let ipv6 = n.ip_addresses ? n.ip_addresses.find(ip => /\w*:\w*/g.exec(ip)) : null
+          if (ipv4) {
+            body.push(`IP Address (IPv4): ${ipv4}`)
+          }
+          if (ipv6) {
+            body.push(`IP Address (IPv6): ${ipv6}`)
+          }
+          body.push(`MAC Address: ${n.mac_address}`)
+          return body
+        }
+        let sourceBody = getBody(nic)
+        let destinationBody = []
+
+        let destinationNetworkName = String(destinationNetworkMap[nic.network_name])
+        if (transferResult) {
+          let destinationNic = transferResult.devices.nics
+            .find(n => n.network_id === destinationNetworkName || n.network_name === destinationNetworkName)
+          if (destinationNic) {
+            destinationNetworkName = destinationNic.network_name
+            destinationBody = getBody(destinationNic)
+          }
+        } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+          destinationBody = ['Waiting for migration to finish']
+        }
+
+        rows.push(this.renderRow(
+          `${instance.instance_name}-${nic.network_name}`,
+          'network',
+          nic.network_name,
+          destinationNetworkName,
+          sourceBody,
+          destinationBody
+        ))
+      }
+    })
+
+    return rows
+  }
+
+  renderInstanceDetails(instance: Instance) {
+    let getBody = (i: Instance): string[] => [
+      `Cores: ${i.num_cpu}`,
+      `Memory: ${i.memory_mb} MB`,
+      `Flavor Name: ${i.flavor_name || 'N/A'}`,
+      `OS Type: ${i.os_type}`,
+    ]
+
+    let sourceBody: string[] = getBody(instance)
+    let destinationBody: string[] = []
+    let destinationName: string = ''
+    let transferResult = this.getTransferResult(instance)
+    if (transferResult) {
+      destinationName = transferResult.instance_name
+      destinationBody = getBody(transferResult)
+    } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
+      destinationName = 'Waiting for migration to finish'
+    }
+
+    return this.renderRow(
+      instance.instance_name,
+      'instance',
+      `${instance.instance_name}`,
+      `${destinationName}`,
+      sourceBody,
+      destinationBody
+    )
+  }
+
+  render() {
+    if (this.props.instancesDetails.length === 0 || !this.props.item) {
+      return null
+    }
+
+    return (
+      <Wrapper>
+        <Header>
+          <HeaderLabel>Source</HeaderLabel>
+          <HeaderLabel>Destination</HeaderLabel>
+        </Header>
+        {this.props.instancesDetails.map(instance => (
+          <InstanceInfo key={instance.name}>
+            <InstanceName>{instance.name}</InstanceName>
+            <InstanceBody>
+              {this.renderInstanceDetails(instance)}
+              {this.renderNetworks(instance)}
+              {this.renderStorage(instance)}
+            </InstanceBody>
+          </InstanceInfo>
+        ))}
+      </Wrapper>
+    )
+  }
+}
+
+export default MainDetailsTable

+ 14 - 0
src/components/molecules/MainDetailsTable/images/arrow.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="21px" height="14px" viewBox="0 0 21 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
+    <title>Arrow</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
+        <g id="Migration/Details/Overview-Open" transform="translate(-710.000000, -745.000000)" stroke="#A4AAB5" stroke-width="1.5">
+            <g id="Icon/Arrow/Small" transform="translate(704.000000, 744.000000)">
+                <polyline id="Path-181-Copy-4" stroke-linejoin="round" points="21 2.5 26 8 21 13.5"></polyline>
+                <path d="M7,8 L26,8" id="Line" transform="translate(16.500000, 8.000000) rotate(-180.000000) translate(-16.500000, -8.000000) "></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/components/molecules/MainDetailsTable/images/instance.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="16px" viewBox="0 0 14 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon/Storage/16-2 Copy 2</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Migration/Details/Overview-Closed" transform="translate(-337.000000, -649.000000)" stroke="#0044CA" stroke-width="1.5">
+            <g id="Icon/Network/16-Copy-3" transform="translate(336.000000, 649.000000)">
+                <polygon id="Page-1" stroke-linecap="round" stroke-linejoin="round" points="8 1 2 4.5 2 11.4750313 8 15 14 11.5 14 4.5"></polygon>
+                <polyline id="Path-3" stroke-linejoin="round" points="2 5 7.87559762 7.9545219 13.8755976 5"></polyline>
+                <path d="M7.87559762,7.70746946 L7.87559762,15.5342087" id="Path-4"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/components/molecules/MainDetailsTable/images/network.svg

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

+ 16 - 0
src/components/molecules/MainDetailsTable/images/storage.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 52.4 (67378) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon/Storage/16</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Migration/Details/Overview-Closed" transform="translate(-336.000000, -745.000000)">
+            <g id="Icon/Storage/16-Copy-2" transform="translate(336.000000, 745.000000)">
+                <path d="M7,11 C6.44964029,11 6,11.4493709 6,11.9994008 C6,12.5494308 6.44964029,13 7,13 C7.55035971,13 8,12.5494308 8,11.9994008 C8,11.4493709 7.55035971,11 7,11" id="Fill-7-Copy-4" fill="#0044CA" fill-rule="evenodd"></path>
+                <path d="M4,11 C3.44964029,11 3,11.4493709 3,11.9994008 C3,12.5494308 3.44964029,13 4,13 C4.55035971,13 5,12.5494308 5,11.9994008 C5,11.4493709 4.55035971,11 4,11" id="Fill-7-Copy-5" fill="#0044CA" fill-rule="evenodd"></path>
+                <rect id="Rectangle-9-Copy-2" stroke="#0044CA" stroke-width="1.5" x="1" y="9" width="14" height="6" rx="1.5"></rect>
+                <path d="M1,10.3866757 C1,9.88540581 1.13769484,9.10377173 1.30967924,8.63504411 L3.80143187,1.84400221 C3.97246299,1.37787266 4.50214254,1 4.98820212,1 L11.0117979,1 C11.4962019,1 12.0265837,1.37527459 12.1985681,1.84400221 L14.6903208,8.63504411 C14.8613519,9.10117366 15,9.88591999 15,10.3866757" id="Path" stroke="#0044CA" stroke-width="1.5"></path>
+            </g>
+        </g>
+    </g>
+</svg>

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

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

+ 1 - 1
src/components/molecules/TaskItem/TaskItem.jsx

@@ -186,7 +186,7 @@ class TaskItem extends React.Component<Props> {
         <HeaderData width={this.props.columnWidths[3]}>
           {date ? DateUtils.getLocalTime(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
         </HeaderData>
-        <ArrowStyled primary orientation={this.props.open ? 'up' : 'down'} opacity={this.props.open ? 1 : 0} />
+        <ArrowStyled primary orientation={this.props.open ? 'up' : 'down'} opacity={this.props.open ? 1 : 0} thick />
       </Header>
     )
   }

+ 19 - 47
src/components/organisms/MainDetails/MainDetails.jsx

@@ -22,7 +22,7 @@ import EndpointLogos from '../../atoms/EndpointLogos'
 import CopyValue from '../../atoms/CopyValue'
 import StatusIcon from '../../atoms/StatusIcon'
 import StatusImage from '../../atoms/StatusImage'
-import Table from '../../molecules/Table'
+import MainDetailsTable from '../../molecules/MainDetailsTable'
 import CopyMultilineValue from '../../atoms/CopyMultilineValue'
 
 import type { Instance } from '../../../types/Instance'
@@ -80,10 +80,6 @@ const ValueLink = styled.a`
   text-decoration: none;
   cursor: pointer;
 `
-const TableStyled = styled(Table)`
-  margin-top: 89px;
-  margin-bottom: 48px;
-`
 const Loading = styled.div`
   display: flex;
   justify-content: center;
@@ -205,27 +201,6 @@ class MainDetails extends React.Component<Props> {
     return <CopyValue value={value} maxWidth="90%" data-test-id={dateTestId ? `mainDetails-${dateTestId}` : undefined} />
   }
 
-  renderNetworksTable() {
-    if (this.props.loading) {
-      return null
-    }
-
-    let items = this.getNetworks()
-
-    if (!items || !items.length) {
-      return null
-    }
-
-    return (
-      <TableStyled
-        header={['Source Network', 'Connected VMs', 'Destination Network', 'Destination Type']}
-        items={items}
-        columnsStyle={[css`color: ${Palette.black};`]}
-        data-test-id="mainDetails-networksTable"
-      />
-    )
-  }
-
   renderEndpointLink(type: string): React.Node {
     let endpointIsMissing = (
       <Value flex data-test-id={`mainDetails-missing-${type}`}>
@@ -250,8 +225,8 @@ class MainDetails extends React.Component<Props> {
       if (value === false) {
         return 'No'
       }
-      if (value.join && value.length && value[0].destination && (value[0].source || value[0].disk_id)) {
-        return value.map(v => `${v.source || v.disk_id}=${v.destination}`).join(', ')
+      if (value.join && value.length && value[0].destination && value[0].source) {
+        return value.map(v => `${v.source}=${v.destination}`).join(', ')
       }
       return value.toString()
     }
@@ -269,6 +244,9 @@ class MainDetails extends React.Component<Props> {
         })
       } else if (value && typeof value === 'object') {
         properties = properties.concat(Object.keys(value).map(p => {
+          if (p === 'disk_mappings') {
+            return null
+          }
           return {
             label: `${label} - ${LabelDictionary.get(p)}`,
             value: getValue(value[p]),
@@ -282,6 +260,9 @@ class MainDetails extends React.Component<Props> {
     return (
       <PropertiesTable>
         {properties.map(prop => {
+          if (prop == null) {
+            return null
+          }
           return (
             <PropertyRow key={prop.label}>
               <PropertyName>{prop.label}</PropertyName>
@@ -304,7 +285,7 @@ class MainDetails extends React.Component<Props> {
 
     return (
       <ColumnsLayout>
-        <Column width="34.5%">
+        <Column width="42.5%">
           <Row>
             <Field>
               <Label>Source</Label>
@@ -338,12 +319,6 @@ class MainDetails extends React.Component<Props> {
                 : <Value>-</Value>}
             </Field>
           </Row>
-          <Row>
-            <Field>
-              <Label>Type</Label>
-              <Value capitalize data-test-id="mainDetails-type">Coriolis {this.props.item && this.props.item.type}</Value>
-            </Field>
-          </Row>
           <Row>
             <Field>
               <Label>Last Updated</Label>
@@ -351,7 +326,7 @@ class MainDetails extends React.Component<Props> {
             </Field>
           </Row>
         </Column>
-        <Column width="17.5%">
+        <Column width="9.5%">
           <Arrow />
         </Column>
         <Column width="48%" style={{ flexGrow: 1 }}>
@@ -375,14 +350,6 @@ class MainDetails extends React.Component<Props> {
               </Field>
             </Row>
           ) : null}
-          {this.props.item && this.props.item.instances ? (
-            <Row>
-              <Field>
-                <Label>Instances</Label>
-                <CopyMultilineValue value={this.props.item.instances.join('<br />')} useDangerousHtml />
-              </Field>
-            </Row>
-          ) : null}
         </Column>
       </ColumnsLayout>
     )
@@ -397,7 +364,7 @@ class MainDetails extends React.Component<Props> {
   }
 
   renderLoading() {
-    if (!this.props.loading) {
+    if (!this.props.loading && !this.props.instancesDetailsLoading) {
       return null
     }
 
@@ -412,9 +379,14 @@ class MainDetails extends React.Component<Props> {
     return (
       <Wrapper>
         {this.renderTable()}
-        {this.renderNetworksTable()}
-        {this.renderBottomControls()}
+        {this.props.instancesDetailsLoading || this.props.loading ? null : (
+          <MainDetailsTable
+            item={this.props.item}
+            instancesDetails={this.props.instancesDetails}
+          />
+        )}
         {this.renderLoading()}
+        {this.renderBottomControls()}
       </Wrapper>
     )
   }

+ 0 - 21
src/components/organisms/MainDetails/test.jsx

@@ -37,9 +37,6 @@ let item = {
   instances: ['instance_1'],
   destination_environment: {
     description: 'A description',
-    network_map: {
-      network_1: 'Mapping 1',
-    },
   },
   type: 'Replica',
 }
@@ -73,24 +70,6 @@ describe('MainDetails Component', () => {
     expect(wrapper.find('targetLogo').prop('endpoint')).toBe('azure')
   })
 
-  it('renders network_map', () => {
-    let wrapper = wrap({ item, endpoints, instancesDetails })
-    let tableItems = wrapper.find('networksTable').prop('items')
-    expect(tableItems.length).toBe(1)
-    expect(tableItems[0].length).toBe(4)
-    expect(tableItems[0][0]).toBe('network_1')
-    expect(new TW(shallow(tableItems[0][1][0])).find('vm-', true).text()).toBe('instance_1')
-    expect(tableItems[0][2]).toBe('Mapping 1')
-    expect(tableItems[0][3]).toBe('Existing network')
-    expect(wrapper.find('loading').length).toBe(0)
-  })
-
-  it('renders network map with missing source instance', () => {
-    let wrapper = wrap({ item, endpoints, instancesDetails: [] })
-    let tableItems = wrapper.find('networksTable').prop('items')
-    expect(tableItems[0][1]).toBe('Failed to read network configuration for the original instance')
-  })
-
   it('renders loading', () => {
     let wrapper = wrap({ item: {}, endpoints: [], loading: true })
     expect(wrapper.find('loading').length).toBe(1)

+ 9 - 1
src/types/Instance.js

@@ -16,12 +16,19 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 export type Nic = {
   id: string,
-  network_name: string
+  network_name: string,
+  ip_addresses?: string[],
+  mac_address: string,
+  network_id: string,
 }
 
 export type Disk = {
   id: string,
+  name?: string,
   storage_backend_identifier?: string,
+  format?: string,
+  guest_device?: string,
+  size_bytes?: number,
 }
 
 export type Instance = {
@@ -31,6 +38,7 @@ export type Instance = {
   instance_name: string,
   num_cpu: number,
   memory_mb: number,
+  os_type: string,
   devices: {
     nics: Nic[],
     disks: Disk[],

+ 13 - 0
src/types/MainItem.js

@@ -16,6 +16,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import type { Execution } from './Execution'
 import type { Task } from './Task'
+import type { Instance } from './Instance'
 
 export type MainItemInfo = {
   export_info: {
@@ -52,4 +53,16 @@ export type MainItem = {
   type: string,
   info: { [string]: MainItemInfo },
   destination_environment: DestinationEnvInfo,
+  transfer_result: ?{ [string]: Instance },
+  storage_mappings: ?{
+    backend_mappings: ?{
+      destination: string,
+      source: string,
+    }[],
+    default: ?string,
+    disk_mappings: ?{
+      destination: string,
+      disk_id: string,
+    }[],
+  },
 }