/*
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 (
{getName(this.props.item.task_type)}
{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;