index.jsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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. `
  123. const ProgressUpdateDate = styled.div`
  124. min-width: ${props => props.width || 'auto'};
  125. & > span {margin-left: 24px;}
  126. `
  127. const ProgressUpdateValue = styled.div`
  128. width: 100%;
  129. margin-right: 32px;
  130. `
  131. type Props = {
  132. columnWidths: string[],
  133. item: Task,
  134. open: boolean,
  135. onDependsOnClick: (id: string) => void,
  136. }
  137. @observer
  138. class TaskItem extends React.Component<Props> {
  139. getLastMessage() {
  140. let message
  141. if (this.props.item.progress_updates.length) {
  142. message = this.props.item.progress_updates[this.props.item.progress_updates.length - 1].message
  143. } else {
  144. message = '-'
  145. }
  146. return message
  147. }
  148. getMessageProgress(message: string) {
  149. let match = message.match(/.*progress.*?(100|\d{1,2})%/)
  150. return match && match[1]
  151. }
  152. handleExceptionTextClick(exceptionText: string) {
  153. let succesful = DomUtils.copyTextToClipboard(exceptionText)
  154. if (succesful) {
  155. NotificationStore.notify('The message has been copied to clipboard.')
  156. }
  157. }
  158. renderHeader() {
  159. let date = this.props.item.updated_at ? this.props.item.updated_at : this.props.item.created_at
  160. return (
  161. <Header>
  162. <HeaderData capitalize width={this.props.columnWidths[0]} black>
  163. <Title>
  164. <StatusIcon status={this.props.item.status} style={{ marginRight: '8px' }} />
  165. <TitleText>{this.props.item.task_type.replace(/_/g, ' ').toLowerCase()}</TitleText>
  166. </Title>
  167. </HeaderData>
  168. <HeaderData width={this.props.columnWidths[1]}>
  169. {this.props.item.instance}
  170. </HeaderData>
  171. <HeaderData width={this.props.columnWidths[2]}>
  172. {this.getLastMessage()}
  173. </HeaderData>
  174. <HeaderData width={this.props.columnWidths[3]}>
  175. {date ? DateUtils.getLocalTime(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
  176. </HeaderData>
  177. <ArrowStyled primary orientation={this.props.open ? 'up' : 'down'} opacity={this.props.open ? 1 : 0} />
  178. </Header>
  179. )
  180. }
  181. renderDependsOnValue() {
  182. if (this.props.item.depends_on && this.props.item.depends_on.length > 0 && this.props.item.depends_on[0]) {
  183. return (
  184. <Value
  185. width="calc(100% - 16px)"
  186. primaryOnHover
  187. textEllipsis
  188. onClick={e => { e.stopPropagation(); this.props.onDependsOnClick(this.props.item.depends_on[0]) }}
  189. onMouseDown={e => { e.stopPropagation() }}
  190. onMouseUp={e => { e.stopPropagation() }}
  191. >{this.props.item.depends_on[0]}</Value>)
  192. }
  193. return <Value>N/A</Value>
  194. }
  195. renderProgressUpdates() {
  196. let naValue = <Value style={{ marginLeft: '24px' }}>N/A</Value>
  197. if (!this.props.item.progress_updates.length) {
  198. return naValue
  199. }
  200. return (
  201. <ProgressUpdates>
  202. {this.props.item.progress_updates.map((update, i) => {
  203. if (!update) {
  204. return <Value>N/A</Value>
  205. }
  206. let messageProgress = this.getMessageProgress(update.message)
  207. return (
  208. <ProgressUpdate key={i}>
  209. <ProgressUpdateDate width={this.props.columnWidths[0]}>
  210. <span>{DateUtils.getLocalTime(update.created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
  211. </ProgressUpdateDate>
  212. <ProgressUpdateValue data-test-id={`taskItem-progressUpdateMessage-${i}`}>
  213. {update.message}
  214. {messageProgress && <ProgressBar style={{ margin: '8px 0' }} progress={Number(messageProgress)} data-test-id={`taskItem-progressBar-${i}`} />}
  215. </ProgressUpdateValue>
  216. </ProgressUpdate>
  217. )
  218. })}
  219. </ProgressUpdates>
  220. )
  221. }
  222. renderExceptionDetails() {
  223. let exceptionsText = (this.props.item.exception_details && this.props.item.exception_details.length
  224. && this.props.item.exception_details)
  225. let valueField
  226. if (!exceptionsText) {
  227. valueField = <Value>N/A</Value>
  228. } else {
  229. valueField = (
  230. <ExceptionText
  231. onClick={(e) => { e.stopPropagation(); this.handleExceptionTextClick(exceptionsText) }}
  232. onMouseDown={e => { e.stopPropagation() }}
  233. onMouseUp={e => { e.stopPropagation() }}
  234. >{exceptionsText}<CopyButton /></ExceptionText>)
  235. }
  236. return valueField
  237. }
  238. renderBody() {
  239. return (
  240. <Collapse isOpened={this.props.open} springConfig={{ stiffness: 100, damping: 20 }}>
  241. <Body>
  242. <Row>
  243. <RowData width={this.props.columnWidths[0]}>
  244. <Label>Status</Label>
  245. <StatusPill small status={this.props.item.status} />
  246. </RowData>
  247. <RowData width={`${parseInt(this.props.columnWidths[1], 10) + parseInt(this.props.columnWidths[2], 10)}%`}>
  248. <Label>ID</Label>
  249. <CopyValue value={this.props.item.id} width="auto" />
  250. </RowData>
  251. <RowData width={this.props.columnWidths[3]}>
  252. <Label>Depends on</Label>
  253. {this.renderDependsOnValue()}
  254. </RowData>
  255. </Row>
  256. <Row>
  257. <RowData width="100%">
  258. <Label>Exception Details</Label>
  259. {this.renderExceptionDetails()}
  260. </RowData>
  261. </Row>
  262. <Row style={{ marginBottom: 0 }}>
  263. <RowData width="100%">
  264. <Label>Progress Updates</Label>
  265. </RowData>
  266. </Row>
  267. {this.renderProgressUpdates()}
  268. </Body>
  269. </Collapse>
  270. )
  271. }
  272. render() {
  273. return (
  274. <Wrapper {...this.props}>
  275. {this.renderHeader()}
  276. {this.renderBody()}
  277. </Wrapper>
  278. )
  279. }
  280. }
  281. export default TaskItem