admin-dashboard-web/src/Pages/GroupTasks/GroupTasksViewModal.js

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>
);
}