import { UserOutlined } from "@ant-design/icons"; import { Avatar, Badge, Tooltip } from "antd"; import { createContext, useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Buffer } from "buffer"; /** * constants */ export const Constants = { COLORS: { PRIMARY: "#e67e22", SECONDARY: "#9b59b6", ICON_INFO: "#08c", }, TEXT_EMPTY_PLACEHOLDER: "-/-", API_ADDRESS: "http://localhost:8080/v1", STATIC_CONTENT_ADDRESS: "http://localhost:8080/", WS_ADDRESS: "ws://localhost:8080/ws", ROUTE_PATHS: { GROUP_TASKS: "/group-tasks", GROUP_TASKS_VIEW: "/group-tasks/", }, GROUP_TASKS_STATUS: { FINISHED: 1, RUNNING: 2, CANCELED: 3, FAILED: 4, INPUT_REQUIRED: 5, }, GLOBALS: { MIN_USERNAME_LENGTH: 2, MAX_USERNAME_LENGTH: 20, MIN_PASSWORD_LENGTH: 6, MAX_PASSWORD_LENGTH: 64, }, MAX_AVATAR_SIZE: 5 * 1024 * 1024, ACCEPTED_FILE_TYPES: ["image/png", "image/jpeg", "image/jpg"], GROUP_TASK_LOCKED_TIME: 3 * 1000, }; /* This ID is needed because the message is sent to all clients connected to the backend server when a task is locked and unlocked. With this ID the client checks if the respective browser tab of the user is the initiator of the lock. */ export const GroupTasksStepsLockedAndUserUpdateInputValueRememberId = window.crypto.randomUUID(); /** * user session */ export function UseUserSession() { const getUserSession = () => { return getUserSessionFromLocalStorage(); }; const [userSession, setUserSession] = useState(getUserSession()); const saveUserSession = (session) => { setUserSession(session); if (session === undefined) { localStorage.removeItem("session"); } else { localStorage.setItem("session", JSON.stringify(session)); } }; return { setUserSession: saveUserSession, userSession, }; } export function getUserSessionFromLocalStorage() { return JSON.parse(localStorage.getItem("session")); } /** * websocket */ let l = "loading..."; let webSocketContextPreview = { User: { Id: "", Username: l, Email: l, Sessions: [], }, AllUsers: [], ConnectionBadgeStatus: "error", ConnectedWebSocketUsersCount: 0, CategoryGroups: [], GroupTasks: [], GroupTasksSteps: [], Scanners: [], }; export const WebSocketContext = createContext(webSocketContextPreview); // commands received from the backend server const ReceivedMessagesCommands = { InitUserSocketConnection: 1, UpdateConnectedUsers: 2, NewGroupTaskStarted: 3, NewGroupTaskStep: 4, UpdateGroupTaskStep: 5, UpdateGroupTask: 6, ReloadingGroupTasks: 7, GroupTasksReloaded: 8, UpdateUserSessions: 9, UpdateAllUsersUserAvatar: 10, NewScanner: 11, DeleteScanner: 12, UpdateScannerUsedByUserId: 13, ScanResult: 14, UpdateScannerLastUsed: 15, TaskLocked: 16, TaskUnlocked: 17, }; // commands sent to the backend server export const SentMessagesCommands = { StartGroupTasks: 1, TaskFailedTryAgainRunTaskStep: 2, TaskContinueTaskStep: 3, ReloadGroupTasks: 4, TaskLocking: 5, }; export function WebSocketProvider({ children, userSession, setUserSession, notificationApi, }) { const [isReady, setIsReady] = useState(false); const [connectionBadgeStatus, setConnectionBadgeStatus] = useState( webSocketContextPreview.ConnectionBadgeStatus ); const [connectedWebSocketUsersCount, setConnectedWebSocketUsersCount] = useState(0); const [user, setUser] = useState(webSocketContextPreview.User); const [allUsers, setAllUsers] = useState(webSocketContextPreview.AllUsers); const [categoryGroups, setCategoryGroups] = useState( webSocketContextPreview.CategoryGroups ); // these are all group tasks that are then filtered and displayed in the respective tables for the category const [groupTasks, setGroupTasks] = useState([]); const [groupTasksSteps, setGroupTasksSteps] = useState([]); const [scanners, setScanners] = useState([]); const navigate = useNavigate(); const StartGroupTasksOpenModalRememberIdRef = useRef(null); const ws = useRef(null); const connect = () => { ws.current = new WebSocket(Constants.WS_ADDRESS + "?auth=" + userSession); ws.current.onopen = () => { console.log("connected"); setConnectionBadgeStatus("success"); setIsReady(true); }; ws.current.onmessage = (event) => { const data = JSON.parse(event.data); console.log("received message", data); const cmd = data.Cmd; const body = data.Body; switch (cmd) { case ReceivedMessagesCommands.InitUserSocketConnection: setUser(body.User); setAllUsers(body.AllUsers); setCategoryGroups(body.CategoryGroups); setGroupTasks(body.GroupTasks); setGroupTasksSteps(body.GroupTasksSteps); if (body.Scanners !== null) setScanners(body.Scanners); localStorage.setItem("userId", body.User.Id); break; case ReceivedMessagesCommands.UpdateConnectedUsers: setConnectedWebSocketUsersCount(body.WebSocketUsersCount); setAllUsers((arr) => { const newArr = [...arr]; newArr[ arr.findIndex((arr1) => arr1.Id === body.UserId) ].ConnectionStatus = body.ConnectionStatus; return newArr; }); break; case ReceivedMessagesCommands.NewGroupTaskStarted: setGroupTasks((arr) => [...arr, body]); if ( body.RememberId === StartGroupTasksOpenModalRememberIdRef.current ) { navigate(`${Constants.ROUTE_PATHS.GROUP_TASKS}/${body.Id}`); } break; case ReceivedMessagesCommands.NewGroupTaskStep: setGroupTasksSteps((arr) => [...arr, body]); break; case ReceivedMessagesCommands.UpdateGroupTaskStep: setGroupTasksSteps((arr) => { const newArr = [...arr]; newArr[arr.findIndex((arr1) => arr1.Id === body.Id)] = body; return newArr; }); break; case ReceivedMessagesCommands.UpdateGroupTask: setGroupTasks((arr) => { const newArr = [...arr]; newArr[arr.findIndex((arr1) => arr1.Id === body.Id)] = body; return newArr; }); break; case ReceivedMessagesCommands.ReloadingGroupTasks: notificationApi["warning"]({ message: `Group ${body} is reloading`, duration: 2, }); break; case ReceivedMessagesCommands.GroupTasksReloaded: setCategoryGroups((arr) => { const newArr = [...arr]; newArr[ arr.findIndex((arr1) => arr1.category === body.Category) ].groups = body.UpdatedGroups; return newArr; }); notificationApi["info"]({ message: `Group ${body.Category} reloaded`, duration: 2, }); break; case ReceivedMessagesCommands.UpdateUserSessions: setUser((arr) => ({ ...arr, Sessions: body })); break; case ReceivedMessagesCommands.UpdateAllUsersUserAvatar: setAllUsers((arr) => { const newArr = [...arr]; newArr[arr.findIndex((arr1) => arr1.Id === body.UserId)].Avatar = body.Avatar; return newArr; }); break; case ReceivedMessagesCommands.NewScanner: setScanners((arr) => [...arr, body]); break; case ReceivedMessagesCommands.DeleteScanner: setScanners((arr) => arr.filter((arr) => arr.Id !== body.Id)); break; case ReceivedMessagesCommands.UpdateScannerUsedByUserId: setScanners((arr) => { const newArr = [...arr]; newArr[ arr.findIndex((arr1) => arr1.Id === body.ScannerId) ].UsedByUserId = body.UsedByUserId; return newArr; }); break; case ReceivedMessagesCommands.ScanResult: const decodedScanResult = Buffer.from(body, "base64").toString(); if (decodedScanResult === "" || decodedScanResult === null) { notificationApi["error"]({ message: `Failed to decode scan result`, description: "See in developer console", }); console.error( "Received scan result: ", body, "Decoded result: ", decodedScanResult ); break; } notificationApi["info"]({ message: `Scan Result`, description: Buffer.from(body, "base64").toString(), }); new Audio( `${Constants.STATIC_CONTENT_ADDRESS}sounds/scan_result.mp3` ).play(); break; case ReceivedMessagesCommands.UpdateScannerLastUsed: setScanners((arr) => { const newArr = [...arr]; newArr[ arr.findIndex((arr1) => arr1.Id === body.ScannerId) ].LastUsed = body.LastUsed; return newArr; }); break; case ReceivedMessagesCommands.TaskLocked: if ( body.rememberId === GroupTasksStepsLockedAndUserUpdateInputValueRememberId ) break; console.log("body task locked", body); setGroupTasksSteps((arr) => { const newArr = [...arr]; newArr[ arr.findIndex( (arr1) => arr1.GroupTasksId === body.groupTaskId && arr1.Step === body.step ) ].LockedByUserId = body.lockedByUserId; return newArr; }); // update input value // html based DOM manipulation const foundInput = document.getElementById(body.element); console.log("here", foundInput); if (foundInput) { // this timeout is needed because the previous useState for the lockedByUserId takes some milliseconds to complete setTimeout(() => setNativeValue(foundInput, body.value), 50); } // update group task step as html based DOM manipulation only works if user has no other modal open setGroupTasksSteps((arr) => { const newArr = [...arr]; const stepIndex = arr.findIndex( (arr1) => arr1.GroupTasksId === body.groupTaskId && arr1.Step === body.step ); if (stepIndex === -1) return newArr; let inputs = []; if (newArr[stepIndex].Inputs !== "") { inputs = JSON.parse(newArr[stepIndex].Inputs); } let parameterFound = false; for (let i = 0; i < inputs.length; i++) { if (inputs[i].parameterName === body.parameterName) { inputs[i].value = body.value; parameterFound = true; break; } } if (!parameterFound) { let obj = {}; obj["parameterName"] = body.parameterName; obj["value"] = body.value; inputs.push(obj); } newArr[stepIndex].Inputs = JSON.stringify(inputs); return newArr; }); break; case ReceivedMessagesCommands.TaskUnlocked: if ( body.rememberId === GroupTasksStepsLockedAndUserUpdateInputValueRememberId ) break; setGroupTasksSteps((arr) => { const newArr = [...arr]; newArr[ arr.findIndex( (arr1) => arr1.GroupTasksId === body.GroupTaskId && arr1.Step === body.Step ) ].LockedByUserId = ""; return newArr; }); break; /*case ReceivedMessagesCommands.UpdateGroupTaskStepUserInputValue: if ( body.rememberId === GroupTasksStepsLockedAndUserUpdateInputValueRememberId ) break; // html based DOM manipulation const foundInput = document.getElementById(body.element); if (foundInput) { setNativeValue(foundInput, body.value); } // update group task step as html based DOM manipulation only works if user has no other modal open setGroupTasksSteps((arr) => { const newArr = [...arr]; const stepIndex = arr.findIndex( (arr1) => arr1.GroupTasksId === body.groupTaskId && arr1.Step === body.step ); if (stepIndex === -1) return newArr; let inputs = []; if (newArr[stepIndex].Inputs !== "") { inputs = JSON.parse(newArr[stepIndex].Inputs); } let parameterFound = false; for (let i = 0; i < inputs.length; i++) { if (inputs[i].parameterName === body.parameterName) { inputs[i].value = body.value; parameterFound = true; break; } } if (!parameterFound) { let obj = {}; obj["parameterName"] = body.parameterName; obj["value"] = body.value; inputs.push(obj); } newArr[stepIndex].Inputs = JSON.stringify(inputs); return newArr; }); break;*/ default: console.error("unknown command", cmd); break; } }; ws.current.onclose = (event) => { setIsReady(false); setConnectionBadgeStatus("error"); console.log("closed", event); // custom code defined by the backend server if (event.code === 4001 || event.code === 4002) { //Unauthorized || SessionClosed setUserSession(); window.location.href = "/"; return; } if (event.reason.code === 1005) return; console.log("reconnecting..."); setTimeout(() => connect(), 1000); }; }; useEffect(() => { connect(); //return () => socket.close(); return () => ws.current.close(); }, []); const SendSocketMessage = (cmd, body) => { if (isReady) { ws.current.send(JSON.stringify({ Cmd: cmd, Body: body })); } else { console.log("websocket not ready"); } }; return ( {children} ); } // https://stackoverflow.com/a/52486921 function setNativeValue(element, value) { let lastValue = element.value; element.value = value; let event = new Event("input", { target: element, bubbles: true }); // React 15 event.simulated = true; // React 16 let tracker = element._valueTracker; if (tracker) { tracker.setValue(lastValue); } element.dispatchEvent(event); } export function FormatDatetime(datetime) { if (datetime === "0001-01-01T00:00:00Z") { return Constants.TEXT_EMPTY_PLACEHOLDER; } return new Date(datetime).toLocaleString("de-DE"); } export function GetDuration(startTime, endTime) { if (endTime === "0001-01-01T00:00:00Z") { return Constants.TEXT_EMPTY_PLACEHOLDER; } const diff = Math.abs(new Date(startTime) - new Date(endTime)); if (diff === 0) { return "0ms"; } const milliseconds = diff % 1000; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); let result = ""; if (days > 0) { result += days + "d "; } if (hours > 0) { result += (hours % 24) + "h "; } if (minutes > 0) { result += (minutes % 60) + "m "; } if (seconds > 0) { result += (seconds % 60) + "s "; } if (milliseconds > 0) { result += milliseconds + "ms"; } return result.trim(); } export function getConnectionStatusItem(connectionStatus) { return connectionStatus === 0 ? ( ) : ( ); } export function MyAvatar({ avatarWidth, avatar, tooltip, tooltipTitle, allUsers, userId, }) { const MyDefaultAvatar = () => { return } />; }; if (avatar === undefined || avatar === null) { if (allUsers !== undefined && userId !== undefined) { const user = allUsers.find((u) => u.Id === userId); if (user === undefined) return ; avatar = user.Avatar; tooltipTitle = user.Username; } else { return ; } } const MyAvat = () => { return ( ); }; if (tooltip) { return ( <> ); } return ; } export function getUserId() { return localStorage.getItem("userId"); }