TaskItem.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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. import React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled, { css, createGlobalStyle } from 'styled-components'
  17. import { Collapse } from 'react-collapse'
  18. import type { ProgressUpdate, Task } from '@src/@types/Task'
  19. import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
  20. import Arrow from '@src/components/ui/Arrow'
  21. import StatusPill from '@src/components/ui/StatusComponents/StatusPill'
  22. import CopyValue from '@src/components/ui/CopyValue'
  23. import ProgressBar from '@src/components/ui/ProgressBar'
  24. import CopyButton from '@src/components/ui/CopyButton'
  25. import notificationStore from '@src/stores/NotificationStore'
  26. import DomUtils from '@src/utils/DomUtils'
  27. import { ThemePalette, ThemeProps } from '@src/components/Theme'
  28. import DateUtils from '@src/utils/DateUtils'
  29. import { Instance } from '@src/@types/Instance'
  30. const GlobalStyle = createGlobalStyle`
  31. .ReactCollapse--collapse {
  32. transition: height 0.4s ease-in-out;
  33. }
  34. `
  35. const Wrapper = styled.div<any>`
  36. cursor: pointer;
  37. border-bottom: 1px solid white;
  38. transition: all ${ThemeProps.animations.swift};
  39. ${props => (props.open ? `background: ${ThemePalette.grayscale[0]};` : '')}
  40. &:hover {
  41. background: ${ThemePalette.grayscale[0]};
  42. }
  43. `
  44. const ArrowStyled = styled(Arrow)`
  45. position: absolute;
  46. left: -24px;
  47. `
  48. const Header = styled.div<any>`
  49. display: flex;
  50. padding: 8px;
  51. position: relative;
  52. &:hover ${ArrowStyled} {
  53. opacity: 1;
  54. }
  55. `
  56. const HeaderData = styled.div<any>`
  57. display: block;
  58. ${props => (props.capitalize ? 'text-transform: capitalize;' : '')}
  59. width: ${props => props.width};
  60. color: ${props => (props.black ? ThemePalette.black : ThemePalette.grayscale[4])};
  61. padding-right: 8px;
  62. overflow: hidden;
  63. white-space: nowrap;
  64. text-overflow: ellipsis;
  65. position: relative;
  66. `
  67. const Title = styled.div<any>`
  68. display: flex;
  69. `
  70. const TitleText = styled.div<any>`
  71. overflow: hidden;
  72. white-space: nowrap;
  73. text-overflow: ellipsis;
  74. `
  75. const Body = styled.div<any>`
  76. display: flex;
  77. flex-direction: column;
  78. padding: 24px 8px;
  79. `
  80. const Columns = styled.div<any>`
  81. display: flex;
  82. `
  83. const Column = styled.div<any>`
  84. display: flex;
  85. flex-direction: column;
  86. `
  87. const Row = styled.div<any>`
  88. display: flex;
  89. margin-bottom: 24px;
  90. `
  91. const RowData = styled.div<any>`
  92. ${props => (props.width ? css`min-width: ${props.width};` : '')}
  93. ${props => (!props.skipPaddingLeft ? css`
  94. &:first-child {
  95. padding-left: 24px;
  96. min-width: calc(${props.width} + 21px);
  97. }
  98. ` : '')}
  99. `
  100. const Label = styled.div<any>`
  101. text-transform: uppercase;
  102. font-size: 10px;
  103. font-weight: ${ThemeProps.fontWeights.medium};
  104. color: ${ThemePalette.grayscale[5]};
  105. margin-bottom: 4px;
  106. `
  107. const Value = styled.div<any>`
  108. ${props => (props.width ? css`width: ${props.width};` : '')}
  109. overflow: hidden;
  110. white-space: nowrap;
  111. text-overflow: ellipsis;
  112. ${props => (props.primary ? css`color: ${ThemePalette.primary};` : '')}
  113. &:hover {
  114. ${props => (props.primaryOnHover ? css`color: ${ThemePalette.primary};` : '')}
  115. }
  116. `
  117. const DependsOnIds = styled.div<any>`
  118. display: flex;
  119. flex-direction: column;
  120. `
  121. const ExceptionText = styled.div<any>`
  122. cursor: pointer;
  123. text-overflow: ellipsis;
  124. overflow: hidden;
  125. &:hover > span {
  126. opacity: 1;
  127. }
  128. > span {
  129. background-position-y: 4px;
  130. margin-left: 4px;
  131. }
  132. `
  133. const ProgressUpdates = styled.div<any>`
  134. color: ${ThemePalette.black};
  135. `
  136. const ProgressUpdateDiv = styled.div<any>`
  137. display: flex;
  138. color: ${props => (props.secondary ? ThemePalette.grayscale[5] : 'inherit')};
  139. `
  140. const ProgressUpdateDate = styled.div<any>`
  141. min-width: ${props => props.width || 'auto'};
  142. & > span {margin-left: 24px;}
  143. `
  144. const ProgressUpdateValue = styled.div<any>`
  145. width: 100%;
  146. margin-right: 32px;
  147. word-break: break-word;
  148. `
  149. type Props = {
  150. columnWidths: string[],
  151. item: Task,
  152. open: boolean,
  153. instancesDetails: Instance[],
  154. onDependsOnClick: (id: string) => void,
  155. onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void
  156. onMouseUp?: (e: React.MouseEvent<HTMLDivElement>) => void
  157. }
  158. @observer
  159. class TaskItem extends React.Component<Props> {
  160. getLastMessage() {
  161. let message
  162. const progressUpdates = this.props.item.progress_updates
  163. if (progressUpdates.length) {
  164. message = progressUpdates[progressUpdates.length - 1].message
  165. } else {
  166. message = '-'
  167. }
  168. return message
  169. }
  170. getProgressPercentage(progressUpdate: ProgressUpdate): { useLabel: boolean, value: number } | null {
  171. if (progressUpdate.total_steps && progressUpdate.current_step) {
  172. const currentStep = Math.min(progressUpdate.total_steps, progressUpdate.current_step)
  173. return {
  174. value: Math.round((currentStep * 100) / progressUpdate.total_steps),
  175. useLabel: true,
  176. }
  177. }
  178. const stringPercentage = progressUpdate.message.match(/.*progress.*?(100|\d{1,2})%/)?.[1]
  179. if (!stringPercentage) {
  180. return null
  181. }
  182. return {
  183. value: Number(stringPercentage),
  184. useLabel: false,
  185. }
  186. }
  187. handleExceptionTextClick(exceptionText: string) {
  188. const succesful = DomUtils.copyTextToClipboard(exceptionText)
  189. if (succesful) {
  190. notificationStore.alert('The message has been copied to clipboard.')
  191. }
  192. }
  193. renderHeader() {
  194. const date = this.props.item.updated_at
  195. ? this.props.item.updated_at : this.props.item.created_at
  196. const instance = this.props.instancesDetails.find(i => i.id === this.props.item.instance)
  197. const instanceLabel = instance?.instance_name || instance?.name || this.props.item.instance
  198. return (
  199. <Header>
  200. <HeaderData capitalize width={this.props.columnWidths[0]} black>
  201. <Title>
  202. <StatusIcon status={this.props.item.status} style={{ marginRight: '8px' }} />
  203. <TitleText>{this.props.item.task_type.replace(/_/g, ' ').toLowerCase()}</TitleText>
  204. </Title>
  205. </HeaderData>
  206. <HeaderData title={instanceLabel} width={this.props.columnWidths[1]}>
  207. {instanceLabel}
  208. </HeaderData>
  209. <HeaderData width={this.props.columnWidths[2]}>
  210. {this.getLastMessage()}
  211. </HeaderData>
  212. <HeaderData width={this.props.columnWidths[3]}>
  213. {date ? DateUtils.getLocalTime(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
  214. </HeaderData>
  215. <ArrowStyled primary orientation={this.props.open ? 'up' : 'down'} opacity={this.props.open ? 1 : 0} thick />
  216. </Header>
  217. )
  218. }
  219. renderDependsOnValue() {
  220. const { depends_on: dependsOn } = this.props.item
  221. if (!dependsOn || !dependsOn.length || !dependsOn.find(Boolean)) {
  222. return <Value>N/A</Value>
  223. }
  224. return (
  225. <DependsOnIds>
  226. {dependsOn.map(id => (id ? (
  227. <Value
  228. key={id}
  229. width="140px"
  230. primaryOnHover
  231. textEllipsis
  232. onClick={(e: React.MouseEvent<HTMLDivElement>) => {
  233. e.stopPropagation()
  234. this.props.onDependsOnClick(id)
  235. }}
  236. onMouseDown={(e: React.MouseEvent<HTMLDivElement>) => { e.stopPropagation() }}
  237. onMouseUp={(e: React.MouseEvent<HTMLDivElement>) => { e.stopPropagation() }}
  238. >{id}
  239. </Value>
  240. ) : null))}
  241. </DependsOnIds>
  242. )
  243. }
  244. renderProgressUpdates() {
  245. const naValue = <Value style={{ marginLeft: '24px' }}>N/A</Value>
  246. if (!this.props.item.progress_updates.length) {
  247. return naValue
  248. }
  249. return (
  250. <ProgressUpdates>
  251. {this.props.item.progress_updates.map((update, i) => {
  252. if (!update) {
  253. return <Value>N/A</Value>
  254. }
  255. const progressPercentage = this.getProgressPercentage(update)
  256. return (
  257. // eslint-disable-next-line react/no-array-index-key
  258. <ProgressUpdateDiv key={i} secondary={i < this.props.item.progress_updates.length - 1 || this.props.item.status !== 'RUNNING'}>
  259. <ProgressUpdateDate width={this.props.columnWidths[0]}>
  260. <span>{DateUtils.getLocalTime(update.created_at).format('YYYY-MM-DD HH:mm:ss')}</span>
  261. </ProgressUpdateDate>
  262. <ProgressUpdateValue>
  263. {update.message}
  264. {progressPercentage && (
  265. <ProgressBar
  266. style={{ margin: '8px 0' }}
  267. progress={progressPercentage.value}
  268. useLabel={progressPercentage.useLabel}
  269. />
  270. )}
  271. </ProgressUpdateValue>
  272. </ProgressUpdateDiv>
  273. )
  274. })}
  275. </ProgressUpdates>
  276. )
  277. }
  278. renderExceptionDetails() {
  279. const exceptionsText = (this.props.item.exception_details
  280. && this.props.item.exception_details.length
  281. && this.props.item.exception_details)
  282. let valueField
  283. if (!exceptionsText) {
  284. valueField = <Value>N/A</Value>
  285. } else {
  286. valueField = (
  287. <ExceptionText
  288. onClick={(e: { stopPropagation: () => void }) => {
  289. e.stopPropagation(); this.handleExceptionTextClick(exceptionsText)
  290. }}
  291. onMouseDown={(e: { stopPropagation: () => void }) => { e.stopPropagation() }}
  292. onMouseUp={(e: { stopPropagation: () => void }) => { e.stopPropagation() }}
  293. >{exceptionsText}<CopyButton />
  294. </ExceptionText>
  295. )
  296. }
  297. return valueField
  298. }
  299. renderBody() {
  300. const { columnWidths } = this.props
  301. return (
  302. <Collapse isOpened={this.props.open}>
  303. <Body>
  304. <Columns>
  305. <Column style={{
  306. minWidth: `calc(${columnWidths[0]} + ${columnWidths[1]} + ${columnWidths[2]} - 16px)`,
  307. paddingRight: '16px',
  308. }}
  309. >
  310. <Row>
  311. <RowData width={columnWidths[0]}>
  312. <Label>Status</Label>
  313. <StatusPill small status={this.props.item.status} />
  314. </RowData>
  315. <RowData width={`calc(${columnWidths[1]} + ${columnWidths[2]})`}>
  316. <Label>ID</Label>
  317. <CopyValue value={this.props.item.id} width="auto" />
  318. </RowData>
  319. </Row>
  320. <Row>
  321. <RowData style={{ width: 'calc(100% - 24px)' }}>
  322. <Label>Exception Details</Label>
  323. {this.renderExceptionDetails()}
  324. </RowData>
  325. </Row>
  326. </Column>
  327. <Column style={{ minWidth: columnWidths[3] }}>
  328. <RowData skipPaddingLeft>
  329. <Label>Depends on</Label>
  330. {this.renderDependsOnValue()}
  331. </RowData>
  332. </Column>
  333. </Columns>
  334. <Row style={{ marginBottom: 0 }}>
  335. <RowData width="100%">
  336. <Label>Progress Updates</Label>
  337. </RowData>
  338. </Row>
  339. {this.renderProgressUpdates()}
  340. </Body>
  341. </Collapse>
  342. )
  343. }
  344. render() {
  345. return (
  346. // eslint-disable-next-line react/jsx-props-no-spreading
  347. <Wrapper {...this.props}>
  348. <GlobalStyle />
  349. {this.renderHeader()}
  350. {this.renderBody()}
  351. </Wrapper>
  352. )
  353. }
  354. }
  355. export default TaskItem