responsive design, mymodal and equipment documentation

main
alex 2023-08-20 18:32:35 +00:00
parent c50eac0545
commit 6a60f47693
21 changed files with 561 additions and 331 deletions

View File

@ -12,9 +12,9 @@
"save": "Speichern",
"delete": "Löschen",
"confirm": "Bestätigen"
}
},
"contactAdmin": "Bitte kontaktieren Sie einen Administrator"
},
"contactAdmin": "Bitte kontaktieren Sie einen Administrator",
"sideMenu": {
"dashboard": "Dashboard",
"equipmentDocumentation": "Gerätedokumentation",
@ -333,5 +333,9 @@
}
},
"header": { "scanners": "Scanner" }
},
"equipmentDocumentation": {},
"equipmentViewModal": {
"equipmentDocumentationNotFound": "Gerätedokumentation nicht gefunden"
}
}

View File

@ -12,9 +12,9 @@
"save": "Save",
"delete": "Delete",
"confirm": "Confirm"
}
},
"contactAdmin": "Please contact an administrator"
},
"contactAdmin": "Please contact an administrator",
"sideMenu": {
"dashboard": "Dashboard",
"equipmentDocumentation": "Equipment Documentation",
@ -332,5 +332,9 @@
}
},
"header": { "scanners": "Scanners" }
},
"equipmentDocumentation": {},
"equipmentViewModal": {
"equipmentDocumentationNotFound": "Equipment documentation not found"
}
}

View File

@ -51,3 +51,19 @@
padding-bottom: 20px;
}
/* full screen ant modal for an screen size smaller than 576px */
@media (max-width: 575px) {
.ant-modal {
height: -webkit-fill-available;
max-width: 100vw;
}
.ant-modal-content {
width: 100vw;
height: 100vh;
top: 0;
overflow: auto;
}
.ant-modal-centered::before {
content: unset;
}
}

View File

