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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import {
import { import {
Constants, Constants,
FormatDatetime, FormatDatetime,
MyAvatar,
SentMessagesCommands, SentMessagesCommands,
WebSocketContext, WebSocketContext,
getConnectionStatusItem, getConnectionStatusItem,
@ -23,6 +22,7 @@ import { Link } from "react-router-dom";
import { UserAddOutlined } from "@ant-design/icons"; import { UserAddOutlined } from "@ant-design/icons";
import CreateUserModal from "./CreateUserModal"; import CreateUserModal from "./CreateUserModal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MyAvatar } from "../../Components/MyAvatar";
export default function AllUsers() { export default function AllUsers() {
const webSocketContext = useContext(WebSocketContext); 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 { CameraOutlined } from "@ant-design/icons";
import { Avatar, Button, Card, Col, Input, Row, Table, Typography } from "antd"; import {
import { AppStyle } from "../../utils"; Avatar,
import { useState } from "react"; 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 { 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 [isScanning, setIsScanning] = useState(false);
const [fetchingEquipment, setFetchingEquipment] = useState(false);
const [res, setRes] = useState(""); 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 = () => { const getTableColumns = () => {
return [ return [
{ {
@ -29,22 +55,49 @@ export default function EquipmentDocumentation() {
title: "Action", title: "Action",
dataIndex: "action", dataIndex: "action",
key: "action", key: "action",
render: (_, record) => {
return (
<Link
to={
Constants.ROUTE_PATHS.EQUIPMENT_DOCUMENTATION_VIEW + record.key
}
>
View
</Link>
);
},
}, },
]; ];
}; };
const getTableItems = () => { const getTableItems = () => {
return [ return equipment.map((eq) => ({
{ key: eq.Id,
key: "1", name: (
name: "Bambu Lab P1S", <>
}, <Popover
{ placement="right"
key: "2", 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 ( return (
@ -85,10 +138,13 @@ export default function EquipmentDocumentation() {
</Row> </Row>
<Table <Table
loading={fetchingEquipment}
scroll={{ x: "max-content" }} scroll={{ x: "max-content" }}
columns={getTableColumns()} columns={getTableColumns()}
dataSource={getTableItems()} dataSource={getTableItems()}
/> />
<EquipmentViewModal isOpen={isEquipmentViewModalOpen} />
</> </>
); );
} }

View File

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

View File

@ -1,21 +1,16 @@
import { import {
Alert, Alert,
Button, Button,
Col,
Form, Form,
Image,
Input, Input,
InputNumber, InputNumber,
Modal,
Popover, Popover,
Result,
Row,
Space, Space,
Steps, Steps,
Tag, Tag,
notification, notification,
} from "antd"; } from "antd";
import { useContext, useMemo, useRef, useState } from "react"; import { useContext, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import {
Constants, Constants,
@ -23,22 +18,23 @@ import {
WebSocketContext, WebSocketContext,
SentMessagesCommands, SentMessagesCommands,
GetDuration, GetDuration,
MyAvatar,
getUserId, getUserId,
GroupTasksStepsLockedAndUserUpdateInputValueRememberId, GroupTasksStepsLockedAndUserUpdateInputValueRememberId,
hasXYPermission, hasXYPermission,
} from "../../../utils"; } from "../../../utils";
import { import {
CheckOutlined, CheckOutlined,
FileImageOutlined,
InfoCircleOutlined, InfoCircleOutlined,
LockOutlined, LockOutlined,
RetweetOutlined, RetweetOutlined,
UndoOutlined, UndoOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { StlViewer } from "react-stl-viewer";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { useTranslation } from "react-i18next"; 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 }) { export default function GroupTasksViewModal({ isOpen }) {
const webSocketContext = useContext(WebSocketContext); const webSocketContext = useContext(WebSocketContext);
@ -67,20 +63,11 @@ export default function GroupTasksViewModal({ isOpen }) {
) )
) { ) {
return ( return (
<Modal <MyNotFoundModal
open={isOpen} isOpen={isOpen}
width="70%"
maskClosable={true}
onCancel={handleCancel} onCancel={handleCancel}
footer={ resultTitle={t("groupTasks.groupTasksViewModal.groupTaskNotFound")}
<Button onClick={handleCancel}>{t("common.button.close")}</Button> />
}
>
<Result
status="500"
title={t("groupTasks.groupTasksViewModal.groupTaskNotFound")}
/>
</Modal>
); );
} }
@ -567,15 +554,7 @@ export default function GroupTasksViewModal({ isOpen }) {
}; };
return ( return (
<Modal <MyModal isOpen={isOpen} onCancel={handleCancel}>
open={isOpen}
width="70%"
onCancel={handleCancel}
maskClosable={false}
footer={
<Button onClick={handleCancel}>{t("common.button.close")}</Button>
}
>
{notificationContextHolder} {notificationContextHolder}
{webSocketContext.GroupTasks.map((groupTask) => { {webSocketContext.GroupTasks.map((groupTask) => {
@ -718,7 +697,7 @@ export default function GroupTasksViewModal({ isOpen }) {
} }
return ""; 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 }) { 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 ( return (
<span style={{ whiteSpace: "pre-line" }}> <span style={{ whiteSpace: "pre-line" }}>
{log} {log}
{files !== "" && files !== " " && (
{files !== "" && files !== " " && fileContent(JSON.parse(files))} <MyAttachments
attachments={JSON.parse(files)}
downloadUrl={`${Constants.STATIC_CONTENT_ADDRESS}grouptasks/${currentGroupTaskId}/`}
/>
)}
</span> </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 { useContext, useState } from "react";
import GroupTasksViewModal from "./GroupTasksViewModal"; import GroupTasksViewModal from "./GroupTasksViewModal";
import GroupTypeSelectionModal from "./GroupTypeSelectionModal"; import GroupTypeSelectionModal from "./GroupTypeSelectionModal";
@ -49,7 +49,7 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
SentMessagesCommands.GroupTasksInstallGlobalPythonPackages, SentMessagesCommands.GroupTasksInstallGlobalPythonPackages,
{} {}
); );
} };
return ( return (
<> <>
@ -59,31 +59,44 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
<> <>
{filteredCategoryGroups.length > 0 ? ( {filteredCategoryGroups.length > 0 ? (
<> <>
{hasPermission( <Row>
webSocketContext.User.Permissions, <Col
Constants.PERMISSIONS.GROUP_TASKS.INSTALL_GLOBAL_PYTHON_PACKAGES xs={24}
) && ( sm={{ span: 10, offset: 14 }}
<div md={{ span: 8, offset: 16 }}
style={{
display: "flex",
justifyContent: "flex-end",
}}
> >
<Popconfirm {hasPermission(
placement="top" webSocketContext.User.Permissions,
okText={t("common.button.confirm")} Constants.PERMISSIONS.GROUP_TASKS
cancelText={t("common.button.cancel")} .INSTALL_GLOBAL_PYTHON_PACKAGES
title={t( ) && (
"groupTasks.button.installGlobalPythonPackages.popover.title" <div
)} style={
onConfirm={() => onInstallGlobalPythonPackages()} {
> // display: "flex",
<Button icon={<ReloadOutlined />}> // justifyContent: "flex-end",
{t("groupTasks.button.installGlobalPythonPackages.title")} }
</Button> }
</Popconfirm> >
</div> <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) => { {filteredCategoryGroups.map((categoryGroup) => {
return ( return (
@ -101,7 +114,7 @@ export default function GroupTasks({ isGroupTasksViewModalOpen }) {
key="result" key="result"
status="403" status="403"
title={t("groupTasks.categoryGroups.assignedToNoTask.title")} 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 { import {
Constants, Constants,
EncodeStringToBase64, EncodeStringToBase64,
myFetch,
setUserSessionToLocalStorage, setUserSessionToLocalStorage,
} from "../../utils"; } from "../../utils";
import { useState } from "react"; import { useState } from "react";
@ -38,21 +39,10 @@ export default function Login() {
return; return;
} }
fetch(`${Constants.API_ADDRESS}/user/auth/login`, { myFetch("/user/auth/login", "POST", {
method: "POST", username: username,
headers: { "Content-Type": "application/json" }, password: EncodeStringToBase64(password),
body: JSON.stringify({
username: username,
password: EncodeStringToBase64(password),
}),
}) })
.then((res) => {
if (res.status === 200) {
return res.json();
}
return Promise.reject(res.status);
})
.then((data) => { .then((data) => {
setUserSessionToLocalStorage(data.Session); setUserSessionToLocalStorage(data.Session);
window.location.href = "/"; window.location.href = "/";

View File

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

View File

@ -3,7 +3,6 @@ import {
Card, Card,
Col, Col,
Form, Form,
Grid,
Input, Input,
Popconfirm, Popconfirm,
Row, Row,
@ -20,14 +19,13 @@ import {
Constants, Constants,
EncodeStringToBase64, EncodeStringToBase64,
FormatDatetime, FormatDatetime,
MyAvatar,
SentMessagesCommands, SentMessagesCommands,
WebSocketContext, WebSocketContext,
getConnectionStatusItem, getConnectionStatusItem,
getUserSessionFromLocalStorage, getUserSessionFromLocalStorage,
handleUnauthorizedStatus,
hasPermission, hasPermission,
isEmailValid, isEmailValid,
myFetch,
} from "../../utils"; } from "../../utils";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -38,15 +36,13 @@ import {
FileTextOutlined, FileTextOutlined,
KeyOutlined, KeyOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { MyAvatar } from "../../Components/MyAvatar";
const { useBreakpoint } = Grid;
export default function UserProfile() { export default function UserProfile() {
const webSocketContext = useContext(WebSocketContext); const webSocketContext = useContext(WebSocketContext);
const [notificationApi, notificationContextHolder] = const [notificationApi, notificationContextHolder] =
notification.useNotification(); notification.useNotification();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const screenBreakpoint = useBreakpoint();
const [oldPassword, setOldPassword] = useState(""); const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
@ -85,19 +81,7 @@ export default function UserProfile() {
<Space size="middle"> <Space size="middle">
<Link <Link
href="#" href="#"
onClick={() => { onClick={() => myFetch(`/user/session/${record.key}`, "DELETE")}
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);
});
}}
> >
{t("userProfile.column.action.signOut")} {t("userProfile.column.action.signOut")}
</Link> </Link>
@ -334,7 +318,7 @@ export default function UserProfile() {
</Upload> </Upload>
</Col> </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")}> <Form.Item label={t("userProfile.form.language")}>
<Select <Select
style={{ width: "100%" }} style={{ width: "100%" }}

View File

@ -1,5 +1,4 @@
import { UserOutlined } from "@ant-design/icons"; import { Badge } from "antd";
import { Avatar, Badge, Tooltip } from "antd";
import { createContext, useEffect, useRef, useState } from "react"; import { createContext, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
@ -47,6 +46,8 @@ export const Constants = {
STATIC_CONTENT_ADDRESS: staticContentAddress, STATIC_CONTENT_ADDRESS: staticContentAddress,
WS_ADDRESS: wsAddress, WS_ADDRESS: wsAddress,
ROUTE_PATHS: { ROUTE_PATHS: {
EQUIPMENT_DOCUMENTATION: "/equipment-documentation",
EQUIPMENT_DOCUMENTATION_VIEW: "/equipment-documentation/",
GROUP_TASKS: "/group-tasks", GROUP_TASKS: "/group-tasks",
GROUP_TASKS_VIEW: "/group-tasks/", GROUP_TASKS_VIEW: "/group-tasks/",
}, },
@ -173,13 +174,6 @@ export function setUserSessionToLocalStorage(session) {
localStorage.setItem("session", session); localStorage.setItem("session", session);
} }
export function handleUnauthorizedStatus(status) {
if (status === 401) {
setUserSessionToLocalStorage("");
window.location.href = "/";
}
}
/** /**
* websocket * websocket
*/ */
@ -1087,7 +1081,7 @@ export function WebSocketProvider({
description: `You can now continue with the work`, description: `You can now continue with the work`,
}); });
break; break;
case ReceivedMessagesCommands.default: default:
console.error("unknown command", cmd); console.error("unknown command", cmd);
break; break;
} }
@ -1118,7 +1112,6 @@ export function WebSocketProvider({
useEffect(() => { useEffect(() => {
connect(); connect();
//return () => socket.close();
return () => ws.current.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() { export function getUserId() {
return localStorage.getItem("userId"); return localStorage.getItem("userId");
} }
@ -1355,3 +1291,35 @@ export function EncodeStringToBase64(value) {
export function DecodedBase64ToString(value) { export function DecodedBase64ToString(value) {
return Buffer.from(value, "base64").toString(); 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;
});
}