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