675 lines
21 KiB
JavaScript
675 lines
21 KiB
JavaScript
import {
|
|
Alert,
|
|
Button,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Modal,
|
|
Popover,
|
|
Result,
|
|
Steps,
|
|
Tag,
|
|
notification,
|
|
} from "antd";
|
|
import { useContext, useRef, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import {
|
|
Constants,
|
|
FormatDatetime,
|
|
WebSocketContext,
|
|
SentMessagesCommands,
|
|
GetDuration,
|
|
MyAvatar,
|
|
getUserId,
|
|
setNativeValue,
|
|
} from "../../utils";
|
|
import { InfoCircleOutlined, LockOutlined } from "@ant-design/icons";
|
|
|
|
export default function GroupTasksViewModal({ isOpen }) {
|
|
const webSocketContext = useContext(WebSocketContext);
|
|
const navigate = useNavigate();
|
|
const [notificationApi, notificationContextHolder] =
|
|
notification.useNotification();
|
|
let { paramGroupTaskId } = useParams();
|
|
|
|
const handleCancel = () => navigate(Constants.ROUTE_PATHS.GROUP_TASKS);
|
|
let currentGroupTask;
|
|
|
|
webSocketContext.GroupTasks.forEach((groupTask) => {
|
|
if (groupTask.Id === paramGroupTaskId) {
|
|
currentGroupTask = groupTask;
|
|
}
|
|
});
|
|
|
|
// invalid group task id in url specified
|
|
if (!currentGroupTask) {
|
|
return (
|
|
<Modal
|
|
open={isOpen}
|
|
width="70%"
|
|
maskClosable={true}
|
|
onCancel={handleCancel}
|
|
footer={<Button onClick={handleCancel}>Close</Button>}
|
|
>
|
|
<Result status="500" title="Group Task not found" />{" "}
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const getAlertType = (status) => {
|
|
switch (status) {
|
|
case Constants.GROUP_TASKS_STATUS.FINISHED:
|
|
return "success";
|
|
case Constants.GROUP_TASKS_STATUS.RUNNING:
|
|
return "info";
|
|
case Constants.GROUP_TASKS_STATUS.CANCELED:
|
|
return "warning";
|
|
case Constants.GROUP_TASKS_STATUS.FAILED:
|
|
return "error";
|
|
case Constants.GROUP_TASKS_STATUS.INPUT_REQUIRED:
|
|
return "info";
|
|
default:
|
|
return "info";
|
|
}
|
|
};
|
|
|
|
const getAlertMessage = (status) => {
|
|
switch (status) {
|
|
case Constants.GROUP_TASKS_STATUS.FINISHED:
|
|
return "Success";
|
|
case Constants.GROUP_TASKS_STATUS.RUNNING:
|
|
return "Task is running";
|
|
case Constants.GROUP_TASKS_STATUS.CANCELED:
|
|
return "Task cancelled";
|
|
case Constants.GROUP_TASKS_STATUS.FAILED:
|
|
return "Task failed";
|
|
case Constants.GROUP_TASKS_STATUS.INPUT_REQUIRED:
|
|
return "Input required";
|
|
default:
|
|
return "Alert message not found";
|
|
}
|
|
};
|
|
|
|
const getStepItemStatus = (status) => {
|
|
switch (status) {
|
|
case Constants.GROUP_TASKS_STATUS.FINISHED:
|
|
return "finish";
|
|
case Constants.GROUP_TASKS_STATUS.RUNNING:
|
|
return "process";
|
|
case Constants.GROUP_TASKS_STATUS.CANCELED:
|
|
case Constants.GROUP_TASKS_STATUS.FAILED:
|
|
return "error";
|
|
case Constants.GROUP_TASKS_STATUS.INPUT_REQUIRED:
|
|
return "wait";
|
|
default:
|
|
return "wait";
|
|
}
|
|
};
|
|
|
|
const handleTaskFailedTryAgainRunTaskStep = (taskStepId, step) => {
|
|
webSocketContext.SendSocketMessage(
|
|
SentMessagesCommands.TaskFailedTryAgainRunTaskStep,
|
|
{
|
|
groupTaskId: currentGroupTask.Id,
|
|
category: currentGroupTask.Category,
|
|
groupId: currentGroupTask.GroupId,
|
|
step: step,
|
|
taskStepId: taskStepId,
|
|
}
|
|
);
|
|
};
|
|
|
|
const handleTaskContinueTaskStep = (taskStepId, step) => {
|
|
const groupTasksViewModalRequiredInputsForm = document.getElementById(
|
|
"groupTasksViewModalRequiredInputsForm"
|
|
);
|
|
|
|
let canTaskContinued = true;
|
|
let taskInputs = [];
|
|
|
|
if (groupTasksViewModalRequiredInputsForm !== null) {
|
|
const specifiedTaskInputs =
|
|
groupTasksViewModalRequiredInputsForm.getElementsByTagName("input");
|
|
|
|
if (specifiedTaskInputs.length > 0) {
|
|
for (let i = 0; i < specifiedTaskInputs.length; i++) {
|
|
if (specifiedTaskInputs[i].value === "") {
|
|
canTaskContinued = false;
|
|
break;
|
|
}
|
|
|
|
taskInputs.push({
|
|
parameterName:
|
|
specifiedTaskInputs[i].id.split(
|
|
"-"
|
|
)[6] /* Format: UUID-STEP-PARAMETER_NAME */,
|
|
value: specifiedTaskInputs[i].value,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!canTaskContinued) {
|
|
notificationApi["error"]({
|
|
message: `Inputs cannot be empty`,
|
|
description: `Please fill in all inputs`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
webSocketContext.SendSocketMessage(
|
|
SentMessagesCommands.TaskContinueTaskStep,
|
|
{
|
|
groupTaskId: currentGroupTask.Id,
|
|
category: currentGroupTask.Category,
|
|
groupId: currentGroupTask.GroupId,
|
|
step: step,
|
|
taskStepId: taskStepId,
|
|
taskInputs: taskInputs,
|
|
}
|
|
);
|
|
};
|
|
|
|
const alertActionHandler = (status, taskStepId, index, taskLocked) => {
|
|
switch (status) {
|
|
case Constants.GROUP_TASKS_STATUS.FAILED:
|
|
return (
|
|
<Button
|
|
size="small"
|
|
danger
|
|
disabled={taskLocked}
|
|
onClick={() =>
|
|
handleTaskFailedTryAgainRunTaskStep(taskStepId, index + 1)
|
|
}
|
|
>
|
|
Try again
|
|
</Button>
|
|
);
|
|
case Constants.GROUP_TASKS_STATUS.INPUT_REQUIRED:
|
|
return (
|
|
<Button
|
|
size="small"
|
|
disabled={taskLocked}
|
|
onClick={() => handleTaskContinueTaskStep(taskStepId, index + 1)}
|
|
>
|
|
Continue
|
|
</Button>
|
|
);
|
|
default:
|
|
return <></>;
|
|
}
|
|
};
|
|
|
|
const getGroupTaskStepLog = (log) => {
|
|
return log.length === 0 ? (
|
|
""
|
|
) : (
|
|
<span style={{ whiteSpace: "pre-line" }}>{log}</span>
|
|
);
|
|
};
|
|
|
|
const stepsItemHandler = () => {
|
|
let groupTaskSteps = [];
|
|
let groupTasks = [];
|
|
|
|
webSocketContext.GroupTasksSteps.forEach((step) => {
|
|
if (step.GroupTasksId === paramGroupTaskId) {
|
|
groupTaskSteps.push(step);
|
|
}
|
|
});
|
|
|
|
groupTaskSteps.sort((a, b) => a.Step - b.Step);
|
|
|
|
webSocketContext.CategoryGroups.forEach((categoryGroup) => {
|
|
if (categoryGroup.category === currentGroupTask.Category) {
|
|
categoryGroup.groups.forEach((group) => {
|
|
if (currentGroupTask.GroupId === group.id) {
|
|
groupTasks = group.tasks;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
let stepItems = [];
|
|
|
|
groupTasks.forEach((groupTask, index) => {
|
|
stepItems.push({
|
|
key: index,
|
|
title:
|
|
groupTaskSteps[index] !== undefined &&
|
|
groupTaskSteps[index].Inputs !== "" ? (
|
|
<>
|
|
{groupTask.name}{" "}
|
|
<Popover
|
|
key={groupTaskSteps[index].Id + index}
|
|
trigger="click"
|
|
placement="bottomLeft"
|
|
title={
|
|
<h2
|
|
style={{
|
|
fontWeight: "bold",
|
|
color: Constants.COLORS.SECONDARY,
|
|
}}
|
|
>
|
|
Specified Task Inputs
|
|
</h2>
|
|
}
|
|
content={
|
|
<>
|
|
{groupTask.parameters.length > 0 &&
|
|
groupTask.parameters.map((task) => {
|
|
return (
|
|
<span key={task.parameterName}>
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{task.displayName}:{" "}
|
|
</span>
|
|
<span>
|
|
{
|
|
JSON.parse(groupTaskSteps[index].Inputs).find(
|
|
(step) =>
|
|
step.parameterName === task.parameterName
|
|
).value
|
|
}
|
|
</span>
|
|
<br />
|
|
</span>
|
|
);
|
|
})}
|
|
</>
|
|
}
|
|
>
|
|
<InfoCircleOutlined
|
|
style={{
|
|
fontSize: "18px",
|
|
color: Constants.COLORS.ICON_INFO,
|
|
marginBottom: "4px",
|
|
verticalAlign: "middle",
|
|
}}
|
|
/>
|
|
</Popover>{" "}
|
|
{groupTaskSteps[index]?.CreatorUserId !== undefined && (
|
|
<MyAvatar
|
|
tooltip
|
|
allUsers={webSocketContext.AllUsers}
|
|
userId={groupTaskSteps[index].CreatorUserId}
|
|
/>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{groupTask.name}{" "}
|
|
{groupTaskSteps[index]?.CreatorUserId !== undefined && (
|
|
<MyAvatar
|
|
tooltip
|
|
allUsers={webSocketContext.AllUsers}
|
|
userId={groupTaskSteps[index].CreatorUserId}
|
|
/>
|
|
)}
|
|
</>
|
|
),
|
|
status:
|
|
groupTaskSteps[index] !== undefined
|
|
? getStepItemStatus(groupTaskSteps[index].Status)
|
|
: "wait",
|
|
description:
|
|
groupTaskSteps[index] !== undefined ? (
|
|
<>
|
|
<p style={{ color: "#000" }}>
|
|
<b>Id:</b> {groupTaskSteps[index].Id}
|
|
<br />
|
|
<b>Started at:</b>{" "}
|
|
{FormatDatetime(groupTaskSteps[index].StartedAt)}
|
|
<br />
|
|
<b>Ended at:</b>{" "}
|
|
{groupTaskSteps[index].EndedAt !== "0001-01-01T00:00:00Z"
|
|
? FormatDatetime(groupTaskSteps[index].EndedAt)
|
|
: Constants.TEXT_EMPTY_PLACEHOLDER}
|
|
<br />
|
|
<b>Duration:</b>{" "}
|
|
{GetDuration(
|
|
groupTaskSteps[index].StartedAt,
|
|
groupTaskSteps[index].EndedAt
|
|
)}
|
|
</p>
|
|
|
|
<Alert
|
|
message={
|
|
<b>
|
|
{getAlertMessage(groupTaskSteps[index].Status)}{" "}
|
|
{groupTaskSteps[index].LockedByUserId !== "" && (
|
|
<>
|
|
<LockOutlined />{" "}
|
|
<MyAvatar
|
|
tooltip
|
|
allUsers={webSocketContext.AllUsers}
|
|
userId={groupTaskSteps[index].LockedByUserId}
|
|
avatarWidth={24}
|
|
/>
|
|
</>
|
|
)}
|
|
</b>
|
|
}
|
|
description={
|
|
groupTaskSteps[index].Status ===
|
|
Constants.GROUP_TASKS_STATUS.INPUT_REQUIRED ? (
|
|
<InputRequiredHandler
|
|
webSocketContext={webSocketContext}
|
|
currentGroupTask={currentGroupTask}
|
|
groupTaskParameters={groupTask.parameters}
|
|
notificationApi={notificationApi}
|
|
step={index + 1}
|
|
taskLockedByUserId={groupTaskSteps[index].LockedByUserId}
|
|
/>
|
|
) : (
|
|
getGroupTaskStepLog(groupTaskSteps[index].Log)
|
|
)
|
|
}
|
|
type={getAlertType(groupTaskSteps[index].Status)}
|
|
showIcon
|
|
action={alertActionHandler(
|
|
groupTaskSteps[index].Status,
|
|
groupTaskSteps[index].Id,
|
|
index,
|
|
groupTaskSteps[index].LockedByUserId
|
|
)}
|
|
/>
|
|
</>
|
|
) : (
|
|
""
|
|
),
|
|
});
|
|
});
|
|
|
|
return stepItems;
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
open={isOpen}
|
|
width="70%"
|
|
onCancel={handleCancel}
|
|
maskClosable={false}
|
|
footer={<Button onClick={handleCancel}>Close</Button>}
|
|
>
|
|
{notificationContextHolder}
|
|
{webSocketContext.GroupTasks.map((groupTask) => {
|
|
if (groupTask.Id === paramGroupTaskId) {
|
|
let currentGroupTask = groupTask;
|
|
|
|
return (
|
|
<div key={paramGroupTaskId}>
|
|
<h1
|
|
style={{
|
|
color: Constants.COLORS.PRIMARY,
|
|
fontWeight: "bold",
|
|
marginBottom: 0,
|
|
}}
|
|
>
|
|
{currentGroupTask.GroupName}{" "}
|
|
<Popover
|
|
trigger="click"
|
|
title={
|
|
<h2
|
|
style={{
|
|
fontWeight: "bold",
|
|
color: Constants.COLORS.SECONDARY,
|
|
}}
|
|
>
|
|
Details
|
|
</h2>
|
|
}
|
|
content={
|
|
<>
|
|
<p>
|
|
<span style={{ fontWeight: "bold" }}>ID:</span>{" "}
|
|
{paramGroupTaskId} <br />
|
|
<span style={{ fontWeight: "bold" }}>
|
|
Category:
|
|
</span>{" "}
|
|
{currentGroupTask.Category}
|
|
<br />
|
|
<span style={{ fontWeight: "bold" }}>
|
|
Started at:
|
|
</span>{" "}
|
|
{FormatDatetime(currentGroupTask.StartedAt)}
|
|
<br />
|
|
<span style={{ fontWeight: "bold" }}>
|
|
Endet at:
|
|
</span>{" "}
|
|
{FormatDatetime(currentGroupTask.EndedAt)}
|
|
<br />
|
|
<span style={{ fontWeight: "bold" }}>
|
|
Duration:
|
|
</span>{" "}
|
|
{GetDuration(
|
|
currentGroupTask.StartedAt,
|
|
currentGroupTask.EndedAt
|
|
)}
|
|
</p>
|
|
|
|
{currentGroupTask.GlobalInputs !== "[]" ? (
|
|
<>
|
|
<h2
|
|
style={{
|
|
fontWeight: "bold",
|
|
color: Constants.COLORS.SECONDARY,
|
|
}}
|
|
>
|
|
Specified Global Inputs
|
|
</h2>
|
|
|
|
<p>
|
|
{JSON.parse(currentGroupTask.GlobalInputs).map(
|
|
(globalInput) => {
|
|
return (
|
|
<span
|
|
key={
|
|
globalInput.parameterName +
|
|
globalInput.value
|
|
}
|
|
>
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{globalInput.parameterName}:{" "}
|
|
</span>
|
|
<span>
|
|
{globalInput.value !== ""
|
|
? globalInput.value
|
|
: Constants.TEXT_EMPTY_PLACEHOLDER}
|
|
</span>
|
|
<br />
|
|
</span>
|
|
);
|
|
}
|
|
)}
|
|
</p>
|
|
</>
|
|
) : null}
|
|
</>
|
|
}
|
|
>
|
|
<InfoCircleOutlined
|
|
style={{
|
|
fontSize: "18px",
|
|
color: Constants.COLORS.ICON_INFO,
|
|
marginBottom: "4px",
|
|
verticalAlign: "middle",
|
|
}}
|
|
/>
|
|
</Popover>{" "}
|
|
<MyAvatar
|
|
tooltip
|
|
allUsers={webSocketContext.AllUsers}
|
|
userId={currentGroupTask.CreatorUserId}
|
|
/>
|
|
<br />
|
|
</h1>
|
|
<div style={{ marginBottom: 16 }}>
|
|
<span>{currentGroupTask.Description}</span>
|
|
</div>
|
|
|
|
<Steps direction="vertical" items={stepsItemHandler()} />
|
|
</div>
|
|
);
|
|
}
|
|
return "";
|
|
})}
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function InputRequiredHandler({
|
|
webSocketContext,
|
|
currentGroupTask,
|
|
groupTaskParameters,
|
|
notificationApi,
|
|
step,
|
|
taskLockedByUserId,
|
|
}) {
|
|
const [inputFields, setInputFields] = useState({});
|
|
|
|
const globalInputs = JSON.parse(currentGroupTask.GlobalInputs);
|
|
|
|
const getDefaultValue = (groupTaskParameter) => {
|
|
if (globalInputs === undefined || !groupTaskParameter.global) return null;
|
|
|
|
return globalInputs.find(
|
|
(globalInput) =>
|
|
globalInput.parameterName === groupTaskParameter.parameterName
|
|
).value;
|
|
};
|
|
|
|
const getLabel = (displayName, groupTaskParameter) => {
|
|
return groupTaskParameter.global ? (
|
|
<>
|
|
{displayName}
|
|
<Tag style={{ marginLeft: 6 }} color="purple">
|
|
Global
|
|
</Tag>
|
|
</>
|
|
) : (
|
|
displayName
|
|
);
|
|
};
|
|
|
|
let lastChanges = useRef([]);
|
|
let typingTimer = useRef();
|
|
|
|
const onInputChange = (
|
|
inputValue,
|
|
currentGroupTaskId,
|
|
groupTaskParameterName
|
|
) => {
|
|
setInputFields((fields) => {
|
|
return ({ ...fields }[groupTaskParameterName] = inputValue);
|
|
});
|
|
|
|
if (taskLockedByUserId !== "") return;
|
|
|
|
lastChanges.current = lastChanges.current.filter(
|
|
(lc) =>
|
|
Date.now() - new Date(lc.changeTime) < Constants.GROUP_TASK_LOCKED_TIME
|
|
);
|
|
|
|
const lastChange = lastChanges.current.find((lC) => lC.step === step);
|
|
|
|
if (lastChange === undefined) {
|
|
lastChanges.current.push({
|
|
step: step,
|
|
changeTime: Date.now(),
|
|
});
|
|
|
|
webSocketContext.SendSocketMessage(SentMessagesCommands.TaskLocking, {
|
|
lockedByUserId: getUserId(),
|
|
groupTaskId: currentGroupTaskId,
|
|
step: step,
|
|
});
|
|
}
|
|
|
|
// sync input value with other web clients
|
|
clearTimeout(typingTimer.current);
|
|
|
|
typingTimer.current = setTimeout(() => {
|
|
webSocketContext.SendSocketMessage(
|
|
SentMessagesCommands.UpdateGroupTaskStepUserInputValue,
|
|
{
|
|
element: `${currentGroupTaskId}-${step}-${groupTaskParameterName}`,
|
|
value: inputValue,
|
|
}
|
|
);
|
|
}, 500);
|
|
};
|
|
|
|
return (
|
|
<Form layout="vertical" id="groupTasksViewModalRequiredInputsForm">
|
|
{groupTaskParameters.map((groupTaskParameter) => {
|
|
switch (groupTaskParameter.type) {
|
|
case "text":
|
|
return (
|
|
<Form.Item
|
|
key={"fitem-" + groupTaskParameter.parameterName}
|
|
label={getLabel(
|
|
groupTaskParameter.displayName,
|
|
groupTaskParameter
|
|
)}
|
|
required
|
|
>
|
|
<Input
|
|
key={"input-" + groupTaskParameter.parameterName}
|
|
id={`${currentGroupTask.Id}-${step}-${groupTaskParameter.parameterName}`}
|
|
defaultValue={getDefaultValue(groupTaskParameter)}
|
|
disabled={taskLockedByUserId !== ""}
|
|
onChange={(e) =>
|
|
onInputChange(
|
|
e.target.value,
|
|
currentGroupTask.Id,
|
|
groupTaskParameter.parameterName
|
|
)
|
|
}
|
|
value={inputFields[groupTaskParameter.parameterName]}
|
|
/>
|
|
</Form.Item>
|
|
);
|
|
case "number":
|
|
return (
|
|
<Form.Item
|
|
key={"fitem-" + groupTaskParameter.parameterName}
|
|
label={getLabel(
|
|
groupTaskParameter.displayName,
|
|
groupTaskParameter
|
|
)}
|
|
required
|
|
>
|
|
<InputNumber
|
|
key={"fitem-" + groupTaskParameter.parameterName}
|
|
id={`${currentGroupTask.Id}-${step}-${groupTaskParameter.parameterName}`}
|
|
style={{ width: "100%" }}
|
|
defaultValue={getDefaultValue(groupTaskParameter)}
|
|
disabled={taskLockedByUserId !== ""}
|
|
max={Number.MAX_SAFE_INTEGER}
|
|
onChange={(e) =>
|
|
onInputChange(
|
|
e,
|
|
currentGroupTask.Id,
|
|
groupTaskParameter.parameterName
|
|
)
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
);
|
|
default:
|
|
notificationApi["error"]({
|
|
message: `Type ${groupTaskParameter.type} not implemented`,
|
|
description: `Was specified in: ${groupTaskParameter.displayName}`,
|
|
});
|
|
return (
|
|
<p>
|
|
Type ${groupTaskParameter.type} not implemented. Was specified
|
|
in: ${groupTaskParameter.displayName}
|
|
</p>
|
|
);
|
|
}
|
|
})}
|
|
</Form>
|
|
);
|
|
}
|