TaskItem.jsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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 React from 'react'
  16. import { observer } from 'mobx-react'
  17. import styled, { css } from 'styled-components'
  18. import { Collapse } from 'react-collapse'
  19. import type { Task } from '../../../types/Task'
  20. import StatusIcon from '../../atoms/StatusIcon'
  21. import Arrow from '../../atoms/Arrow'
  22. import StatusPill from '../../atoms/StatusPill'
  23. import CopyValue from '../../atoms/CopyValue'
  24. import ProgressBar from '../../atoms/ProgressBar'
  25. import CopyButton from '../../atoms/CopyButton'
  26. import notificationStore from '../../../stores/NotificationStore'
  27. import DomUtils from '../../../utils/DomUtils'
  28. import Palette from '../../styleUtils/Palette'
  29. import StyleProps from '../../styleUtils/StyleProps'
  30. import DateUtils from '../../../utils/DateUtils'
  31. const Wrapper = styled.div`
  32. cursor: pointer;
  33. border-bottom: 1px solid white;
  34. transition: all ${StyleProps.animations.swift};
  35. ${props => props.open ? `background: ${Palette.grayscale[0]};` : ''}
  36. &:hover {
  37. background: ${Palette.grayscale[0]};
  38. }
  39. `
  40. const ArrowStyled = styled(Arrow)`
  41. position: absolute;
  42. left: -24px;
  43. `
  44. const Header = styled.div`
  45. display: flex;
  46. padding: 8px;
  47. position: relative;
  48. &:hover ${ArrowStyled} {
  49. opacity: 1;
  50. }
  51. `
  52. const HeaderData = styled.div`
  53. display: block;
  54. ${props => props.capitalize ? 'text-transform: capitalize;' : ''}
  55. width: ${props => props.width};
  56. color: ${props => props.black ? Palette.black : Palette.grayscale[4]};
  57. padding-right: 8px;
  58. overflow: hidden;
  59. white-space: nowrap;
  60. text-overflow: ellipsis;
  61. position: relative;
  62. `
  63. const Title = styled.div`
  64. display: flex;
  65. `
  66. const TitleText = styled.div`
  67. overflow: hidden;
  68. white-space: nowrap;
  69. text-overflow: ellipsis;
  70. `
  71. const Body = styled.div`
  72. display: flex;
  73. flex-direction: column;
  74. padding: 24px 8px;
  75. `
  76. const Row = styled.div`
  77. display: flex;
  78. margin-bottom: 24px;
  79. &:last-child {
  80. margin-bottom: 0;
  81. }
  82. `
  83. const RowData = styled.div`
  84. ${props => props.width ? css`width: ${props.width};` : ''}
  85. &:first-child {
  86. padding-left: 24px;
  87. ${props => css`width: calc(${props.width} - 24px);`}
  88. }
  89. `
  90. const Label = styled.div`
  91. text-transform: uppercase;
  92. font-size: 10px;
  93. font-weight: ${StyleProps.fontWeights.medium};
  94. color: ${Palette.grayscale[5]};
  95. margin-bottom: 4px;
  96. `
  97. const Value = styled.div`
  98. ${props => props.width ? css`width: ${props.width};` : ''}
  99. overflow: hidden;
  100. white-space: nowrap;
  101. text-overflow: ellipsis;
  102. ${props => props.primary ? css`color: ${Palette.primary};` : ''}
  103. &:hover {
  104. ${props => props.primaryOnHover ? css`color: ${Palette.primary};` : ''}
  105. }
  106. `
  107. const ExceptionText = styled.div`
  108. cursor: pointer;
  109. &:hover > span {
  110. opacity: 1;
  111. }
  112. > span {
  113. background-position-y: 4px;
  114. margin-left: 4px;
  115. }
  116. `
  117. const ProgressUpdates = styled.div`
  118. color: ${Palette.black};
  119. `
  120. const ProgressUpdate = styled.div`
  121. display: flex;
  122. color: ${props => props.secondary ? Palette.grayscale[5] : 'inherit'};
  123. `
  124. const ProgressUpdateDate = styled.div`
  125. min-width: ${props => props.width || 'auto'};
  126. & > span {margin-left: 24px;}
  127. `
  128. const ProgressUpdateValue = styled.div`
  129. width: 100%;
  130. margin-right: 32px;
  131. `
  132. type Props = {
  133. columnWidths: string[],
  134. item: Task,
  135. open: boolean,
  136. onDependsOnClick: (id: string) => void,
  137. }
  138. @observer
  139. class TaskItem extends React.Component<Props> {
  140. getLastMessage() {
  141. let message
  142. if (this.props.item.progress_updates.length) {
  143. message = this.props.item.progress_updates[0].message
  144. } else {
  145. message = '-'
  146. }
  147. return message
  148. }
  149. getMessageProgress(message: string) {
  150. let match = message.match(/.*progress.*?(100|\d{1,2})%/)
  151. return match && match[1]
  152. }
  153. handleExceptionTextClick(exceptionText: string) {
  154. let succesful = DomUtils.copyTextToClipboard(exceptionText)
  155. if (succesful) {
  156. notificationStore.alert('The message has been copied to clipboard.')
  157. }
  158. }
  159. renderHeader() {
  160. let date = this.props.item.updated_at ? this.props.item.updated_at : this.props.item.created_at
  161. return (
  162. <Header>
  163. <HeaderData capitalize width={this.props.columnWidths[0]} black>
  164. <Title>
  165. <StatusIcon status={this.props.item.status} style={{ marginRight: '8px' }} />
  166. <TitleText>{this.props.item.task_type.replace(/_/g, ' ').toLowerCase()}</TitleText>
  167. </Title>
  168. </HeaderData>
  169. <HeaderData width={this.props.columnWidths[1]}>
  170. {this.props.item.instance}
  171. </HeaderData>
  172. <HeaderData width={this.props.columnWidths[2]}>
  173. {this.getLastMessage()}
  174. </HeaderData>
  175. <HeaderData width={this.props.columnWidths[3]}>
  176. {date ? DateUtils.getLocalTime(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
  177. </HeaderData>
  178. <ArrowStyled primary orientation={this.props.open ? 'up' : 'down'} opacity={this.props.open ? 1 : 0} thick />
  179. </Header>
  180. )
  181. }
  182. renderDependsOnValue() {
  183. if (this.props.item.depends_on && this.props.item.depends_on.length > 0 && this.props.item.depends_on[0]) {
  184. return (
  185. <Value
  186. width="calc(100% - 16px)"
  187. primaryOnHover
  188. textEllipsis
  189. onClick={e => { e.stopPropagation(); this.props.onDependsOnClick(this.props.item.depends_on[0]) }}
  190. onMouseDown={e => { e.stopPropagation() }}
  191. onMouseUp={e => { e.stopPropagation() }}
  192. >{this.props.item.depends_on[0]}</Value>)
  193. }
  194. return <Value>N/A</Value>
  195. }
  196. renderProgressUpdates() {
  197. let naValue = <Value style={{ marginLeft: '24px' }}>N/A</Value>
  198. if (!this.props.item.progress_updates.length) {
  199. return naValue
  200. }
  201. return (
  202. <ProgressUpdates>
  203. {this.props.item.progress_updates.map((update, i) => {
  204. if (!update) {
  205. return <Value>N/A</Value>
  206. }
  207. let messageProgress = this.getMessageProgress(update.message)
  208. return (
  209. <ProgressUpdate key={i} secondary={i < this.props.item.progress_updates.length - 1 || this.props.item.status !== 'RUNNING'}>
  210. <ProgressUpdateDate width={this.props.columnWidths[0]}>
  211. <span>{DateUtils.getLocalTime(update.created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
  212. </ProgressUpdateDate>
  213. <ProgressUpdateValue data-test-id={`taskItem-progressUpdateMessage-${i}`}>
  214. {update.message}
  215. {messageProgress && <ProgressBar style={{ margin: '8px 0' }} progress={Number(messageProgress)} data-test-id={`taskItem-progressBar-${i}`} />}
  216. </ProgressUpdateValue>
  217. </ProgressUpdate>
  218. )
  219. })}
  220. </ProgressUpdates>
  221. )
  222. }
  223. renderExceptionDetails() {
  224. let exceptionsText = (this.props.item.exception_details && this.props.item.exception_details.length
  225. && this.props.item.exception_details)
  226. let valueField
  227. if (!exceptionsText) {
  228. valueField = <Value>N/A</Value>
  229. } else {
  230. valueField = (
  231. <ExceptionText
  232. onClick={(e) => { e.stopPropagation(); this.handleExceptionTextClick(exceptionsText) }}
  233. onMouseDown={e => { e.stopPropagation() }}
  234. onMouseUp={e => { e.stopPropagation() }}
  235. >{exceptionsText}<CopyButton /></ExceptionText>)
  236. }
  237. return valueField
  238. }
  239. renderBody() {
  240. return (
  241. <Collapse isOpened={this.props.open} springConfig={{ stiffness: 100, damping: 20 }}>
  242. <Body>
  243. <Row>
  244. <RowData width={this.props.columnWidths[0]}>
  245. <Label>Status</Label>
  246. <StatusPill small status={this.props.item.status} />
  247. </RowData>
  248. <RowData width={`${parseInt(this.props.columnWidths[1], 10) + parseInt(this.props.columnWidths[2], 10)}%`}>
  249. <Label>ID</Label>
  250. <CopyValue value={this.props.item.id} width="auto" />
  251. </RowData>
  252. <RowData width={this.props.columnWidths[3]}>
  253. <Label>Depends on</Label>
  254. {this.renderDependsOnValue()}
  255. </RowData>
  256. </Row>
  257. <Row>
  258. <RowData width="100%">
  259. <Label>Exception Details</Label>
  260. {this.renderExceptionDetails()}
  261. </RowData>
  262. </Row>
  263. <Row style={{ marginBottom: 0 }}>
  264. <RowData width="100%">
  265. <Label>Progress Updates</Label>
  266. </RowData>
  267. </Row>
  268. {this.renderProgressUpdates()}
  269. </Body>
  270. </Collapse>
  271. )
  272. }
  273. render() {
  274. return (
  275. <Wrapper {...this.props}>
  276. {this.renderHeader()}
  277. {this.renderBody()}
  278. </Wrapper>
  279. )
  280. }
  281. }
  282. export default TaskItem