/* 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 notificationStore from '@src/stores/NotificationStore' 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; ` 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; ` type Props = { columnWidths: string[], item: 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) { const succesful = DomUtils.copyTextToClipboard(exceptionText) if (succesful) { notificationStore.alert('The message has been copied to clipboard.') } } renderHeader() { 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 instanceLabel = instance?.instance_name || instance?.name || this.props.item.instance return (
<StatusIcon status={this.props.item.status} style={{ marginRight: '8px' }} /> <TitleText>{this.props.item.task_type.replace(/_/g, ' ').toLowerCase()}</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() }} >{id} ) : 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() { const { columnWidths } = this.props return ( {this.renderExceptionDetails()} {this.renderDependsOnValue()} {this.renderProgressUpdates()} ) } render() { return ( // eslint-disable-next-line react/jsx-props-no-spreading {this.renderHeader()} {this.renderBody()} ) } } export default TaskItem