added robotics

main
alex 2023-10-13 23:15:26 +02:00
parent c080c2a9af
commit 4acbe50f8b
11 changed files with 590 additions and 23 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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() {
<UserProfileProvider>
<UsersProvider>
<ConsolesProvider>
<WebSocketProvider
userSession={userSession}
setUserSession={setUserSession}
isWebSocketReady={isWebSocketReady}
setIsWebSocketReady={setIsWebSocketReady}
notificationApi={notificationApi}
>
<ReconnectingView isWebSocketReady={isWebSocketReady} />
<DashboardLayout
<RoboticsRobotProvider>
<WebSocketProvider
userSession={userSession}
setUserSession={setUserSession}
/>
</WebSocketProvider>
isWebSocketReady={isWebSocketReady}
setIsWebSocketReady={setIsWebSocketReady}
notificationApi={notificationApi}
>
<ReconnectingView
isWebSocketReady={isWebSocketReady}
/>
<DashboardLayout
userSession={userSession}
setUserSession={setUserSession}
/>
</WebSocketProvider>
</RoboticsRobotProvider>
</ConsolesProvider>
</UsersProvider>
</UserProfileProvider>

View File

@ -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
) && (
<Route
path={Constants.ROUTE_PATHS.ROBOTICS_ROBOTS}
element={
<SuspenseFallback>
<RoboticsRobots />
</SuspenseFallback>
}
/>
)}
<Route
path="*"
element={

View File

@ -92,8 +92,6 @@ export default function LogCard({
sseEventSource.current.onmessage = (event) => {
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;
});
};

View File

@ -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: <SnippetsOutlined />,
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: <RobotOutlined />,
key: Constants.ROUTE_PATHS.ROBOTICS_ROBOTS,
});
items.push(roboticsGroup);
}
// admin area
if (
hasOnePermission(

View File

@ -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 (
<RoboticsRobot.Provider
value={{
robots,
setRobots,
robotsTotalPages,
setRobotsTotalPages,
unauthorizedRobots,
setUnauthorizedRobots,
unauthorizedRobotsTotalPages,
setUnauthorizedRobotsTotalPages,
}}
>
{children}
</RoboticsRobot.Provider>
);
}

View File

@ -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];

View File

@ -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 <Badge status="success" text={"Idle"} />;
case 2:
return <Badge status="processing" text={"Running"} />;
case 3:
return <Badge status="warning" text={"Connecting"} />;
case 4:
return <Badge status="warning" text={"Error"} />;
case 5:
return <Badge status="default" text={"Offline"} />;
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) => (
<Space size="middle">
<Link to="#">{t("common.text.edit")}</Link>
<Link to="#">{t("common.text.disconnect")}</Link>
</Space>
),
},
];
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) => (
<Space size="middle">
<Popconfirm
placement="left"
title={t("robotics.unauthorizedRobots.popconfirmDeny.title")}
description={t(
"robotics.unauthorizedRobots.popconfirmDeny.description"
)}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() =>
myFetch(
`/robot/deny/${record.id}`,
"DELETE",
null,
{},
myFetchContentType.JSON,
Constants.ROBOTICS_API_ADDRESS
).then((data) => {
console.log("data", data);
})
}
>
<Link to="#">{t("common.text.deny")}</Link>
</Popconfirm>
<Popconfirm
placement="left"
title={t("robotics.unauthorizedRobots.popconfirmAuthorize.title")}
description={t(
"robotics.unauthorizedRobots.popconfirmAuthorize.description"
)}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() =>
myFetch(
`/robot/authorize/${record.id}`,
"POST",
null,
{},
myFetchContentType.JSON,
Constants.ROBOTICS_API_ADDRESS
).then((data) => {
console.log("data", data);
})
}
>
<Link to="#">{t("common.text.authorize")}</Link>
</Popconfirm>
</Space>
),
},
];
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 (
<>
<Typography.Title level={4}>
{t("robotics.robots.header")} ({robotsContext.robots.length})
</Typography.Title>
<Table
scroll={{ x: "max-content" }}
columns={getRobotsTableContent()}
dataSource={getRobotsTableItems(robotsContext.robots)}
pagination={false}
/>
<MyPagination
paginationPage={robotsPaginationPage}
setPaginationPage={(page) => setRobotsPaginationPage(page)}
totalPages={robotsContext.robotsTotalPages}
/>
<Typography.Title level={4}>
{t("robotics.unauthorizedRobots.header")} (
{robotsContext.unauthorizedRobots.length})
</Typography.Title>
<Table
scroll={{ x: "max-content" }}
columns={getUnauthorizedTableContent()}
dataSource={getUnauthorizedTableItems(robotsContext.unauthorizedRobots)}
pagination={false}
/>
<MyPagination
paginationPage={unauthorizedRobotsPaginationPage}
setPaginationPage={(page) => setUnauthorizedRobotsPaginationPage(page)}
totalPages={robotsContext.unauthorizedRobotsTotalPages}
/>
</>
);
}

View File

@ -88,7 +88,7 @@ export default function UserProfile() {
return (
<Space size="middle">
<Link
href="#"
to="#"
onClick={() => myFetch(`/user/session/${record.key}`, "DELETE")}
>
{t("userProfile.column.action.signOut")}

View File

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