|
|
@@ -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
|