diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index ab2c42f..b17472e 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -21,7 +21,11 @@ "updatedAt": "Aktualisiert am:", "createdBy": "Erstellt von:", "endedAt": "Beendet am:", - "type": "Typ:" + "type": "Typ:", + "edit": "Bearbeiten", + "authorize": "Autorisieren", + "deny": "Ablehnen", + "disconnect": "Verbindung trennen" } }, "sideMenu": { @@ -33,6 +37,10 @@ "overview": "Kategorien", "history": "Verlauf" }, + "robotics": { + "menuCategory": "Robotik", + "robots": "Roboter" + }, "adminArea": { "menuCategory": "Adminbereich", "roles": "Rollen", @@ -155,6 +163,35 @@ } } }, + "robotics": { + "robots": { + "header": "Roboter", + "column": { + "id": "ID", + "type": "Typ", + "name": "Name", + "status": "Status", + "currentJob": "Aktueller Job", + "jobsWaiting": "Wartende Jobs", + "address": "Adresse", + "connectedAt": "Verbunden am", + "firmwareVersion": "Firmware Version", + "createdAt": "Erstellt am", + "actions": "Maßnahmen" + } + }, + "unauthorizedRobots": { + "header": "Nicht autorisierte Roboter", + "popconfirmDeny": { + "title": "Sind Sie sicher, dass Sie diesen Roboter ablehnen wollen?", + "description": "Der Roboter wird getrennt und muss sich ernuet verbinden" + }, + "popconfirmAuthorize": { + "title": "Sind Sie sicher, dass Sie diesen Roboter autorisieren wollen?", + "description": "Der Roboter wird autorisiert und kann dann für Aufträge verwendet werden" + } + } + }, "logCard": { "popover": { "groupTaskId.title": "Gruppenaufgabe", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index bf8af83..79aee14 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -21,7 +21,11 @@ "updatedAt": "Updated at:", "createdBy": "Created by:", "endedAt": "Ended at:", - "type": "Type:" + "type": "Type:", + "edit": "Edit", + "authorize": "Authorize", + "deny": "Deny", + "disconnect": "Disconnect" } }, "sideMenu": { @@ -33,6 +37,10 @@ "overview": "Categories", "history": "History" }, + "robotics": { + "menuCategory": "Robotics", + "robots": "Robots" + }, "adminArea": { "menuCategory": "Admin Area", "roles": "Roles", @@ -155,6 +163,35 @@ } } }, + "robotics": { + "robots": { + "header": "Robots", + "column": { + "id": "ID", + "type": "Type", + "name": "Name", + "status": "Status", + "currentJob": "Current job", + "jobsWaiting": "Jobs waiting", + "address": "Address", + "connectedAt": "Connected at", + "firmwareVersion": "Firmware version", + "createdAt": "Created At", + "actions": "Actions" + } + }, + "unauthorizedRobots": { + "header": "Unauthorized Robots", + "popconfirmDeny": { + "title": "Are you sure you want to deny this robot?", + "description": "The robot will be disconnected and needs to reconnect" + }, + "popconfirmAuthorize": { + "title": "Are you sure you want to authorize this robot?", + "description": "The robot can than be used for jobs" + } + } + }, "logCard": { "popover": { "groupTaskId.title": "Group Task", diff --git a/src/App.js b/src/App.js index 0e96f82..af9df7f 100644 --- a/src/App.js +++ b/src/App.js @@ -14,6 +14,7 @@ import { UserProfileProvider } from "./Contexts/UserProfileContext"; import { UsersProvider } from "./Contexts/UsersContext"; import HeaderProvider from "./Contexts/HeaderContext"; import ConsolesProvider from "./Contexts/ConsolesContext"; +import { RoboticsRobotProvider } from "./Contexts/RoboticsRobot"; export default function App() { const [notificationApi, notificationContextHolder] = @@ -44,20 +45,24 @@ export default function App() { - - - - + - + isWebSocketReady={isWebSocketReady} + setIsWebSocketReady={setIsWebSocketReady} + notificationApi={notificationApi} + > + + + + + diff --git a/src/Components/AppRoutes/index.js b/src/Components/AppRoutes/index.js index c2344b7..dfdc743 100644 --- a/src/Components/AppRoutes/index.js +++ b/src/Components/AppRoutes/index.js @@ -22,6 +22,7 @@ const ViewEquipmentDocumentations = lazy(() => import("../../Pages/EquipmentDocumentation/ViewEquipmentDocumentation") ); const Consoles = lazy(() => import("../../Pages/Consoles")); +const RoboticsRobots = lazy(() => import("../../Pages/Robotics/Robots")); function SuspenseFallback({ children }) { return ( @@ -251,6 +252,20 @@ export default function AppRoutes() { /> )} + {hasPermission( + appContext.userPermissions, + Constants.PERMISSIONS.ROBOTICS.ROBOTS + ) && ( + + + + } + /> + )} + { const data = JSON.parse(event.data); - let newLogLength = 0; - setLogs((prevLogs) => { const newLogs = [...prevLogs]; @@ -101,8 +99,6 @@ export default function LogCard({ newLogs.push(log); }); - newLogLength = newLogs.length; - return newLogs; }); }; diff --git a/src/Components/SideMenu/index.js b/src/Components/SideMenu/index.js index a59ae4e..99f9a8d 100644 --- a/src/Components/SideMenu/index.js +++ b/src/Components/SideMenu/index.js @@ -6,6 +6,7 @@ import { FileTextOutlined, HistoryOutlined, LogoutOutlined, + RobotOutlined, ScanOutlined, SettingOutlined, SnippetsOutlined, @@ -101,7 +102,6 @@ export default function SideMenu({ let groupTasksGroup = { label: t("sideMenu.groupTasks.menuCategory"), type: "group", - icon: , children: [], }; @@ -148,6 +148,28 @@ export default function SideMenu({ items.push(groupTasksGroup); + // robotics + if ( + hasPermission( + appContext.userPermissions, + Constants.PERMISSIONS.ROBOTICS.ROBOTS + ) + ) { + let roboticsGroup = { + label: t("sideMenu.robotics.menuCategory"), + type: "group", + children: [], + }; + + roboticsGroup.children.push({ + label: t("sideMenu.robotics.robots"), + icon: , + key: Constants.ROUTE_PATHS.ROBOTICS_ROBOTS, + }); + + items.push(roboticsGroup); + } + // admin area if ( hasOnePermission( diff --git a/src/Contexts/RoboticsRobot.js b/src/Contexts/RoboticsRobot.js new file mode 100644 index 0000000..fc8c104 --- /dev/null +++ b/src/Contexts/RoboticsRobot.js @@ -0,0 +1,37 @@ +import { createContext, useContext, useState } from "react"; + +const preview = { + robots: [], + robotsTotalPages: 0, + unauthorizedRobots: [], + unauthorizedRobotsTotalPages: 0, +}; + +const RoboticsRobot = createContext(preview); + +export const useRoboticsRobotContext = () => useContext(RoboticsRobot); + +export function RoboticsRobotProvider({ children }) { + const [robots, setRobots] = useState([]); + const [robotsTotalPages, setRobotsTotalPages] = useState(0); + const [unauthorizedRobots, setUnauthorizedRobots] = useState([]); + const [unauthorizedRobotsTotalPages, setUnauthorizedRobotsTotalPages] = + useState(0); + + return ( + + {children} + + ); +} diff --git a/src/Handlers/WebSocketMessageHandler.js b/src/Handlers/WebSocketMessageHandler.js index 27a5892..10e63d9 100644 --- a/src/Handlers/WebSocketMessageHandler.js +++ b/src/Handlers/WebSocketMessageHandler.js @@ -109,7 +109,7 @@ export function handleWebSocketMessage( sideBarContext.setConnectedUsers(body); break; case ReceivedMessagesCommands.NewGroupTaskStarted: - // add new group task to list and remove latest group task if list length will be greater than 5 + // add new group task to list and remove latest group task if list length will be greater than pagination limit if (groupTasksContext.paginationPageRef.current === 1) { groupTasksContext.setGroupTasks((arr) => { const newArr = [...arr]; diff --git a/src/Pages/Robotics/Robots/index.js b/src/Pages/Robotics/Robots/index.js new file mode 100644 index 0000000..27a05b4 --- /dev/null +++ b/src/Pages/Robotics/Robots/index.js @@ -0,0 +1,404 @@ +import { Badge, Popconfirm, Space, Table, Typography } from "antd"; +import { useTranslation } from "react-i18next"; +import { useRoboticsRobotContext } from "../../../Contexts/RoboticsRobot"; +import { useEffect, useRef, useState } from "react"; +import { + Constants, + FormatDatetime, + myFetch, + myFetchContentType, +} from "../../../utils"; +import MyPagination from "../../../Components/MyPagination"; +import { Link } from "react-router-dom"; + +const ReceivedSSECommands = { + UpdateRobotStatus: 1, + AddUnauthorizedRobot: 2, + AddRobot: 3, + RemoveUnauthorizedRobot: 4, +}; + +function getRobotTypeString(type) { + switch (type) { + case 1: + return "Rex"; + case 2: + return "Yeet"; + default: + return "Unknown"; + } +} + +export default function Robots() { + const robotsContext = useRoboticsRobotContext(); + const { t } = useTranslation(); + + const [robotsPaginationPage, setRobotsPaginationPage] = useState(1); + const [ + unauthorizedRobotsPaginationPage, + setUnauthorizedRobotsPaginationPage, + ] = useState(1); + + const sseEventSource = useRef(null); + + const getRobotStatusBadge = (status) => { + switch (status) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + default: + return "Unknown"; + } + }; + + const getRobotsTableContent = () => { + let items = [ + { + title: t("robotics.robots.column.id"), + dataIndex: "id", + key: "id", + }, + { + title: t("robotics.robots.column.type"), + dataIndex: "type", + key: "type", + }, + { + title: t("robotics.robots.column.name"), + dataIndex: "name", + key: "name", + }, + { + title: t("robotics.robots.column.status"), + dataIndex: "status", + key: "status", + }, + { + title: t("robotics.robots.column.currentJob"), + dataIndex: "currentJob", + key: "currentJob", + }, + { + title: t("robotics.robots.column.jobsWaiting"), + dataIndex: "jobsWaiting", + key: "jobsWaiting", + }, + { + title: t("robotics.robots.column.address"), + dataIndex: "address", + key: "address", + }, + { + title: t("robotics.robots.column.connectedAt"), + dataIndex: "connectedAt", + key: "connectedAt", + }, + { + title: t("robotics.robots.column.firmwareVersion"), + dataIndex: "firmwareVersion", + key: "firmwareVersion", + }, + { + title: t("robotics.robots.column.createdAt"), + dataIndex: "createdAt", + key: "createdAt", + }, + { + title: t("robotics.robots.column.actions"), + dataIndex: "actions", + key: "actions", + render: (_, record) => ( + + {t("common.text.edit")} + {t("common.text.disconnect")} + + ), + }, + ]; + + return items; + }; + + const getRobotsTableItems = (robots) => { + let items = []; + + robots.forEach((robot) => { + items.push({ + key: robot.Id, + id: robot.Id, + type: getRobotTypeString(robot.Type), + name: robot.Name, + status: getRobotStatusBadge(robot.Status), + currentJob: robot.CurrentJob, + jobsWaiting: robot.JobsWaitingCount, + address: robot.Address, + firmwareVersion: robot.FirmwareVersion, + connectedAt: FormatDatetime(robot.ConnectedAt), + actions: robot.Actions, + }); + }); + + return items; + }; + + const getUnauthorizedTableContent = () => { + let items = [ + { + title: t("robotics.robots.column.id"), + dataIndex: "id", + key: "id", + }, + { + title: t("robotics.robots.column.type"), + dataIndex: "type", + key: "type", + }, + { + title: t("robotics.robots.column.address"), + dataIndex: "address", + key: "address", + }, + { + title: t("robotics.robots.column.connectedAt"), + dataIndex: "connectedAt", + key: "connectedAt", + }, + { + title: t("robotics.robots.column.actions"), + dataIndex: "actions", + key: "actions", + render: (_, record) => ( + + + myFetch( + `/robot/deny/${record.id}`, + "DELETE", + null, + {}, + myFetchContentType.JSON, + Constants.ROBOTICS_API_ADDRESS + ).then((data) => { + console.log("data", data); + }) + } + > + {t("common.text.deny")} + + + + myFetch( + `/robot/authorize/${record.id}`, + "POST", + null, + {}, + myFetchContentType.JSON, + Constants.ROBOTICS_API_ADDRESS + ).then((data) => { + console.log("data", data); + }) + } + > + {t("common.text.authorize")} + + + ), + }, + ]; + + return items; + }; + + const getUnauthorizedTableItems = (unauthorizedRobots) => { + let items = []; + + unauthorizedRobots.forEach((robot) => { + items.push({ + key: robot.Id, + id: robot.Id, + type: getRobotTypeString(robot.Type), + address: robot.Address, + connectedAt: FormatDatetime(robot.ConnectedAt), + actions: robot.Actions, + }); + }); + + return items; + }; + + // type = 0 => robots, type = 1 => unauthorizedRobots + const fetchRobots = (type, page = 1) => { + myFetch( + `/${type === 1 ? "u" : ""}robots?page=${page}`, + "GET", + null, + {}, + myFetchContentType.JSON, + Constants.ROBOTICS_API_ADDRESS + ).then((data) => { + if (type === 1) { + robotsContext.setUnauthorizedRobots( + data.UnauthorizedRobots === null ? [] : data.UnauthorizedRobots + ); + + robotsContext.setUnauthorizedRobotsTotalPages(data.TotalPages); + } else { + robotsContext.setRobots(data.Robots === null ? [] : data.Robots); + robotsContext.setRobotsTotalPages(data.TotalPages); + } + }); + }; + + useEffect(() => { + fetchRobots(0); + + fetchRobots(1); + + sseEventSource.current = new EventSource( + `${Constants.ROBOTICS_API_ADDRESS}/sse` + ); + + sseEventSource.current.onmessage = (event) => { + const data = JSON.parse(event.data); + + const cmd = data.Cmd; + const body = data.Body; + + console.log("sse message", data); + + switch (cmd) { + case ReceivedSSECommands.UpdateRobotStatus: + robotsContext.setRobots((arr) => { + const newArr = [...arr]; + + console.log("arr", arr); + + const index = arr.findIndex((x) => x.Id === body.RobotId); + + console.log("index", index); + + if (index !== -1) { + newArr[index].Status = body.Status; + } + + return newArr; + }); + break; + case ReceivedSSECommands.AddUnauthorizedRobot: + robotsContext.setUnauthorizedRobots((arr) => { + const newArr = [...arr]; + + const index = arr.findIndex((x) => x.Id === body.Id); + + if (index !== -1) { + newArr[index] = body; + } else { + newArr.push(body); + } + + return newArr; + }); + break; + case ReceivedSSECommands.AddRobot: + robotsContext.setRobots((arr) => { + const newArr = [...arr]; + + const index = arr.findIndex((x) => x.Id === body.Id); + + if (index !== -1) { + newArr[index] = body; + } else { + newArr.push(body); + } + + return newArr; + }); + break; + case ReceivedSSECommands.RemoveUnauthorizedRobot: + robotsContext.setUnauthorizedRobots((arr) => { + const newArr = [...arr]; + + const index = arr.findIndex((x) => x.Id === body); + + if (index !== -1) { + newArr.splice(index, 1); + } + + return newArr; + }); + break; + default: + break; + } + }; + + sseEventSource.current.onerror = (event) => console.log("sse error", event); + + sseEventSource.current.onopen = (event) => console.log("sse open", event); + + sseEventSource.current.onclose = (event) => console.log("sse close", event); + + return () => sseEventSource.current.close(); + }, []); + + return ( + <> + + {t("robotics.robots.header")} ({robotsContext.robots.length}) + + + + + setRobotsPaginationPage(page)} + totalPages={robotsContext.robotsTotalPages} + /> + + + {t("robotics.unauthorizedRobots.header")} ( + {robotsContext.unauthorizedRobots.length}) + + +
+ + setUnauthorizedRobotsPaginationPage(page)} + totalPages={robotsContext.unauthorizedRobotsTotalPages} + /> + + ); +} diff --git a/src/Pages/UserProfile/index.js b/src/Pages/UserProfile/index.js index 448d594..5d72541 100644 --- a/src/Pages/UserProfile/index.js +++ b/src/Pages/UserProfile/index.js @@ -88,7 +88,7 @@ export default function UserProfile() { return ( myFetch(`/user/session/${record.key}`, "DELETE")} > {t("userProfile.column.action.signOut")} diff --git a/src/utils.js b/src/utils.js index 8921faa..6a05b24 100644 --- a/src/utils.js +++ b/src/utils.js @@ -12,6 +12,7 @@ let apiAddress = ""; let staticContentAddress = ""; let wsAddress = ""; let logApiAddress = ""; +let roboticsApiAddress = ""; if (window.location.hostname === "localhost" && window.location.port === "") { // for docker container testing on localhost @@ -19,12 +20,14 @@ if (window.location.hostname === "localhost" && window.location.port === "") { staticContentAddress = "http://localhost/api/"; wsAddress = "ws://localhost/ws"; logApiAddress = "http://localhost/lm/v1/log"; + roboticsApiAddress = "http://localhost/rcm/v1"; } else if (window.location.hostname === "localhost") { // programming on localhost apiAddress = "http://localhost:50050/v1"; staticContentAddress = "http://localhost:50050/"; wsAddress = "ws://localhost:50050/ws"; logApiAddress = "http://127.0.0.1:50110/v1/log"; + roboticsApiAddress = "http://localhost:50055/v1"; /*} else if (window.location.hostname === "192.168.178.93") { apiAddress = "http://192.168.178.93:50050/v1"; staticContentAddress = "http://192.168.178.93:50050/"; @@ -34,7 +37,8 @@ if (window.location.hostname === "localhost" && window.location.port === "") { apiAddress = `${window.location.protocol}//${window.location.hostname}/api/v1`; staticContentAddress = `${window.location.protocol}//${window.location.hostname}/api/`; wsAddress = `${wssProtocol}${window.location.hostname}/ws`; - logApiAddress = `${wssProtocol}${window.location.hostname}/lm/v1/log`; + logApiAddress = `${window.location.protocol}${window.location.hostname}/lm/v1/log`; + roboticsApiAddress = `${window.location.protocol}${window.location.hostname}/rcm/v1`; } export const Constants = { @@ -56,6 +60,7 @@ export const Constants = { STATIC_CONTENT_ADDRESS: staticContentAddress, WS_ADDRESS: wsAddress, LOG_API_ADDRESS: logApiAddress, + ROBOTICS_API_ADDRESS: roboticsApiAddress, // robot-control-manager ROUTE_PATHS: { EQUIPMENT_DOCUMENTATION: "/equipment-documentation", EQUIPMENT_DOCUMENTATION_VIEW: "/equipment-documentation/", @@ -69,6 +74,7 @@ export const Constants = { ADMIN_AREA_LOGS: "/admin-area/logs", ADMIN_AREA_MANAGE: "/admin-area/manage", CONSOLES: "/consoles", + ROBOTICS_ROBOTS: "/robotics/robots", }, GROUP_TASKS_STATUS: { FINISHED: 1, @@ -161,6 +167,9 @@ export const Constants = { CONSOLES: { VIEW: "consoles.view", }, + ROBOTICS: { + ROBOTS: "robotics.view", + }, }, SYSTEM_LOG_TYPE: { INFO: 0, @@ -1384,7 +1393,12 @@ export function myFetch( return; } - return response.json(); + // check if response is json + if (response.headers.get("content-type")?.includes("application/json")) { + return response.json(); + } + + return response.text(); }) .catch((error) => { console.error("myFetch error:", error);