/*
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 .
*/
import * as React from 'react'
import styled, { createGlobalStyle } from 'styled-components'
import { Collapse } from 'react-collapse'
import Arrow from '@src/components/ui/Arrow'
import { ThemePalette, ThemeProps } from '@src/components/Theme'
import {
TransferNetworkMap, isNetworkMapSecurityGroups,
isNetworkMapSourceDest, TransferItem,
} from '@src/@types/MainItem'
import type { Instance, Nic, Disk } from '@src/@types/Instance'
import { Network, NetworkUtils } from '@src/@types/Network'
import { MinionPool } from '@src/@types/MinionPool'
import { EndpointUtils, StorageBackend } from '@src/@types/Endpoint'
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 GlobalStyle = createGlobalStyle`
.ReactCollapse--collapse {
transition: height 0.4s ease-in-out;
}
`
const Wrapper = styled.div`
margin: 24px 0;
`
const ArrowStyled = styled(Arrow)`
position: absolute;
left: -24px;
`
const Header = styled.div`
display: flex;
`
const HeaderLabel = styled.div`
font-size: 10px;
color: ${ThemePalette.grayscale[3]};
font-weight: ${ThemeProps.fontWeights.medium};
text-transform: uppercase;
width: 50%;
margin-bottom: 8px;
&:last-child { margin-left: 36px; }
`
const InstanceInfo = styled.div`
background: ${ThemePalette.grayscale[1]};
border-radius: ${ThemeProps.borderRadius};
margin-bottom: 32px;
&:last-child { margin-bottom: 0; }
`
const InstanceName = styled.div`
padding: 16px;
border-bottom: 1px solid ${ThemePalette.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 ${ThemeProps.animations.swift};
&:last-child {
border-bottom: 0;
border-bottom-left-radius: ${ThemeProps.borderRadius};
border-bottom-right-radius: ${ThemeProps.borderRadius};
}
&:hover {
background: ${ThemePalette.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;
${ThemeProps.exactWidth('50%')}
&:last-child { margin-left: 19px; }
`
const HeaderName = styled.div`
overflow: hidden;
text-overflow: ellipsis;
${props => ThemeProps.exactWidth(`calc(100% - ${props.source ? 120 : 8}px)`)}
`
const RowBody = styled.div`
display: flex;
color: ${ThemePalette.grayscale[5]};
padding: 0 16px;
margin-top: 4px;
`
const RowBodyColumn = styled.div`
&:first-child {
${ThemeProps.exactWidth('calc(50% - 70px)')}
margin-right: 88px;
}
&:last-child {
${ThemeProps.exactWidth('calc(50% - 16px)')}
}
`
const RowBodyColumnValue = styled.div`
overflow-wrap: break-word;
`
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;
`
export const TEST_ID = 'mainDetailsTable'
export type Props = {
item?: TransferItem | null,
instancesDetails: Instance[],
networks?: Network[],
minionPools: MinionPool[]
storageBackends: StorageBackend[]
}
type State = {
openedRows: string[],
}
class TransferDetailsTable extends React.Component {
state = {
openedRows: [],
}
getTransferResult(instance: Instance): Instance | null {
if (this.props.item?.transfer_result) {
const transferInstanceKey = Object.keys(this.props.item.transfer_result)
.find(k => k === instance.name || k === instance.instance_name || k === instance.id)
if (transferInstanceKey) {
return this.props.item.transfer_result[transferInstanceKey]
}
}
return null
}
handleRowClick(id: string) {
if (this.state.openedRows.find(i => i === id)) {
this.setState(prevState => ({
openedRows: prevState.openedRows.filter(i => i !== id),
}))
} else {
this.setState(prevState => ({
openedRows: [...prevState.openedRows, id],
}))
}
}
renderRow(
id: string,
icon: 'instance' | 'network' | 'storage',
sourceName: string,
destinationName: React.ReactNode,
sourceBody: string[],
destinationBody: string[],
) {
const isOpened: boolean = Boolean(this.state.openedRows.find(i => i === id))
return (
{ this.handleRowClick(id) }}>
{sourceName}
{destinationName ? : null}
{destinationName}
{sourceBody.map(l => {l})}
{destinationBody.map(l => {l})}
)
}
renderStorage(instance: Instance, type: 'backend' | 'disk') {
const storageMapping = this.props.item?.storage_mappings
const transferResult = this.getTransferResult(instance)
const rows: React.ReactNode[] = []
const diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
const mappingFieldName = type === 'backend' ? 'source' : 'disk_id'
const storageMappingFieldName = type === 'backend' ? 'backend_mappings' : 'disk_mappings'
instance.devices.disks.forEach(disk => {
const sourceName = disk[diskFieldName] || ''
const mappedDisk = (storageMapping?.[storageMappingFieldName] as any)
?.find((m: any) => String(m[mappingFieldName]) === String(disk[diskFieldName]))
let destinationName: React.ReactNode
let destinationKey: string
const defaultBusTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, this.props.item?.storage_mappings?.default || null)
if (disk.disabled) {
destinationKey = disk.disabled.info || disk.disabled.message
destinationName = {destinationKey}
} else {
destinationName = defaultBusTypeInfo.id || 'Default'
destinationKey = destinationName as string
}
let destinationBody: string[] = []
if (mappedDisk) {
const busTypeInfo = EndpointUtils.getBusTypeStorageId(this.props.storageBackends, mappedDisk?.destination)
destinationName = busTypeInfo.id
destinationKey = destinationName as string
if (busTypeInfo.busType) {
destinationBody.push(`Bus Type: ${busTypeInfo.busType}`)
}
} else if (defaultBusTypeInfo.busType) {
destinationBody.push(`Bus Type: ${defaultBusTypeInfo.busType}`)
}
const getBody = (d: Disk): string[] => {
const 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
}
const sourceBody = getBody(disk)
if (transferResult) {
const transferDisk = transferResult.devices.disks
.find(d => d.storage_backend_identifier === destinationName)
if (transferDisk) {
destinationName = transferDisk.name || transferDisk.id
destinationKey = destinationName as string
destinationBody = destinationBody.concat(getBody(transferDisk))
}
} else if (this.props.item?.type === 'migration' && (
this.props.item.last_execution_status === 'RUNNING'
|| this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
)) {
destinationBody = ['Waiting for migration to finish']
}
rows.push(this.renderRow(
`${instance.instance_name || instance.id}-${sourceName}-${destinationKey}`,
'storage',
sourceName,
destinationName,
sourceBody,
destinationBody,
))
})
return rows
}
renderNetworks(instance: Instance) {
let destinationNetworkMap: TransferNetworkMap | null = null
if (this.props.item && this.props.item.network_map) {
destinationNetworkMap = this.props.item.network_map
}
if (destinationNetworkMap == null) {
return null
}
const transferResult = this.getTransferResult(instance)
const rows: React.ReactNode[] = []
instance.devices.nics.forEach(nic => {
if (destinationNetworkMap
&& isNetworkMapSourceDest(destinationNetworkMap)
&& destinationNetworkMap[nic.network_name]) {
const getBody = (n: Nic): string[] => {
const body: string[] = [`Name: ${n.network_name}`]
const ipv4 = n.ip_addresses ? n.ip_addresses.find(ip => /(?:\d+?\.){3}\d+/g.exec(ip)) : null
const 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}`)
}
return body
}
const destNetMapObj = destinationNetworkMap[nic.network_name]
const portKeyInfo = NetworkUtils.getPortKeyNetworkId(this.props.networks || [], destNetMapObj as any)
const destinationNetworkId = isNetworkMapSecurityGroups(destNetMapObj) ? destNetMapObj.id : portKeyInfo.id
const destinationNetwork = this.props.networks?.find(n => n.id === destinationNetworkId)
const sourceBody = getBody(nic)
let destinationBody: string[] = []
if (isNetworkMapSecurityGroups(destNetMapObj) && destNetMapObj.security_groups?.length) {
const destSecGroupsInfo = (destinationNetwork?.security_groups) || []
const secNames = destNetMapObj.security_groups.map(s => {
const foundSecGroupInfo = destSecGroupsInfo.find(si => (typeof si === 'string' ? si === s : si.id === s))
return foundSecGroupInfo && typeof foundSecGroupInfo !== 'string' && foundSecGroupInfo.name ? foundSecGroupInfo.name : s
})
destinationBody = [`Security Groups: ${secNames.join(', ')}`]
}
if (portKeyInfo.portKey != null) {
destinationBody = [`Port Key: ${portKeyInfo.portKey}`]
}
let destinationNetworkName = destinationNetworkId
if (destinationNetwork) {
destinationNetworkName = destinationNetwork.name
}
if (transferResult) {
const destinationNic = transferResult.devices.nics
.find(n => n.network_id === destinationNetworkId
|| n.network_name === destinationNetworkId)
if (destinationNic) {
destinationNetworkName = destinationNic.network_name
destinationBody = getBody(destinationNic)
}
} else if (this.props.item?.type === 'migration' && (
this.props.item.last_execution_status === 'RUNNING'
|| this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
)) {
destinationBody = ['Waiting for migration to finish']
}
rows.push(this.renderRow(
`${instance.instance_name || instance.id}-${nic.network_name}`,
'network',
nic.mac_address,
destinationNetworkName,
sourceBody,
destinationBody,
))
}
})
return rows
}
renderInstanceDetails(instance: Instance) {
const getBody = (i: Instance): string[] => [
`ID: ${i.id}`,
`Cores: ${i.num_cpu}`,
`Memory: ${i.memory_mb} MB`,
`Flavor Name: ${i.flavor_name || 'N/A'}`,
`OS Type: ${i.os_type}`,
]
const sourceBody: string[] = getBody(instance)
const minionPoolMappings = this.props.item?.instance_osmorphing_minion_pool_mappings
const minionPoolId = minionPoolMappings
&& minionPoolMappings[instance.instance_name || instance.id || instance.name]
if (minionPoolId) {
const minionPool = this.props.minionPools.find(m => m.id === minionPoolId)
sourceBody.push(`Minion Pool: ${minionPool?.name || minionPoolId}`)
}
let destinationBody: string[] = []
let destinationName: string = ''
const transferResult = this.getTransferResult(instance)
if (transferResult) {
destinationName = transferResult.instance_name || transferResult.name
destinationBody = getBody(transferResult)
} else if (this.props.item?.type === 'migration' && (
this.props.item.last_execution_status === 'RUNNING'
|| this.props.item.last_execution_status === 'AWAITING_MINION_ALLOCATIONS'
)) {
destinationName = 'Waiting for migration to finish'
}
const instanceName = instance.instance_name || instance.id
return this.renderRow(
instanceName,
'instance',
instanceName,
destinationName,
sourceBody,
destinationBody,
)
}
render() {
if (this.props.instancesDetails.length === 0 || !this.props.item) {
return null
}
return (
{this.props.instancesDetails.map(instance => (
{instance.name}
{this.renderInstanceDetails(instance)}
{this.renderNetworks(instance)}
{this.renderStorage(instance, 'disk')}
{this.renderStorage(instance, 'backend')}
))}
)
}
}
export default TransferDetailsTable