TaskItem.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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 DomUtils from "@src/utils/DomUtils";
  26. import { ThemePalette, ThemeProps } from "@src/components/Theme";
  27. import DateUtils from "@src/utils/DateUtils";
  28. import { Instance } from "@src/@types/Instance";
  29. const GlobalStyle = createGlobalStyle`
  30. .ReactCollapse--collapse {
  31. transition: height 0.4s ease-in-out;
  32. }
  33. `;
  34. const Wrapper = styled.div<any>`
  35. cursor: pointer;
  36. border-bottom: 1px solid white;
  37. transition: all ${ThemeProps.animations.swift};
  38. ${props => (props.open ? `background: ${ThemePalette.grayscale[0]};` : "")}
  39. &:hover {
  40. background: ${ThemePalette.grayscale[0]};
  41. }
  42. `;
  43. const ArrowStyled = styled(Arrow)`
  44. position: absolute;
  45. left: -24px;
  46. `;
  47. const Header = styled.div<any>`
  48. display: flex;
  49. padding: 8px;
  50. position: relative;
  51. &:hover ${ArrowStyled} {
  52. opacity: 1;
  53. }
  54. `;
  55. const HeaderData = styled.div<any>`
  56. display: block;
  57. ${props => (props.capitalize ? "text-transform: capitalize;" : "")}
  58. width: ${props => props.width};
  59. color: ${props =>
  60. 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 =>
  93. props.width
  94. ? css`
  95. min-width: ${props.width};
  96. `
  97. : ""}
  98. ${props =>
  99. !props.skipPaddingLeft
  100. ? css`
  101. &:first-child {
  102. padding-left: 24px;
  103. min-width: calc(${props.width} + 21px);
  104. }
  105. `
  106. : ""}
  107. `;
  108. const Label = styled.div<any>`
  109. text-transform: uppercase;
  110. font-size: 10px;
  111. font-weight: ${ThemeProps.fontWeights.medium};
  112. color: ${ThemePalette.grayscale[5]};
  113. margin-bottom: 4px;
  114. `;
  115. const Value = styled.div<any>`
  116. ${props =>
  117. props.width
  118. ? css`
  119. width: ${props.width};
  120. `
  121. : ""}
  122. overflow: hidden;
  123. white-space: nowrap;
  124. text-overflow: ellipsis;
  125. ${props =>
  126. props.primary
  127. ? css`
  128. color: ${ThemePalette.primary};
  129. `
  130. : ""}
  131. &:hover {
  132. ${props =>
  133. props.primaryOnHover
  134. ? css`
  135. color: ${ThemePalette.primary};
  136. `
  137. : ""}
  138. }
  139. `;
  140. const DependsOnIds = styled.div<any>`
  141. display: flex;
  142. flex-direction: column;
  143. text-transform: capitalize;
  144. `;
  145. const ExceptionText = styled.div<any>`
  146. cursor: pointer;
  147. text-overflow: ellipsis;
  148. overflow: hidden;
  149. &:hover > span {
  150. opacity: 1;
  151. }
  152. > span {
  153. background-position-y: 4px;
  154. margin-left: 4px;
  155. }
  156. `;
  157. const ProgressUpdates = styled.div<any>`
  158. color: ${ThemePalette.black};
  159. `;
  160. const ProgressUpdateDiv = styled.div<any>`
  161. display: flex;
  162. color: ${props => (props.secondary ? ThemePalette.grayscale[5] : "inherit")};
  163. `;
  164. const ProgressUpdateDate = styled.div<any>`
  165. min-width: ${props => props.width || "auto"};
  166. & > span {
  167. margin-left: 24px;
  168. }
  169. `;
  170. const ProgressUpdateValue = styled.div<any>`
  171. width: 100%;
  172. margin-right: 32px;
  173. word-break: break-word;
  174. `;
  175. const getName = (taskType?: string) =>
  176. taskType
  177. ? taskType
  178. .replace(/_/g, " ")
  179. .toLowerCase()
  180. .replace(/\b(?:os)\b/gi, "OS")
  181. : "N/A";
  182. type Props = {
  183. columnWidths: string[];
  184. item: Task;
  185. otherItems: Task[];
  186. open: boolean;
  187. instancesDetails: Instance[];
  188. onDependsOnClick: (id: string) => void;
  189. onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
  190. onMouseUp?: (e: React.MouseEvent<HTMLDivElement>) => void;
  191. };
  192. @observer
  193. class TaskItem extends React.Component<Props> {
  194. getLastMessage() {
  195. let message;
  196. const progressUpdates = this.props.item.progress_updates;
  197. if (progressUpdates.length) {
  198. message = progressUpdates[progressUpdates.length - 1].message;
  199. } else {
  200. message = "-";
  201. }
  202. return message;
  203. }
  204. getProgressPercentage(
  205. progressUpdate: ProgressUpdate
  206. ): { useLabel: boolean; value: number } | null {
  207. if (progressUpdate.total_steps && progressUpdate.current_step) {
  208. const currentStep = Math.min(
  209. progressUpdate.total_steps,
  210. progressUpdate.current_step
  211. );
  212. return {
  213. value: Math.round((currentStep * 100) / progressUpdate.total_steps),
  214. useLabel: true,
  215. };
  216. }
  217. const stringPercentage = progressUpdate.message.match(
  218. /.*progress.*?(100|\d{1,2})%/
  219. )?.[1];
  220. if (!stringPercentage) {
  221. return null;
  222. }
  223. return {
  224. value: Number(stringPercentage),
  225. useLabel: false,
  226. };
  227. }
  228. handleExceptionTextClick(exceptionText: string) {
  229. DomUtils.copyTextToClipboard(
  230. exceptionText,
  231. "The message has been copied to clipboard",
  232. "Failed to copy the message to clipboard"
  233. );
  234. }
  235. renderHeader(status: string) {
  236. const date = this.props.item.updated_at
  237. ? this.props.item.updated_at
  238. : this.props.item.created_at;
  239. const instance = this.props.instancesDetails.find(
  240. i => i.id === this.props.item.instance
  241. );
  242. const instanceName =
  243. instance?.instance_name || instance?.name || this.props.item.instance;
  244. // get the last '/' path from instance name
  245. const instanceLabel =
  246. instanceName.indexOf("/") > -1
  247. ? `.../${instanceName.substring(instanceName.lastIndexOf("/") + 1)}`
  248. : instanceName;
  249. return (
  250. <Header>
  251. <HeaderData capitalize width={this.props.columnWidths[0]} black>
  252. <Title>
  253. <StatusIcon status={status} style={{ marginRight: "8px" }} />
  254. <TitleText>{getName(this.props.item.task_type)}</TitleText>
  255. </Title>
  256. </HeaderData>
  257. <HeaderData title={instanceName} width={this.props.columnWidths[1]}>
  258. {instanceLabel}
  259. </HeaderData>
  260. <HeaderData width={this.props.columnWidths[2]}>
  261. {this.getLastMessage()}
  262. </HeaderData>
  263. <HeaderData width={this.props.columnWidths[3]}>
  264. {date
  265. ? DateUtils.getLocalTime(date).format("YYYY-MM-DD HH:mm:ss")
  266. : "-"}
  267. </HeaderData>
  268. <ArrowStyled
  269. primary
  270. orientation={this.props.open ? "up" : "down"}
  271. opacity={this.props.open ? 1 : 0}
  272. thick
  273. />
  274. </Header>
  275. );
  276. }
  277. renderDependsOnValue() {
  278. const { depends_on: dependsOn } = this.props.item;
  279. if (!dependsOn || !dependsOn.length || !dependsOn.find(Boolean)) {
  280. return <Value>N/A</Value>;
  281. }
  282. return (
  283. <DependsOnIds>
  284. {dependsOn.map(id =>
  285. id ? (
  286. <Value
  287. key={id}
  288. width="140px"
  289. primaryOnHover
  290. textEllipsis
  291. onClick={(e: React.MouseEvent<HTMLDivElement>) => {
  292. e.stopPropagation();
  293. this.props.onDependsOnClick(id);
  294. }}
  295. onMouseDown={(e: React.MouseEvent<HTMLDivElement>) => {
  296. e.stopPropagation();
  297. }}
  298. onMouseUp={(e: React.MouseEvent<HTMLDivElement>) => {
  299. e.stopPropagation();
  300. }}
  301. >
  302. {getName(
  303. this.props.otherItems.find(item => item.id === id)?.task_type
  304. )}
  305. </Value>
  306. ) : null
  307. )}
  308. </DependsOnIds>
  309. );
  310. }
  311. renderProgressUpdates() {
  312. const naValue = <Value style={{ marginLeft: "24px" }}>N/A</Value>;
  313. if (!this.props.item.progress_updates.length) {
  314. return naValue;
  315. }
  316. return (
  317. <ProgressUpdates>
  318. {this.props.item.progress_updates.map((update, i) => {
  319. if (!update) {
  320. return <Value key={i}>N/A</Value>;
  321. }
  322. const progressPercentage = this.getProgressPercentage(update);
  323. return (
  324. // eslint-disable-next-line react/no-array-index-key
  325. <ProgressUpdateDiv
  326. key={i}
  327. secondary={
  328. i < this.props.item.progress_updates.length - 1 ||
  329. this.props.item.status !== "RUNNING"
  330. }
  331. >
  332. <ProgressUpdateDate width={this.props.columnWidths[0]}>
  333. <span>
  334. {DateUtils.getLocalTime(update.created_at).format(
  335. "YYYY-MM-DD HH:mm:ss"
  336. )}
  337. </span>
  338. </ProgressUpdateDate>
  339. <ProgressUpdateValue>
  340. {update.message}
  341. {progressPercentage && (
  342. <ProgressBar
  343. style={{ margin: "8px 0" }}
  344. progress={progressPercentage.value}
  345. useLabel={progressPercentage.useLabel}
  346. />
  347. )}
  348. </ProgressUpdateValue>
  349. </ProgressUpdateDiv>
  350. );
  351. })}
  352. </ProgressUpdates>
  353. );
  354. }
  355. renderExceptionDetails() {
  356. const exceptionsText =
  357. this.props.item.exception_details &&
  358. this.props.item.exception_details.length &&
  359. this.props.item.exception_details;
  360. let valueField;
  361. if (!exceptionsText) {
  362. valueField = <Value>N/A</Value>;
  363. } else {
  364. valueField = (
  365. <ExceptionText
  366. onClick={(e: { stopPropagation: () => void }) => {
  367. e.stopPropagation();
  368. this.handleExceptionTextClick(exceptionsText);
  369. }}
  370. onMouseDown={(e: { stopPropagation: () => void }) => {
  371. e.stopPropagation();
  372. }}
  373. onMouseUp={(e: { stopPropagation: () => void }) => {
  374. e.stopPropagation();
  375. }}
  376. >
  377. {exceptionsText}
  378. <CopyButton />
  379. </ExceptionText>
  380. );
  381. }
  382. return valueField;
  383. }
  384. renderBody(status: string) {
  385. const { columnWidths } = this.props;
  386. return (
  387. <Collapse isOpened={this.props.open}>
  388. <Body>
  389. <Columns>
  390. <Column
  391. style={{
  392. minWidth: `calc(${columnWidths[0]} + ${columnWidths[1]} + ${columnWidths[2]} - 16px)`,
  393. paddingRight: "16px",
  394. }}
  395. >
  396. <Row>
  397. <RowData width={columnWidths[0]}>
  398. <Label>Status</Label>
  399. <StatusPill small status={status} />
  400. </RowData>
  401. <RowData
  402. width={`calc(${columnWidths[1]} + ${columnWidths[2]})`}
  403. >
  404. <Label>ID</Label>
  405. <CopyValue value={this.props.item.id} width="auto" />
  406. </RowData>
  407. </Row>
  408. <Row>
  409. <RowData style={{ width: "calc(100% - 24px)" }}>
  410. <Label>Exception Details</Label>
  411. {this.renderExceptionDetails()}
  412. </RowData>
  413. </Row>
  414. </Column>
  415. <Column style={{ minWidth: columnWidths[3] }}>
  416. <RowData skipPaddingLeft>
  417. <Label>Depends on</Label>
  418. {this.renderDependsOnValue()}
  419. </RowData>
  420. </Column>
  421. </Columns>
  422. <Row style={{ marginBottom: 0 }}>
  423. <RowData width="100%">
  424. <Label>Progress Updates</Label>
  425. </RowData>
  426. </Row>
  427. {this.renderProgressUpdates()}
  428. </Body>
  429. </Collapse>
  430. );
  431. }
  432. render() {
  433. const status = this.props.item.progress_updates.some(update =>
  434. update.message.startsWith("WARNING")
  435. )
  436. ? "WARNING"
  437. : this.props.item.status;
  438. return (
  439. // eslint-disable-next-line react/jsx-props-no-spreading
  440. <Wrapper {...this.props}>
  441. <GlobalStyle />
  442. {this.renderHeader(status)}
  443. {this.renderBody(status)}
  444. </Wrapper>
  445. );
  446. }
  447. }
  448. export default TaskItem;