@ -21,6 +21,7 @@ export default function AppRoutes() {
const webSocketContext = useContext(WebSocketContext);
/*
TODO: move down
{hasPermission(
webSocketContext.User.Permissions,
Constants.PERMISSIONS.EQUIPMENT_DOCUMENTATION.VIEW
@ -37,8 +38,16 @@ export default function AppRoutes() {
<Route path="/" element={<Dashboard />} />
<Route
path="/equipment-documentation"
element={<EquipmentDocumentation />}
path={Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION}
element={<EquipmentDocumentation isEquipmentViewModalOpen={false} />}
/>
<Route
path={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_VIEW +
":paramEquipmentId"
}
element={<EquipmentDocumentation isEquipmentViewModalOpen={true} />}
/>
<Route

View File

@ -9,8 +9,7 @@ import {
Constants,
FormatDatetime,
WebSocketContext,
getUserSessionFromLocalStorage,
handleUnauthorizedStatus,
myFetch,
} from "../../utils";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
@ -31,10 +30,6 @@ export default function LogCard({ type }) {
const [selectedDate, setSelectedDate] = useState(getDate());
const [loadingSpinner, setLoadingSpinner] = useState(true);
useEffect(() => {
loadLogs(selectedDate);
}, [selectedDate]);
const getColorCode = (index) => {
const colorCodes = ["#3498db", "#9b59b6", "#1abc9c"];
const colorIndex = index % colorCodes.length;
@ -81,22 +76,12 @@ export default function LogCard({ type }) {
const loadLogs = (date) => {
setLoadingSpinner(true);
fetch(
`${Constants.API_ADDRESS}/log?type=${
type === "grouptasks" ? "g" : "s"
}&date=${date}&lang=${i18n.language}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Authorization": getUserSessionFromLocalStorage(),
},
}
myFetch(
`/log?type=${type === "grouptasks" ? "g" : "s"}&date=${date}&lang=${
i18n.language
}`,
"GET"
)
.then((res) => {
handleUnauthorizedStatus(res.status);
return res.json();
})
.then((data) => {
setLoadingSpinner(false);
@ -398,6 +383,8 @@ export default function LogCard({ type }) {
return "";
};
useEffect(() => loadLogs(selectedDate), [selectedDate]);
return (
<Card
title={selectedDate}

View File

@ -0,0 +1,76 @@
import { FileImageOutlined } from "@ant-design/icons";
import { Col, Image, Row, Tag } from "antd";
import MyStlViewer from "../MyStlViewer";
/**
* Attachments is an array of objects with the following structure:
*
* [{OriginalFileName: "test.png", SystemFileName: "7344ac51-6dc6-4a6a-94ae-6e7e4dd2e823.txt"}]
*
* Supports images, files and stl files.
*/
export default function MyAttachments({ attachments, downloadUrl }) {
const getDownloadUrl = (file) => {
return `${downloadUrl}${file.SystemFileName}`;
};
const generateTagWithDownloadLink = (file) => (
<a
key={"a" + file.SystemFileName}
href={getDownloadUrl(file)}
download="test"
target="_blank"
rel="noreferrer"
>
<Tag
icon={<FileImageOutlined />}
color="processing"
style={{ marginTop: 6 }}
>
{file.OriginalFileName}
</Tag>
</a>
);
const imageFiles = attachments.filter((file) => {
const fileExtension = file.SystemFileName.split(".")[1];
return ["png", "jpeg", "jpg", "webp"].includes(fileExtension);
});
const nonImageFiles = attachments.filter((file) => {
const fileExtension = file.SystemFileName.split(".")[1];
return !["png", "jpeg", "jpg", "webp"].includes(fileExtension);
});
const nonImageElements = nonImageFiles.map((nonImageFile) => {
const fileExtension = nonImageFile.SystemFileName.split(".")[1];
if (fileExtension === "stl") {
return (
<div key={"tag" + nonImageFile.SystemFileName}>
<div style={{ height: 300, backgroundColor: "#fff" }}>
<MyStlViewer url={getDownloadUrl(nonImageFile)} />
</div>
{generateTagWithDownloadLink(nonImageFile)}
</div>
);
} else {
return generateTagWithDownloadLink(nonImageFile);
}
});
const imageFileElements = imageFiles.map((imageFile) => (
<Col xs={24} sm={12} lg={6} key={imageFile.SystemFileName}>
<Image width={"100%"} src={getDownloadUrl(imageFile)} />
{generateTagWithDownloadLink(imageFile)}
</Col>
));
const imageRow = imageFileElements.length > 0 && (
<Row key={"imgrow"} gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
{imageFileElements}
</Row>
);
return [...nonImageElements, imageRow];
}

View File

@ -0,0 +1,60 @@
import { UserOutlined } from "@ant-design/icons";
import { Avatar, Tooltip } from "antd";
import { Constants } from "../../utils";
export function MyAvatar({
avatarWidth,
avatar,
tooltip,
tooltipTitle,
allUsers,
userId,
}) {
const MyDefaultAvatar = () => {
if (tooltip) {
return (
<Tooltip placement="top" trigger="hover" title={tooltipTitle}>
<Avatar size={avatarWidth} icon={<UserOutlined />} />
</Tooltip>
);
}
return <Avatar size={avatarWidth} icon={<UserOutlined />} />;
};
if (avatar === undefined || avatar === null || avatar === "") {
if (allUsers !== undefined && userId !== undefined) {
const user = allUsers.find((u) => u.Id === userId);
if (user === undefined) return <MyDefaultAvatar />;
avatar = user.Avatar;
tooltipTitle = user.Username;
} else {
return <MyDefaultAvatar />;
}
}
const MyAvat = () => {
if (avatar === "") return <MyDefaultAvatar />;
return (
<Avatar
size={avatarWidth}
src={Constants.STATIC_CONTENT_ADDRESS + "avatars/" + avatar}
/>
);
};
if (tooltip) {
return (
<Tooltip placement="top" trigger="hover" title={tooltipTitle}>
<>
<MyAvat />
</>
</Tooltip>
);
}
return <MyAvat />;
}

View File

@ -0,0 +1,105 @@
import { Button, Grid, Modal, Result, Spin } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { myFetch } from "../../utils";
const { useBreakpoint } = Grid;
export default function MyModal({
children,
isOpen,
onCancel,
footer = <MyModalOnlyCloseButtonFooter onCancel={onCancel} />,
}) {
const screenBreakpoint = useBreakpoint();
return (
<Modal
open={isOpen}
width={screenBreakpoint.xs ? "100vw" : "70vw"}
maskClosable={false}
onCancel={onCancel}
footer={footer}
centered={screenBreakpoint.xs}
>
{children}
</Modal>
);
}
// This modal will show a loading indicator until the data is loaded.
// If the data is loaded and no data is found, it will show a not found modal.
// If the data is loaded and data is found, it will show the children.
export function MyLazyLoadingModal({
isOpen,
onCancel,
children,
resultTitleNoDataFound,
setFoundData,
fetchUrl,
fetchType,
}) {
// for the loading indicator
const [isDataLoaded, setIsDataLoaded] = useState(false);
// response data by the fetch
const [noDataFound, setNoDataFound] = useState(false);
useEffect(() => {
if (!isOpen) return;
if (isDataLoaded) {
setIsDataLoaded(false);
setNoDataFound(false);
}
myFetch(fetchUrl, fetchType).then((data) => {
setIsDataLoaded(true);
if (!data) {
setNoDataFound(true);
return;
}
setFoundData(data);
});
}, [isOpen]);
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
{noDataFound ? (
<MyNotFoundModalContent resultTitle={resultTitleNoDataFound} />
) : isDataLoaded ? (
children
) : (
<div
style={{
display: "flex",
height: "48.3vh", // set the loading modal height to the same height as the MyNotFoundModalContent result
justifyContent: "center",
alignItems: "center",
}}
>
<Spin size="large" />
</div>
)}
</MyModal>
);
}
export function MyNotFoundModal({ isOpen, onCancel, resultTitle }) {
return (
<MyModal isOpen={isOpen} onCancel={onCancel}>
<MyNotFoundModalContent resultTitle={resultTitle} />
</MyModal>
);
}
export function MyNotFoundModalContent({ resultTitle }) {
return <Result status="500" title={resultTitle} />;
}
export function MyModalOnlyCloseButtonFooter({ onCancel }) {
const { t } = useTranslation();
return <Button onClick={onCancel}>{t("common.button.close")}</Button>;
}

