MainDetails.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 { Link } from 'react-router-dom'
  17. import { observer } from 'mobx-react'
  18. import styled, { css } from 'styled-components'
  19. import EndpointLogos from '../../atoms/EndpointLogos'
  20. import CopyValue from '../../atoms/CopyValue'
  21. import StatusIcon from '../../atoms/StatusIcon'
  22. import StatusImage from '../../atoms/StatusImage'
  23. import MainDetailsTable from '../../molecules/MainDetailsTable'
  24. import CopyMultilineValue from '../../atoms/CopyMultilineValue'
  25. import type { Instance } from '../../../types/Instance'
  26. import type { MainItem } from '../../../types/MainItem'
  27. import type { Endpoint } from '../../../types/Endpoint'
  28. import type { Network } from '../../../types/Network'
  29. import type { Field as FieldType } from '../../../types/Field'
  30. import fieldHelper from '../../../types/Field'
  31. import StyleProps from '../../styleUtils/StyleProps'
  32. import Palette from '../../styleUtils/Palette'
  33. import DateUtils from '../../../utils/DateUtils'
  34. import LabelDictionary from '../../../utils/LabelDictionary'
  35. import arrowImage from './images/arrow.svg'
  36. const Wrapper = styled.div`
  37. display: flex;
  38. flex-direction: column;
  39. padding-bottom: 48px;
  40. `
  41. const ColumnsLayout = styled.div`
  42. display: flex;
  43. `
  44. const Column = styled.div`
  45. ${props => StyleProps.exactWidth(props.width)}
  46. `
  47. const Arrow = styled.div`
  48. width: 34px;
  49. height: 24px;
  50. background: url('${arrowImage}') center no-repeat;
  51. margin-top: 84px;
  52. `
  53. const Row = styled.div`
  54. margin-bottom: 32px;
  55. &:last-child {
  56. margin-bottom: 16px;
  57. }
  58. `
  59. const Field = styled.div`
  60. display: flex;
  61. flex-direction: column;
  62. `
  63. const Label = styled.div`
  64. font-size: 10px;
  65. color: ${Palette.grayscale[3]};
  66. font-weight: ${StyleProps.fontWeights.medium};
  67. text-transform: uppercase;
  68. display: flex;
  69. align-items: center;
  70. `
  71. const StatusIconStub = styled.div`
  72. ${StyleProps.exactSize('16px')}
  73. `
  74. const Value = styled.div`
  75. display: ${props => props.flex ? 'flex' : props.block ? 'block' : 'inline-table'};
  76. margin-top: 3px;
  77. ${props => props.capitalize ? 'text-transform: capitalize;' : ''}
  78. `
  79. const ValueLink = styled(Link)`
  80. display: flex;
  81. margin-top: 3px;
  82. color: ${Palette.primary};
  83. text-decoration: none;
  84. cursor: pointer;
  85. `
  86. const Loading = styled.div`
  87. display: flex;
  88. justify-content: center;
  89. align-items: center;
  90. height: 200px;
  91. `
  92. const PropertiesTable = styled.div``
  93. const PropertyRow = styled.div`
  94. display: flex;
  95. justify-content: space-between;
  96. margin-bottom: 4px;
  97. `
  98. const PropertyText = css``
  99. const PropertyName = styled.div`
  100. ${PropertyText}
  101. overflow: hidden;
  102. text-overflow: ellipsis;
  103. max-width: 50%;
  104. `
  105. const PropertyValue = styled.div`
  106. ${PropertyText}
  107. color: ${Palette.grayscale[4]};
  108. text-align: right;
  109. overflow: hidden;
  110. text-overflow: ellipsis;
  111. max-width: 50%;
  112. `
  113. type Props = {
  114. item: ?MainItem,
  115. destinationSchema: FieldType[],
  116. destinationSchemaLoading: boolean,
  117. instancesDetails: Instance[],
  118. instancesDetailsLoading: boolean,
  119. endpoints: Endpoint[],
  120. networks?: Network[],
  121. bottomControls: React.Node,
  122. loading: boolean,
  123. }
  124. @observer
  125. class MainDetails extends React.Component<Props> {
  126. getSourceEndpoint(): ?Endpoint {
  127. let endpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.origin_endpoint_id)
  128. return endpoint
  129. }
  130. getDestinationEndpoint(): ?Endpoint {
  131. let endpoint = this.props.endpoints.find(e => this.props.item && e.id === this.props.item.destination_endpoint_id)
  132. return endpoint
  133. }
  134. getLastExecution() {
  135. if (this.props.item && this.props.item.executions && this.props.item.executions.length) {
  136. return this.props.item.executions[this.props.item.executions.length - 1]
  137. }
  138. return this.props.item || {}
  139. }
  140. getConnectedVms(networkId: string) {
  141. if (this.props.instancesDetailsLoading) {
  142. return 'Loading...'
  143. }
  144. if (!this.props.item) {
  145. return '-'
  146. }
  147. let vms: string[] = []
  148. this.props.instancesDetails.forEach(instanceDet => {
  149. if (
  150. instanceDet.devices && instanceDet.devices.nics && instanceDet.devices.nics.find &&
  151. instanceDet.devices.nics.find(n => n.network_name === networkId)
  152. ) {
  153. vms.push(instanceDet.instance_name || instanceDet.name)
  154. }
  155. })
  156. return vms.length === 0 ? 'Failed to read network configuration for the original instance' : vms.map(vm => <div data-test-id={`vm-${vm}`} style={{ marginBottom: '8px' }}>{vm}<br /></div>)
  157. }
  158. renderLastExecutionTime() {
  159. let lastExecution = this.getLastExecution()
  160. let lastUpdate = lastExecution.updated_at || lastExecution.created_at
  161. if (lastUpdate) {
  162. return this.renderValue(DateUtils.getLocalTime(lastUpdate).format('YYYY-MM-DD HH:mm:ss'))
  163. }
  164. return null
  165. }
  166. renderValue(value: string, dateTestId?: string) {
  167. return <CopyValue value={value} maxWidth="90%" data-test-id={dateTestId ? `mainDetails-${dateTestId}` : undefined} />
  168. }
  169. renderEndpointLink(type: string): React.Node {
  170. let endpointIsMissing = (
  171. <Value flex data-test-id={`mainDetails-missing-${type}`}>
  172. <StatusIcon style={{ marginRight: '8px' }} status="ERROR" />Endpoint is missing
  173. </Value>
  174. )
  175. let endpoint = type === 'source' ? this.getSourceEndpoint() : this.getDestinationEndpoint()
  176. if (endpoint) {
  177. return <ValueLink data-test-id={`mainDetails-name-${type}`} to={`/endpoint/${endpoint.id}`}>{endpoint.name}</ValueLink>
  178. }
  179. return endpointIsMissing
  180. }
  181. renderPropertiesTable(propertyNames: string[]) {
  182. let getValue = (name: string, value: any) => {
  183. if (value.join && value.length && value[0].destination && value[0].source) {
  184. return value.map(v => `${v.source}=${v.destination}`).join(', ')
  185. }
  186. return fieldHelper.getValueAlias(name, value, this.props.destinationSchema)
  187. }
  188. let properties = []
  189. propertyNames.forEach(pn => {
  190. let value = this.props.item ? this.props.item.destination_environment[pn] : ''
  191. let label = LabelDictionary.get(pn)
  192. if (value && value.join) {
  193. // $FlowIgnore
  194. value.forEach((v, i) => {
  195. let useLabel = i === 0 ? label : ''
  196. properties.push({ label: useLabel, value: v })
  197. })
  198. } else if (value && typeof value === 'object') {
  199. properties = properties.concat(Object.keys(value).map(p => {
  200. if (p === 'disk_mappings') {
  201. return null
  202. }
  203. let fieldName = pn
  204. if (fieldName === 'migr_image_map') {
  205. fieldName = `${p}_os_image`
  206. }
  207. return {
  208. label: `${label} - ${LabelDictionary.get(p)}`,
  209. value: getValue(fieldName, value[p]),
  210. }
  211. }))
  212. } else {
  213. properties.push({ label, value: getValue(pn, value) })
  214. }
  215. })
  216. return (
  217. <PropertiesTable>
  218. {properties.filter(Boolean).filter(p => p.value != null && p.value !== '').map(prop => {
  219. return (
  220. <PropertyRow key={prop.label}>
  221. <PropertyName>{prop.label}</PropertyName>
  222. <PropertyValue><CopyValue value={prop.value} /></PropertyValue>
  223. </PropertyRow>
  224. )
  225. })}
  226. </PropertiesTable>
  227. )
  228. }
  229. renderTable() {
  230. if (this.props.loading) {
  231. return null
  232. }
  233. const sourceEndpoint = this.getSourceEndpoint()
  234. const destinationEndpoint = this.getDestinationEndpoint()
  235. const lastUpdated = this.renderLastExecutionTime()
  236. const destEnv = this.props.item && this.props.item.destination_environment
  237. const propertyNames = destEnv ?
  238. Object.keys(destEnv).filter(k =>
  239. k !== 'network_map' && (
  240. k !== 'storage_mappings' ||
  241. (destEnv[k] != null && typeof destEnv[k] === 'object' && Object.keys(destEnv[k]).length > 0)
  242. )
  243. ) : []
  244. return (
  245. <ColumnsLayout>
  246. <Column width="42.5%">
  247. <Row>
  248. <Field>
  249. <Label>Source</Label>
  250. {this.renderEndpointLink('source')}
  251. </Field>
  252. </Row>
  253. <Row>
  254. <EndpointLogos
  255. endpoint={sourceEndpoint ? sourceEndpoint.type : ''}
  256. data-test-id="mainDetails-sourceLogo"
  257. />
  258. </Row>
  259. <Row>
  260. <Field>
  261. <Label>Id</Label>
  262. {this.renderValue(this.props.item ? this.props.item.id || '-' : '-', 'id')}
  263. </Field>
  264. </Row>
  265. <Row>
  266. <Field>
  267. <Label>Created</Label>
  268. {this.props.item && this.props.item.created_at ? this.renderValue(DateUtils.getLocalTime(this.props.item.created_at).format('YYYY-MM-DD HH:mm:ss'), 'created') : <Value>-</Value>}
  269. </Field>
  270. </Row>
  271. {this.props.item && this.props.item.notes
  272. ? (
  273. <Row >
  274. <Field>
  275. <Label>Description</Label>
  276. <CopyMultilineValue value={this.props.item.notes} data-test-id="mainDetails-description" />
  277. </Field>
  278. </Row>
  279. )
  280. : null}
  281. {lastUpdated ? (
  282. <Row>
  283. <Field>
  284. <Label>Last Updated</Label>
  285. <Value data-test-id="mainDetails-updated">{lastUpdated}</Value>
  286. </Field>
  287. </Row>
  288. ) : null}
  289. {this.props.item && this.props.item.replica_id ? (
  290. <Row>
  291. <Field>
  292. <Label>Created from Replica</Label>
  293. <ValueLink to={`/replica/${this.props.item.replica_id}`}>{this.props.item.replica_id}</ValueLink>
  294. </Field>
  295. </Row>
  296. ) : null}
  297. </Column>
  298. <Column width="9.5%">
  299. <Arrow />
  300. </Column>
  301. <Column width="48%" style={{ flexGrow: 1 }}>
  302. <Row>
  303. <Field>
  304. <Label>Target</Label>
  305. {this.renderEndpointLink('target')}
  306. </Field>
  307. </Row>
  308. <Row>
  309. <EndpointLogos
  310. endpoint={destinationEndpoint ? destinationEndpoint.type : ''}
  311. data-test-id="mainDetails-targetLogo"
  312. />
  313. </Row>
  314. {propertyNames.length > 0 ? (
  315. <Row>
  316. <Field>
  317. <Label>Properties{this.props.destinationSchemaLoading ? (
  318. <StatusIcon status="RUNNING" style={{ marginLeft: '8px' }} />
  319. ) : <StatusIconStub />
  320. }</Label>
  321. <Value block>{this.renderPropertiesTable(propertyNames)}</Value>
  322. </Field>
  323. </Row>
  324. ) : null}
  325. </Column>
  326. </ColumnsLayout>
  327. )
  328. }
  329. renderBottomControls() {
  330. if (this.props.loading) {
  331. return null
  332. }
  333. return this.props.bottomControls
  334. }
  335. renderLoading() {
  336. if (!this.props.loading && !this.props.instancesDetailsLoading) {
  337. return null
  338. }
  339. return (
  340. <Loading>
  341. <StatusImage loading data-test-id="mainDetails-loading" />
  342. </Loading>
  343. )
  344. }
  345. render() {
  346. return (
  347. <Wrapper>
  348. {this.renderTable()}
  349. {this.props.instancesDetailsLoading || this.props.loading ? null : (
  350. <MainDetailsTable
  351. item={this.props.item}
  352. instancesDetails={this.props.instancesDetails}
  353. networks={this.props.networks}
  354. />
  355. )}
  356. {this.renderLoading()}
  357. {this.renderBottomControls()}
  358. </Wrapper>
  359. )
  360. }
  361. }
  362. export default MainDetails