/*
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 (
{this.props.item.task_type.replace(/_/g, ' ').toLowerCase()}
{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