MainDetailsTable.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import * as React from 'react'
  16. import styled from 'styled-components'
  17. import { Collapse } from 'react-collapse'
  18. import Arrow from '../../atoms/Arrow'
  19. import Palette from '../../styleUtils/Palette'
  20. import StyleProps from '../../styleUtils/StyleProps'
  21. import type { MainItem } from '../../../types/MainItem'
  22. import type { Instance, Nic, Disk } from '../../../types/Instance'
  23. import type { Network } from '../../../types/Network'
  24. import instanceIcon from './images/instance.svg'
  25. import networkIcon from './images/network.svg'
  26. import storageIcon from './images/storage.svg'
  27. import arrowIcon from './images/arrow.svg'
  28. const Wrapper = styled.div`
  29. margin: 24px 0;
  30. `
  31. const ArrowStyled = styled(Arrow)`
  32. position: absolute;
  33. left: -24px;
  34. `
  35. const Header = styled.div`
  36. display: flex;
  37. `
  38. const HeaderLabel = styled.div`
  39. font-size: 10px;
  40. color: ${Palette.grayscale[3]};
  41. font-weight: ${StyleProps.fontWeights.medium};
  42. text-transform: uppercase;
  43. width: 50%;
  44. margin-bottom: 8px;
  45. &:last-child { margin-left: 36px; }
  46. `
  47. const InstanceInfo = styled.div`
  48. background: ${Palette.grayscale[1]};
  49. border-radius: ${StyleProps.borderRadius};
  50. margin-bottom: 32px;
  51. &:last-child { margin-bottom: 0; }
  52. `
  53. const InstanceName = styled.div`
  54. padding: 16px;
  55. border-bottom: 1px solid ${Palette.grayscale[5]};
  56. font-size: 16px;
  57. `
  58. const InstanceBody = styled.div`
  59. font-size: 14px;
  60. `
  61. const Row = styled.div`
  62. position: relative;
  63. padding: 8px 0;
  64. border-bottom: 1px solid white;
  65. transition: all ${StyleProps.animations.swift};
  66. &:last-child {
  67. border-bottom: 0;
  68. border-bottom-left-radius: ${StyleProps.borderRadius};
  69. border-bottom-right-radius: ${StyleProps.borderRadius};
  70. }
  71. &:hover {
  72. background: ${Palette.grayscale[0]};
  73. ${ArrowStyled} {
  74. opacity: 1;
  75. }
  76. }
  77. cursor: pointer;
  78. `
  79. const RowHeader = styled.div`
  80. display: flex;
  81. align-items: center;
  82. padding: 0 16px;
  83. `
  84. const RowHeaderColumn = styled.div`
  85. display: flex;
  86. align-items: center;
  87. ${StyleProps.exactWidth('50%')}
  88. &:last-child { margin-left: 19px; }
  89. `
  90. const HeaderName = styled.div`
  91. overflow: hidden;
  92. text-overflow: ellipsis;
  93. ${props => StyleProps.exactWidth(`calc(100% - ${props.source ? 120 : 8}px)`)}
  94. `
  95. const RowBody = styled.div`
  96. display: flex;
  97. color: ${Palette.grayscale[5]};
  98. padding: 0 16px;
  99. margin-top: 4px;
  100. `
  101. const RowBodyColumn = styled.div`
  102. &:first-child {
  103. ${StyleProps.exactWidth('calc(50% - 70px)')}
  104. margin-right: 88px;
  105. }
  106. &:last-child {
  107. ${StyleProps.exactWidth('calc(50% - 16px)')}
  108. }
  109. `
  110. const RowBodyColumnValue = styled.div`
  111. overflow-wrap: break-word;
  112. `
  113. const getHeaderIcon = (icon: 'instance' | 'network' | 'storage'): string => {
  114. switch (icon) {
  115. case 'instance':
  116. return instanceIcon
  117. case 'network':
  118. return networkIcon
  119. default:
  120. return storageIcon
  121. }
  122. }
  123. const HeaderIcon = styled.div`
  124. width: 16px;
  125. height: 16px;
  126. background: url('${props => getHeaderIcon(props.icon)}') center no-repeat;
  127. margin-right: 16px;
  128. `
  129. const ArrowIcon = styled.div`
  130. width: 32px;
  131. height: 16px;
  132. background: url('${arrowIcon}') center no-repeat;
  133. margin-left: 16px;
  134. `
  135. export const TEST_ID = 'mainDetailsTable'
  136. export type Props = {
  137. item: ?MainItem,
  138. instancesDetails: Instance[],
  139. networks?: Network[],
  140. }
  141. type State = {
  142. openedRows: string[],
  143. }
  144. class MainDetailsTable extends React.Component<Props, State> {
  145. state = {
  146. openedRows: [],
  147. }
  148. getTransferResult(instance: Instance): ?Instance {
  149. if (this.props.item && this.props.item.transfer_result) {
  150. let transferInstanceKey = Object.keys(this.props.item.transfer_result).find(i => i.indexOf(instance.name))
  151. if (transferInstanceKey && this.props.item && this.props.item.transfer_result) {
  152. let result = this.props.item.transfer_result[transferInstanceKey]
  153. result.instance_name = transferInstanceKey
  154. return result
  155. }
  156. }
  157. return null
  158. }
  159. handleRowClick(id: string) {
  160. if (this.state.openedRows.find(i => i === id)) {
  161. this.setState({
  162. openedRows: this.state.openedRows.filter(i => i !== id),
  163. })
  164. } else {
  165. this.setState({
  166. openedRows: [...this.state.openedRows, id],
  167. })
  168. }
  169. }
  170. renderRow(
  171. id: string,
  172. icon: 'instance' | 'network' | 'storage',
  173. sourceName: string,
  174. destinationName: React.Node,
  175. sourceBody: string[],
  176. destinationBody: string[]
  177. ) {
  178. let isOpened: boolean = Boolean(this.state.openedRows.find(i => i === id))
  179. return (
  180. <Row key={id} onClick={() => { this.handleRowClick(id) }}>
  181. <ArrowStyled
  182. primary
  183. orientation={isOpened ? 'up' : 'down'}
  184. opacity={isOpened ? 1 : 0}
  185. thick
  186. />
  187. <RowHeader>
  188. <RowHeaderColumn>
  189. <HeaderIcon icon={icon} />
  190. <HeaderName source data-test-id={`${TEST_ID}-source-${icon}`}>{sourceName}</HeaderName>
  191. {destinationName ? <ArrowIcon /> : null}
  192. </RowHeaderColumn>
  193. <RowHeaderColumn>
  194. <HeaderName data-test-id={`${TEST_ID}-destination-${icon}`}>{destinationName}</HeaderName>
  195. </RowHeaderColumn>
  196. </RowHeader>
  197. <Collapse isOpened={isOpened} springConfig={{ stiffness: 100, damping: 20 }}>
  198. <RowBody>
  199. <RowBodyColumn>{sourceBody.map(l => <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>)}</RowBodyColumn>
  200. <RowBodyColumn>{destinationBody.map(l => <RowBodyColumnValue key={l}>{l}</RowBodyColumnValue>)}</RowBodyColumn>
  201. </RowBody>
  202. </Collapse>
  203. </Row>
  204. )
  205. }
  206. renderStorage(instance: Instance) {
  207. let storageMapping = this.props.item && this.props.item.storage_mappings
  208. let transferResult = this.getTransferResult(instance)
  209. let rows = []
  210. instance.devices.disks.forEach(disk => {
  211. let sourceName = disk.id
  212. let mappedDisk = storageMapping && storageMapping.disk_mappings &&
  213. storageMapping.disk_mappings.find(m => String(m.disk_id) === String(disk.id))
  214. let destinationName: React.Node
  215. let destinationKey: string
  216. if (disk.disabled) {
  217. destinationKey = disk.disabled.info || disk.disabled.message
  218. destinationName = <span style={{ color: Palette.grayscale[5] }}>{destinationKey}</span>
  219. } else {
  220. destinationName = (
  221. this.props.item && this.props.item.storage_mappings
  222. && this.props.item.storage_mappings.default
  223. ) || 'Default'
  224. destinationKey = destinationName
  225. }
  226. if (mappedDisk) {
  227. destinationName = mappedDisk.destination
  228. destinationKey = destinationName
  229. }
  230. let getBody = (d: Disk): string[] => {
  231. let body: string[] = []
  232. if (d.size_bytes) {
  233. body.push(`Size: ${(d.size_bytes / 1024 / 1024).toFixed(0)} MB`)
  234. }
  235. if (d.storage_backend_identifier) {
  236. body.push(`Backend Identifier: ${d.storage_backend_identifier}`)
  237. }
  238. if (d.format) {
  239. body.push(`Format: ${d.format}`)
  240. }
  241. if (d.guest_device) {
  242. body.push(`Guest Device: ${d.guest_device}`)
  243. }
  244. return body
  245. }
  246. let sourceBody = getBody(disk)
  247. let destinationBody = []
  248. if (transferResult) {
  249. let transferDisk = transferResult.devices.disks.find(d => d.storage_backend_identifier === destinationName)
  250. if (transferDisk) {
  251. destinationName = transferDisk.name || transferDisk.id
  252. destinationKey = destinationName
  253. destinationBody = getBody(transferDisk)
  254. }
  255. } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
  256. destinationBody = ['Waiting for migration to finish']
  257. }
  258. rows.push(this.renderRow(
  259. `${instance.instance_name || instance.name}-${sourceName}-${destinationKey}`,
  260. 'storage',
  261. sourceName,
  262. destinationName,
  263. sourceBody,
  264. destinationBody
  265. ))
  266. })
  267. return rows
  268. }
  269. renderNetworks(instance: Instance) {
  270. let destinationNetworkMap = null
  271. if (this.props.item && this.props.item.network_map) {
  272. destinationNetworkMap = this.props.item.network_map
  273. }
  274. if (destinationNetworkMap == null) {
  275. return null
  276. }
  277. let transferResult = this.getTransferResult(instance)
  278. let rows = []
  279. instance.devices.nics.forEach(nic => {
  280. if (destinationNetworkMap && destinationNetworkMap[nic.network_name]) {
  281. let getBody = (n: Nic): string[] => {
  282. let body: string[] = [`Name: ${n.network_name}`]
  283. let ipv4 = n.ip_addresses ? n.ip_addresses.find(ip => /(?:\d+?\.){3}\d+/g.exec(ip)) : null
  284. let ipv6 = n.ip_addresses ? n.ip_addresses.find(ip => /\w*:\w*/g.exec(ip)) : null
  285. if (ipv4) {
  286. body.push(`IP Address (IPv4): ${ipv4}`)
  287. }
  288. if (ipv6) {
  289. body.push(`IP Address (IPv6): ${ipv6}`)
  290. }
  291. return body
  292. }
  293. let destNetMapObj = destinationNetworkMap[nic.network_name]
  294. let destinationNetworkId = String(typeof destNetMapObj === 'string' || !destNetMapObj
  295. || !destNetMapObj.id ? destNetMapObj : destNetMapObj.id)
  296. let destinationNetwork = this.props.networks && this.props.networks.find(n => n.id === destinationNetworkId)
  297. let sourceBody = getBody(nic)
  298. let destinationBody = []
  299. if (destNetMapObj.security_groups && destNetMapObj.security_groups.length) {
  300. let destSecGroupsInfo = (destinationNetwork && destinationNetwork.security_groups) || []
  301. // $FlowIgnore
  302. let secNames = destNetMapObj.security_groups.map(s => {
  303. let foundSecGroupInfo = destSecGroupsInfo.find(si => si.id ? si.id === s : si === s)
  304. return foundSecGroupInfo && foundSecGroupInfo.name ? foundSecGroupInfo.name : s
  305. })
  306. destinationBody = [`Security Groups: ${secNames.join(', ')}`]
  307. }
  308. let destinationNetworkName = destinationNetworkId
  309. if (destinationNetwork) {
  310. destinationNetworkName = destinationNetwork.name
  311. }
  312. if (transferResult) {
  313. let destinationNic = transferResult.devices.nics
  314. .find(n => n.network_id === destinationNetworkId || n.network_name === destinationNetworkId)
  315. if (destinationNic) {
  316. destinationNetworkName = destinationNic.network_name
  317. destinationBody = getBody(destinationNic)
  318. }
  319. } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
  320. destinationBody = ['Waiting for migration to finish']
  321. }
  322. rows.push(this.renderRow(
  323. `${instance.instance_name || instance.name}-${nic.network_name}`,
  324. 'network',
  325. nic.mac_address,
  326. destinationNetworkName,
  327. sourceBody,
  328. destinationBody
  329. ))
  330. }
  331. })
  332. return rows
  333. }
  334. renderInstanceDetails(instance: Instance) {
  335. let getBody = (i: Instance): string[] => [
  336. `Cores: ${i.num_cpu}`,
  337. `Memory: ${i.memory_mb} MB`,
  338. `Flavor Name: ${i.flavor_name || 'N/A'}`,
  339. `OS Type: ${i.os_type}`,
  340. ]
  341. let sourceBody: string[] = getBody(instance)
  342. let destinationBody: string[] = []
  343. let destinationName: string = ''
  344. let transferResult = this.getTransferResult(instance)
  345. if (transferResult) {
  346. destinationName = transferResult.instance_name || transferResult.name
  347. destinationBody = getBody(transferResult)
  348. } else if (this.props.item && this.props.item.status === 'RUNNING' && this.props.item.type === 'migration') {
  349. destinationName = 'Waiting for migration to finish'
  350. }
  351. let instanceName = instance.instance_name || instance.name
  352. return this.renderRow(
  353. instanceName,
  354. 'instance',
  355. instanceName,
  356. destinationName,
  357. sourceBody,
  358. destinationBody
  359. )
  360. }
  361. render() {
  362. if (this.props.instancesDetails.length === 0 || !this.props.item) {
  363. return null
  364. }
  365. return (
  366. <Wrapper>
  367. <Header>
  368. <HeaderLabel>Source</HeaderLabel>
  369. <HeaderLabel>Target</HeaderLabel>
  370. </Header>
  371. {this.props.instancesDetails.map(instance => (
  372. <InstanceInfo key={instance.name}>
  373. <InstanceName data-test-id={`${TEST_ID}-instanceName-${instance.name}`}>{instance.name}</InstanceName>
  374. <InstanceBody>
  375. {this.renderInstanceDetails(instance)}
  376. {this.renderNetworks(instance)}
  377. {this.renderStorage(instance)}
  378. </InstanceBody>
  379. </InstanceInfo>
  380. ))}
  381. </Wrapper>
  382. )
  383. }
  384. }
  385. export default MainDetailsTable