View File

@ -0,0 +1,27 @@
import { useMemo } from "react";
import { StlViewer } from "react-stl-viewer";
import { Constants } from "../../utils";
export default function MyStlViewer({ url }) {
const memoizedCode = useMemo(() => {
return (
<StlViewer
style={{ top: 0, left: 0, height: "100%" }}
orbitControls
modelProps={{
color: Constants.COLORS.SECONDARY,
scale: 1,
positionX: 200,
positionY: 200,
}}
shadows
floorProps={{ gridWidth: 400 }}
url={url}
onError={(err) => console.error(err)}
onFinishLoading={(ev) => console.log(ev)}
/>
);
}, [url]);
return memoizedCode;
}

View File

@ -16,13 +16,13 @@ import { useLocation, useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import {
Constants,
MyAvatar,
WebSocketContext,
getUserId,
hasOnePermission,
hasPermission,
} from "../../utils";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../MyAvatar";
export default function SideMenu({
userSession,

View File

@ -14,7 +14,6 @@ import {
} from "antd";
import {
Constants,
MyAvatar,
SentMessagesCommands,
WebSocketContext,
hasPermission,
@ -31,6 +30,7 @@ import {
} from "@ant-design/icons";
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../../Components/MyAvatar";
const { useBreakpoint } = Grid;

View File

@ -11,7 +11,6 @@ import {
import {
Constants,
FormatDatetime,
MyAvatar,
SentMessagesCommands,
WebSocketContext,
getConnectionStatusItem,
@ -23,6 +22,7 @@ import { Link } from "react-router-dom";
import { UserAddOutlined } from "@ant-design/icons";
import CreateUserModal from "./CreateUserModal";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../Components/MyAvatar";
export default function AllUsers() {
const webSocketContext = useContext(WebSocketContext);

View File

@ -0,0 +1,68 @@
import { Constants } from "../../utils";
import { useNavigate, useParams } from "react-router-dom";
import { MyLazyLoadingModal } from "../../Components/MyModal";
import { useState } from "react";
import { Steps } from "antd";
import MyAttachments from "../../Components/MyAttachments";
export default function EquipmentViewModal({ isOpen }) {
//const { t } = useTranslation();
const navigate = useNavigate();
let { paramEquipmentId } = useParams();
const [equipment, setEquipment] = useState([]);
const handleCancel = () =>
navigate(Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION);
console.log("equipment", equipment);
return (
<MyLazyLoadingModal
isOpen={isOpen}
onCancel={handleCancel}
resultTitleNoDataFound={"Not found"}
setFoundData={setEquipment}
fetchUrl={`/equipment/documentation/${paramEquipmentId}`}
fetchType={"GET"}
>
<h1>This is my children</h1>
<Steps
direction="vertical"
current={1}
items={[
{
title: "Finished",
description: (
<>
<p style={{ color: "#000" }}>
<b>Started at:</b> 18.8.2023, 00:59:10
<br />
<b>Endet at:</b> 18.8.2023, 00:59:13
<br />
<b>Duration:</b> 3s 516ms
</p>
<MyAttachments
attachments={[
{
OriginalFileName: "test.png",
SystemFileName:
"e74fc0c4-4114-4d48-8ca2-5adb77d98ebe.jpg",
},
]}
downloadUrl={`${Constants.STATIC_CONTENT_ADDRESS}grouptasks/df1fc270-485c-4d9a-8439-dc7fbc151f4c/`}
/>
</>
),
},
{
title: "In Progress",
description: "This is a description.",
},
]}
/>
</MyLazyLoadingModal>
);
}

View File

@ -1,13 +1,39 @@
import { CameraOutlined } from "@ant-design/icons";
import { Avatar, Button, Card, Col, Input, Row, Table, Typography } from "antd";
import { AppStyle } from "../../utils";
import { useState } from "react";
import {
Avatar,
Button,
Card,
Col,
Input,
Popover,
Row,
Table,
Typography,
} from "antd";
import { AppStyle, Constants, myFetch } from "../../utils";
import { useEffect, useState } from "react";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { Link } from "react-router-dom";
import EquipmentViewModal from "./EquipmentViewModal";
export default function EquipmentDocumentation() {
export default function EquipmentDocumentation({ isEquipmentViewModalOpen }) {
const [isScanning, setIsScanning] = useState(false);
const [fetchingEquipment, setFetchingEquipment] = useState(false);
const [res, setRes] = useState("");
const [equipment, setEquipment] = useState([]);
useEffect(() => {
console.log("eq");
setFetchingEquipment(true);
myFetch("/equipment", "GET").then((data) => {
setEquipment(data);
setFetchingEquipment(false);
});
}, []);
const getTableColumns = () => {
return [
{
@ -29,22 +55,49 @@ export default function EquipmentDocumentation() {
title: "Action",
dataIndex: "action",
key: "action",
render: (_, record) => {
return (
<Link
to={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_VIEW + record.key
}
>
View
</Link>
);
},
},
];
};
const getTableItems = () => {
return [
{
key: "1",
name: "Bambu Lab P1S",
},
{
key: "2",
return equipment.map((eq) => ({
key: eq.Id,
name: (
<>
<Popover
placement="right"
trigger="hover"
content={
<Avatar
src={`${Constants.API_ADDRESS}/equipment/thumbnail${eq.Thumbnail}`}
size={256}
shape="square"
/>
}
>
<>
<Avatar
src={`${Constants.API_ADDRESS}/equipment/thumbnail${eq.Thumbnail}`}
shape="square"
/>
</>
</Popover>
name: "Equipment 2",
},
];
{eq.Name}
</>
),
}));
};
return (
@ -85,10 +138,13 @@ export default function EquipmentDocumentation() {
</Row>
<Table
loading={fetchingEquipment}
scroll={{ x: "max-content" }}
columns={getTableColumns()}
dataSource={getTableItems()}
/>
<EquipmentViewModal isOpen={isEquipmentViewModalOpen} />
</>
);
}

View File

@ -15,10 +15,10 @@ import {
} from "antd";
import { Link } from "react-router-dom";
import {
AppStyle,
Constants,
FormatDatetime,
GetDuration,
MyAvatar,
SentMessagesCommands,
WebSocketContext,
hasOneXYPermission,
@ -26,6 +26,7 @@ import {
} from "../../../utils";
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../../Components/MyAvatar";
const { useBreakpoint } = Grid;
@ -240,15 +241,7 @@ export default function GroupTaskTableList({
) && (
<Row
style={{
marginBottom: 16,
//display: "flex",
/*justifyContent: hasXYPermission(
webSocketContext.User.Permissions,
Constants.PERMISSIONS.GROUP_TASKS.OVERVIEW.XYNewTask,
categoryGroup.category
)
? "space-between"
: "right",*/
marginBottom: AppStyle.app.marginBottom,
}}
>
{hasXYPermission(

View File

@ -1,21 +1,16 @@
import {
Alert,
Button,
Col,
Form,
Image,
Input,
InputNumber,
Modal,
Popover,
Result,
Row,
Space,
Steps,
Tag,
notification,
} from "antd";
import { useContext, useMemo, useRef, useState } from "react";
import { useContext, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Constants,
@ -23,22 +18,23 @@ import {
WebSocketContext,
SentMessagesCommands,
GetDuration,
MyAvatar,
getUserId,
GroupTasksStepsLockedAndUserUpdateInputValueRememberId,
hasXYPermission,
} from "../../../utils";
import {
CheckOutlined,
FileImageOutlined,
InfoCircleOutlined,
LockOutlined,
RetweetOutlined,
UndoOutlined,
} from "@ant-design/icons";
import { StlViewer } from "react-stl-viewer";
import TextArea from "antd/es/input/TextArea";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../../Components/MyAvatar";
import MyModal, { MyNotFoundModal } from "../../../Components/MyModal";
import MyAttachments from "../../../Components/MyAttachments";
export default function GroupTasksViewModal({ isOpen }) {
const webSocketContext = useContext(WebSocketContext);
@ -67,20 +63,11 @@ export default function GroupTasksViewModal({ isOpen }) {
)
) {
return (
<Modal
open={isOpen}
width="70%"
maskClosable={true}
<MyNotFoundModal
isOpen={isOpen}
onCancel={handleCancel}
footer={
<Button onClick={handleCancel}>{t("common.button.close")}</Button>
}
>
<Result
status="500"
title={t("groupTasks.groupTasksViewModal.groupTaskNotFound")}
/>
</Modal>
resultTitle={t("groupTasks.groupTasksViewModal.groupTaskNotFound")}
/>
);
}
@ -567,15 +554,7 @@ export default function GroupTasksViewModal({ isOpen }) {
};
return (
<Modal
open={isOpen}
width="70%"
onCancel={handleCancel}
maskClosable={false}
footer={
<Button onClick={handleCancel}>{t("common.button.close")}</Button>
}
>
<MyModal isOpen={isOpen} onCancel={handleCancel}>
{notificationContextHolder}
{webSocketContext.GroupTasks.map((groupTask) => {
@ -718,7 +697,7 @@ export default function GroupTasksViewModal({ isOpen }) {
}
return "";
})}
</Modal>
</MyModal>
);
}
@ -928,125 +907,16 @@ function InputRequiredHandler({
);
}
const MyStlViewer = ({ url }) => {
const memoizedCode = useMemo(() => {
return (
<StlViewer
style={{ top: 0, left: 0, height: "100%" }}
orbitControls
modelProps={{
color: Constants.COLORS.SECONDARY,
scale: 1,
positionX: 200,
positionY: 200,
}}
shadows
floorProps={{ gridWidth: 400 }}
url={url}
onError={(err) => console.error(err)}
onFinishLoading={(ev) => console.log(ev)}
/>
);
}, [url]);
return memoizedCode;
};
function GroupTaskStepLogHandler({ currentGroupTaskId, log, files }) {
const getDownloadUrl = (file) => {
return `${Constants.STATIC_CONTENT_ADDRESS}grouptasks/${currentGroupTaskId}/${file.SystemFileName}`;
};
const getTag = (file) => {
return (
<a
key={"a" + file.SystemFileName}
href={getDownloadUrl(file)}
download="test"
target="_blank"
rel="noreferrer"
>
<Tag
icon={<FileImageOutlined />}
color="processing"
style={{ marginTop: 6 }}
>
{file.OriginalFileName}
</Tag>
</a>
);
};
const fileContent = (files) => {
const nonImageFiles = [];
const imageFiles = [];
for (const file of files) {
const fileExtension = file.SystemFileName.split(".")[1];
if (
fileExtension === "png" ||
fileExtension === "jpeg" ||
fileExtension === "jpg" ||
fileExtension === "webp"
) {
imageFiles.push(file);
} else {
nonImageFiles.push(file);
}
}
const elements = [];
for (const nonImageFile of nonImageFiles) {
const fileExtension = nonImageFile.SystemFileName.split(".")[1];
if (fileExtension === "stl") {
elements.push(
<div key={"tag" + nonImageFile.SystemFileName}>
<div style={{ height: 300, backgroundColor: "#fff" }}>
<MyStlViewer url={getDownloadUrl(nonImageFile)} />
</div>
{getTag(nonImageFile)}
</div>
);
} else {
elements.push(getTag(nonImageFile));
}
}
const imageFileElements = [];
for (const imageFile of imageFiles) {
imageFileElements.push(
<Col
key={imageFile.SystemFileName}
style={{ marginTop: 6 }}
className="gutter-row"
span={6}
>
<Image width={"100%"} src={getDownloadUrl(imageFile)} />
{getTag(imageFile)}
</Col>
);
}
if (imageFileElements.length > 0) {
elements.push(
<Row key={"imgrow"} gutter={16}>
{imageFileElements}
</Row>
);
}
return elements;
};
return (
<span style={{ whiteSpace: "pre-line" }}>
{log}
{files !== "" && files !== " " && fileContent(JSON.parse(files))}
{files !== "" && files !== " " && (
<MyAttachments
attachments={JSON.parse(files)}
downloadUrl={`${Constants.STATIC_CONTENT_ADDRESS}grouptasks/${currentGroupTaskId}/`}
/>
)}
</span>
);
}

View File

@ -1,4 +1,4 @@
import { Button, Popconfirm, Result } from "antd";
import { Button, Col, Popconfirm, Result, Row } from "antd";
import { useContext, useState } from "react";
import GroupTasksViewModal from "./GroupTasksViewModal";
import GroupTypeSelectionModal from "./GroupTypeSelectionModal";
@ -49,7 +49,7 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
SentMessagesCommands.GroupTasksInstallGlobalPythonPackages,
{}
);
}
};
return (
<>
@ -59,31 +59,44 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
<>
{filteredCategoryGroups.length > 0 ? (
<>
{hasPermission(
webSocketContext.User.Permissions,
Constants.PERMISSIONS.GROUP_TASKS.INSTALL_GLOBAL_PYTHON_PACKAGES
) && (
<div
style={{
display: "flex",
justifyContent: "flex-end",
}}
<Row>
<Col
xs={24}
sm={{ span: 10, offset: 14 }}
md={{ span: 8, offset: 16 }}
>
<Popconfirm
placement="top"
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
title={t(
"groupTasks.button.installGlobalPythonPackages.popover.title"
)}
onConfirm={() => onInstallGlobalPythonPackages()}
>
<Button icon={<ReloadOutlined />}>
{t("groupTasks.button.installGlobalPythonPackages.title")}
</Button>
</Popconfirm>
</div>
)}
{hasPermission(
webSocketContext.User.Permissions,
Constants.PERMISSIONS.GROUP_TASKS
.INSTALL_GLOBAL_PYTHON_PACKAGES
) && (
<div
style={
{
// display: "flex",
// justifyContent: "flex-end",
}
}
>
<Popconfirm
placement="top"
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
title={t(
"groupTasks.button.installGlobalPythonPackages.popover.title"
)}
onConfirm={() => onInstallGlobalPythonPackages()}
>
<Button icon={<ReloadOutlined />} block>
{t(
"groupTasks.button.installGlobalPythonPackages.title"
)}
</Button>
</Popconfirm>
</div>
)}
</Col>
</Row>
{filteredCategoryGroups.map((categoryGroup) => {
return (
@ -101,7 +114,7 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
key="result"
status="403"
title={t("groupTasks.categoryGroups.assignedToNoTask.title")}
subTitle={t("contactAdmin")}
subTitle={t("common.contactAdmin")}
/>
)}
</>

View File

@ -3,6 +3,7 @@ import { Button, Form, Input, Modal, notification } from "antd";
import {
Constants,
EncodeStringToBase64,
myFetch,
setUserSessionToLocalStorage,
} from "../../utils";
import { useState } from "react";
@ -38,21 +39,10 @@ export default function Login() {
return;
}
fetch(`${Constants.API_ADDRESS}/user/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: username,
password: EncodeStringToBase64(password),
}),
myFetch("/user/auth/login", "POST", {
username: username,
password: EncodeStringToBase64(password),
})
.then((res) => {
if (res.status === 200) {
return res.json();
}
return Promise.reject(res.status);
})
.then((data) => {
setUserSessionToLocalStorage(data.Session);
window.location.href = "/";

View File

@ -1,7 +1,6 @@
import { Popconfirm, Space, Table, Typography } from "antd";
import {
FormatDatetime,
MyAvatar,
SentMessagesCommands,
WebSocketContext,
getUserId,
@ -10,6 +9,7 @@ import { useContext } from "react";
import { Link } from "react-router-dom";
import { Constants } from "../../utils";
import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../Components/MyAvatar";
export default function Scanners() {
const webSocketContext = useContext(WebSocketContext);

View File

@ -3,7 +3,6 @@ import {
Card,
Col,
Form,
Grid,
Input,
Popconfirm,
Row,
@ -20,14 +19,13 @@ import {
Constants,
EncodeStringToBase64,
FormatDatetime,
MyAvatar,
SentMessagesCommands,
WebSocketContext,
getConnectionStatusItem,
getUserSessionFromLocalStorage,
handleUnauthorizedStatus,
hasPermission,
isEmailValid,
myFetch,
} from "../../utils";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
@ -38,15 +36,13 @@ import {
FileTextOutlined,
KeyOutlined,
} from "@ant-design/icons";
const { useBreakpoint } = Grid;
import { MyAvatar } from "../../Components/MyAvatar";
export default function UserProfile() {
const webSocketContext = useContext(WebSocketContext);
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const { t, i18n } = useTranslation();
const screenBreakpoint = useBreakpoint();
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
@ -85,19 +81,7 @@ export default function UserProfile() {
<Space size="middle">
<Link
href="#"
onClick={() => {
fetch(`${Constants.API_ADDRESS}/user/session/${record.key}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"X-Authorization": getUserSessionFromLocalStorage(),
},
})
.then((res) => handleUnauthorizedStatus(res.status))
.catch((err) => {
console.error(err);
});
}}
onClick={() => myFetch(`/user/session/${record.key}`, "DELETE")}
>
{t("userProfile.column.action.signOut")}
</Link>
@ -334,7 +318,7 @@ export default function UserProfile() {
</Upload>
</Col>
<Col xs={24} sm={4} offset={screenBreakpoint.sm && 16}>
<Col xs={24} sm={{ span: 4, offset: 16 }}>
<Form.Item label={t("userProfile.form.language")}>
<Select
style={{ width: "100%" }}

View File

@ -1,5 +1,4 @@
import { UserOutlined } from "@ant-design/icons";
import { Avatar, Badge, Tooltip } from "antd";
import { Badge } from "antd";
import { createContext, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Buffer } from "buffer";
@ -47,6 +46,8 @@ export const Constants = {
STATIC_CONTENT_ADDRESS: staticContentAddress,
WS_ADDRESS: wsAddress,
ROUTE_PATHS: {
EQUIPMENT_DOCUMENTATION: "/equipment-documentation",
EQUIPMENT_DOCUMENTATION_VIEW: "/equipment-documentation/",
GROUP_TASKS: "/group-tasks",
GROUP_TASKS_VIEW: "/group-tasks/",
},
@ -173,13 +174,6 @@ export function setUserSessionToLocalStorage(session) {
localStorage.setItem("session", session);
}
export function handleUnauthorizedStatus(status) {
if (status === 401) {
setUserSessionToLocalStorage("");
window.location.href = "/";
}
}
/**
* websocket
*/
@ -1087,7 +1081,7 @@ export function WebSocketProvider({
description: `You can now continue with the work`,
});
break;
case ReceivedMessagesCommands.default:
default:
console.error("unknown command", cmd);
break;
}
@ -1118,7 +1112,6 @@ export function WebSocketProvider({
useEffect(() => {
connect();
//return () => socket.close();
return () => ws.current.close();
}, []);
@ -1244,63 +1237,6 @@ export function getConnectionStatusItem(connectionStatus) {
);
}
export function MyAvatar({
avatarWidth,
avatar,
tooltip,
tooltipTitle,
allUsers,
userId,
}) {
const MyDefaultAvatar = () => {
if (tooltip) {
return (
<Tooltip placement="top" trigger="hover" title={tooltipTitle}>
<Avatar size={avatarWidth} icon={<UserOutlined />} />
</Tooltip>
);
}
return <Avatar size={avatarWidth} icon={<UserOutlined />} />;
};
if (avatar === undefined || avatar === null || avatar === "") {
if (allUsers !== undefined && userId !== undefined) {
const user = allUsers.find((u) => u.Id === userId);
if (user === undefined) return <MyDefaultAvatar />;
avatar = user.Avatar;
tooltipTitle = user.Username;
} else {
return <MyDefaultAvatar />;
}
}
const MyAvat = () => {
if (avatar === "") return <MyDefaultAvatar />;
return (
<Avatar
size={avatarWidth}
src={Constants.STATIC_CONTENT_ADDRESS + "avatars/" + avatar}
/>
);
};
if (tooltip) {
return (
<Tooltip placement="top" trigger="hover" title={tooltipTitle}>
<>
<MyAvat />
</>
</Tooltip>
);
}
return <MyAvat />;
}
export function getUserId() {
return localStorage.getItem("userId");
}
@ -1355,3 +1291,35 @@ export function EncodeStringToBase64(value) {
export function DecodedBase64ToString(value) {
return Buffer.from(value, "base64").toString();
}
const myFetchDefaultHeaders = {
"Content-Type": "application/json",
"X-Authorization": getUserSessionFromLocalStorage(),
};
export function myFetch(url, method, body = null, headers = {}) {
const requestOptions = {
method: method,
headers: { ...myFetchDefaultHeaders, ...headers },
body: body ? JSON.stringify(body) : null,
};
return fetch(`${Constants.API_ADDRESS}${url}`, requestOptions)
.then((response) => {
// if status is not in range 200-299
if (!response.ok) {
if (response.status === 401) {
setUserSessionToLocalStorage("");
window.location.href = "/";
}
return;
}
return response.json();
})
.catch((error) => {
console.error("myFetch error:", error);
throw error;
});
}