/* Copyright (C) 2017 Cloudbase Solutions SRL This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import React from "react"; import { observer } from "mobx-react"; import styled, { css, createGlobalStyle } from "styled-components"; import { Collapse } from "react-collapse"; import type { ProgressUpdate, Task } from "@src/@types/Task"; import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon"; import Arrow from "@src/components/ui/Arrow"; import StatusPill from "@src/components/ui/StatusComponents/StatusPill"; import CopyValue from "@src/components/ui/CopyValue"; import ProgressBar from "@src/components/ui/ProgressBar"; import CopyButton from "@src/components/ui/CopyButton"; import DomUtils from "@src/utils/DomUtils"; import { ThemePalette, ThemeProps } from "@src/components/Theme"; import DateUtils from "@src/utils/DateUtils"; import { Instance } from "@src/@types/Instance"; const GlobalStyle = createGlobalStyle` .ReactCollapse--collapse { transition: height 0.4s ease-in-out; } `; const Wrapper = styled.div` cursor: pointer; border-bottom: 1px solid white; transition: all ${ThemeProps.animations.swift}; ${props => (props.open ? `background: ${ThemePalette.grayscale[0]};` : "")} &:hover { background: ${ThemePalette.grayscale[0]}; } `; const ArrowStyled = styled(Arrow)` position: absolute; left: -24px; `; const Header = styled.div` display: flex; padding: 8px; position: relative; &:hover ${ArrowStyled} { opacity: 1; } `; const HeaderData = styled.div` display: block; ${props => (props.capitalize ? "text-transform: capitalize;" : "")} width: ${props => props.width}; color: ${props => props.black ? ThemePalette.black : ThemePalette.grayscale[4]}; padding-right: 8px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; position: relative; `; const Title = styled.div` display: flex; `; const TitleText = styled.div` overflow: hidden; white-space: nowrap; text-overflow: ellipsis; `; const Body = styled.div` display: flex; flex-direction: column; padding: 24px 8px; `; const Columns = styled.div` display: flex; `; const Column = styled.div` display: flex; flex-direction: column; `; const Row = styled.div` display: flex; margin-bottom: 24px; `; const RowData = styled.div` ${props => props.width ? css` min-width: ${props.width}; ` : ""} ${props => !props.skipPaddingLeft ? css` &:first-child { padding-left: 24px; min-width: calc(${props.width} + 21px); } ` : ""} `; const Label = styled.div` text-transform: uppercase; font-size: 10px; font-weight: ${ThemeProps.fontWeights.medium}; color: ${ThemePalette.grayscale[5]}; margin-bottom: 4px; `; const Value = styled.div` ${props => props.width ? css` width: ${props.width}; ` : ""} overflow: hidden; white-space: nowrap; text-overflow: ellipsis; ${props => props.primary ? css` color: ${ThemePalette.primary}; ` : ""} &:hover { ${props => props.primaryOnHover ? css` color: ${ThemePalette.primary}; ` : ""} } `; const DependsOnIds = styled.div` display: flex; flex-direction: column; text-transform: capitalize; `; const ExceptionText = styled.div` cursor: pointer; text-overflow: ellipsis; overflow: hidden; &:hover > span { opacity: 1; } > span { background-position-y: 4px; margin-left: 4px; } `; const ProgressUpdates = styled.div` color: ${ThemePalette.black}; `; const ProgressUpdateDiv = styled.div` display: flex; color: ${props => (props.secondary ? ThemePalette.grayscale[5] : "inherit")}; `; const ProgressUpdateDate = styled.div` min-width: ${props => props.width || "auto"}; & > span { margin-left: 24px; } `; const ProgressUpdateValue = styled.div` width: 100%; margin-right: 32px; word-break: break-word; `; const getName = (taskType?: string) => taskType ? taskType .replace(/_/g, " ") .toLowerCase() .replace(/\b(?:os)\b/gi, "OS") : "N/A"; type Props = { columnWidths: string[]; item: Task; otherItems: Task[]; open: boolean; instancesDetails: Instance[]; onDependsOnClick: (id: string) => void; onMouseDown?: (e: React.MouseEvent) => void; onMouseUp?: (e: React.MouseEvent) => void; }; @observer class TaskItem extends React.Component { getLastMessage() { let message; const progressUpdates = this.props.item.progress_updates; if (progressUpdates.length) { message = progressUpdates[progressUpdates.length - 1].message; } else { message = "-"; } return message; } getProgressPercentage( progressUpdate: ProgressUpdate ): { useLabel: boolean; value: number } | null { if (progressUpdate.total_steps && progressUpdate.current_step) { const currentStep = Math.min( progressUpdate.total_steps, progressUpdate.current_step ); return { value: Math.round((currentStep * 100) / progressUpdate.total_steps), useLabel: true, }; } const stringPercentage = progressUpdate.message.match( /.*progress.*?(100|\d{1,2})%/ )?.[1]; if (!stringPercentage) { return null; } return { value: Number(stringPercentage), useLabel: false, }; } handleExceptionTextClick(exceptionText: string) { DomUtils.copyTextToClipboard( exceptionText, "The message has been copied to clipboard", "Failed to copy the message to clipboard" ); } renderHeader(status: string) { const date = this.props.item.updated_at ? this.props.item.updated_at : this.props.item.created_at; const instance = this.props.instancesDetails.find( i => i.id === this.props.item.instance ); const instanceName = instance?.instance_name || instance?.name || this.props.item.instance; // get the last '/' path from instance name const instanceLabel = instanceName.indexOf("/") > -1 ? `.../${instanceName.substring(instanceName.lastIndexOf("/") + 1)}` : instanceName; return (
<StatusIcon status={status} style={{ marginRight: "8px" }} /> <TitleText>{getName(this.props.item.task_type)}</TitleText> {instanceLabel} {this.getLastMessage()} {date ? DateUtils.getLocalTime(date).format("YYYY-MM-DD HH:mm:ss") : "-"}
); } renderDependsOnValue() { const { depends_on: dependsOn } = this.props.item; if (!dependsOn || !dependsOn.length || !dependsOn.find(Boolean)) { return N/A; } return ( {dependsOn.map(id => id ? ( ) => { e.stopPropagation(); this.props.onDependsOnClick(id); }} onMouseDown={(e: React.MouseEvent) => { e.stopPropagation(); }} onMouseUp={(e: React.MouseEvent) => { e.stopPropagation(); }} > {getName( this.props.otherItems.find(item => item.id === id)?.task_type )} ) : null )} ); } renderProgressUpdates() { const naValue = N/A; if (!this.props.item.progress_updates.length) { return naValue; } return ( {this.props.item.progress_updates.map((update, i) => { if (!update) { return N/A; } const progressPercentage = this.getProgressPercentage(update); return ( // eslint-disable-next-line react/no-array-index-key {DateUtils.getLocalTime(update.created_at).format( "YYYY-MM-DD HH:mm:ss" )} {update.message} {progressPercentage && ( )} ); })} ); } renderExceptionDetails() { const exceptionsText = this.props.item.exception_details && this.props.item.exception_details.length && this.props.item.exception_details; let valueField; if (!exceptionsText) { valueField = N/A; } else { valueField = ( void }) => { e.stopPropagation(); this.handleExceptionTextClick(exceptionsText); }} onMouseDown={(e: { stopPropagation: () => void }) => { e.stopPropagation(); }} onMouseUp={(e: { stopPropagation: () => void }) => { e.stopPropagation(); }} > {exceptionsText} ); } return valueField; } renderBody(status: string) { const { columnWidths } = this.props; return ( {this.renderExceptionDetails()} {this.renderDependsOnValue()} {this.renderProgressUpdates()} ); } render() { const status = this.props.item.progress_updates.some(update => update.message.startsWith("WARNING") ) ? "WARNING" : this.props.item.status; return ( // eslint-disable-next-line react/jsx-props-no-spreading {this.renderHeader(status)} {this.renderBody(status)} ); } } export default TaskItem;