Initial Commit

This commit is contained in:
Tom Butcher 2024-07-28 17:54:38 +01:00
commit 9fc1f7c8f7
69 changed files with 55618 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
HTTPS=false

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.nova

21406
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "farmcontrol-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@simplewebauthn/browser": "^10.0.0",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.5.0",
"antd": "^5.19.2",
"axios": "*",
"gcode-preview": "^2.17.0",
"moment": "*",
"react": "*",
"react-dom": "*",
"react-router-dom": "*",
"react-scripts": "*",
"socket.io-client": "*",
"styled-components": "*",
"three": "^0.166.1",
"tsparticles": "^3.5.0",
"web-vitals": "*"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"standard": "^17.1.0"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

9
public/icons/eject.svg Normal file
View File

@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
width="24"
height="24"
fill="currentColor"
>
<path d="M819.2 614.4v204.8H204.8v-204.8H819.2zM921.6 819.2H102.4V204.8h819.2v614.4z m-716.8-51.2h614.4V256H204.8v512z"></path>
</svg>

After

Width:  |  Height:  |  Size: 256 B

43
public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

File diff suppressed because it is too large Load Diff

BIN
public/logo-dark512@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
public/logo-light512@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

18
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 377 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/logo512@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

BIN
public/scan-barcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
public/wallpaper.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

38
src/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #be871a;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

89
src/App.jsx Normal file
View File

@ -0,0 +1,89 @@
import React, { useContext, useState } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import { App, ConfigProvider, theme } from "antd";
import LoginUser from "./components/Auth/LoginUser.jsx";
import RegisterPasskey from "./components/Auth/RegisterPasskey.jsx";
import Profile from "./components/Dashboard/Profile.jsx";
import Overview from "./components/Dashboard/Overview";
import Printers from "./components/Dashboard/Printers/Printers";
import EditPrinter from "./components/Dashboard/Printers/EditPrinter.jsx";
import ControlPrinter from "./components/Dashboard/Printers/ControlPrinter.jsx";
import PrintJobs from "./components/Dashboard/PrintJobs/PrintJobs.jsx";
import Fillaments from "./components/Dashboard/Fillaments/Fillaments.jsx";
import GCodeFiles from "./components/Dashboard/GCodeFiles/GCodeFiles.jsx";
import Dashboard from "./components/Dashboard/common/Dashboard";
import PrivateRoute from "./components/PrivateRoute";
import PublicRoute from "./components/PublicRoute.jsx";
import "./App.css";
import { SocketProvider } from "./components/Dashboard/context/SocketContext.js";
import { AuthProvider } from "./components/Auth/AuthContext.js";
const FarmControlApp = () => {
return (
<ConfigProvider
theme={{
// 1. Use dark algorithm
algorithm: theme.darkAlgorithm,
token: {
colorBgBase: '#191919',
colorPrimary: '#0A84FF',
colorSuccess: '#32D74B',
colorWarning: '#FF9F0A',
colorInfo: '#0A84FF',
colorLink: '#5AC8F5',
borderRadius: '10px',
},
// 2. Combine dark algorithm and compact algorithm
// algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
}}
>
<App>
<AuthProvider>
<SocketProvider>
<Router>
<Routes>
<Route
path="login"
element={<PublicRoute component={() => <LoginUser />} />}
/>
<Route
path="login/register-passkey"
element={
<PrivateRoute component={() => <RegisterPasskey />} />
}
/>
<Route
path="/dashboard"
element={<PrivateRoute component={() => <Dashboard />} />}
>
<Route path="profile" element={<Profile />} />
<Route path="overview" element={<Overview />} />
<Route path="printers" element={<Printers />} />
<Route path="printers/edit" element={<EditPrinter />} />
<Route path="printers/control" element={<ControlPrinter />} />
<Route path="printjobs" element={<PrintJobs />} />
<Route path="fillaments" element={<Fillaments />} />
<Route path="gcodefiles" element={<GCodeFiles />} />
</Route>
</Routes>
</Router>
</SocketProvider>
</AuthProvider>
</App>
</ConfigProvider>
);
};
export default FarmControlApp;

8
src/App.test.js Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

Binary file not shown.

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 130 148" version="1.1" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.0275218,-4.21477e-17,-4.21477e-17,-0.0275218,-8.99903,147.87)">
<path d="M2121,5369C1637,5326 1270,5230 958,5066C606,4880 403,4658 343,4391C287,4149 381,3894 598,3692L665,3631L653,3579C626,3466 648,3311 707,3196L731,3149L701,3087C622,2923 623,2749 702,2578C731,2515 731,2514 712,2483C702,2466 685,2428 676,2400C663,2362 642,2333 596,2290C384,2094 292,1848 340,1610C349,1563 367,1502 380,1474C523,1146 928,877 1491,735C1618,703 1809,667 1918,656L1984,649L2025,574C2158,331 2379,145 2641,56C2804,0 2805,0 3970,0L5051,0L5051,945L3827,945L3864,967C4064,1088 4211,1219 4300,1353C4328,1396 4365,1471 4383,1521C4411,1602 4415,1625 4415,1733C4415,1832 4411,1866 4390,1927C4345,2063 4264,2185 4150,2290C4104,2333 4083,2362 4070,2400C4061,2428 4044,2466 4033,2483C4014,2514 4014,2516 4037,2562C4087,2662 4100,2720 4100,2835C4100,2950 4078,3041 4033,3113C4014,3144 4014,3145 4038,3194C4098,3312 4120,3465 4093,3579L4081,3631L4148,3692C4483,4003 4509,4415 4214,4748C3943,5054 3439,5270 2814,5350C2689,5366 2228,5379 2121,5369ZM2621,5050C3138,5009 3560,4873 3849,4656C3951,4579 4013,4507 4061,4411C4096,4342 4100,4324 4100,4253C4100,4146 4061,4063 3963,3960C3721,3702 3229,3524 2631,3476C1884,3416 1103,3621 783,3960C685,4063 646,4146 646,4253C646,4324 650,4342 685,4411C733,4507 795,4579 897,4656C1283,4947 1966,5103 2621,5050ZM1078,3393C1322,3286 1614,3213 1974,3169C2119,3151 2627,3151 2772,3169C3132,3213 3424,3286 3668,3393C3721,3416 3770,3438 3776,3440C3785,3444 3786,3436 3781,3411C3730,3186 3364,2969 2890,2883C2710,2850 2600,2841 2373,2841C2146,2841 2036,2850 1856,2883C1382,2969 1016,3186 965,3411C960,3436 961,3444 970,3440C976,3438 1025,3416 1078,3393ZM1034,2856C1251,2712 1559,2607 1927,2549C2052,2530 2117,2526 2373,2526C2629,2526 2694,2530 2819,2549C3197,2608 3509,2718 3733,2870C3769,2895 3775,2896 3783,2881C3797,2857 3782,2766 3757,2715C3702,2607 3594,2514 3425,2427C2975,2197 2271,2141 1694,2290C1411,2363 1151,2498 1045,2630C979,2712 944,2809 960,2872C967,2901 966,2901 1034,2856ZM1034,2226C1135,2157 1325,2074 1548,2000C1798,1918 1872,1885 1991,1805C2260,1624 2422,1390 2500,1070C2517,1003 2539,927 2550,902C2582,830 2658,744 2723,705C2848,631 2773,636 3804,632L4736,629L4736,314L2840,320L2756,349C2640,388 2551,443 2455,534C2317,665 2249,796 2201,1024C2147,1280 2095,1382 1952,1506C1866,1581 1781,1625 1612,1680C1451,1733 1290,1806 1196,1869C1034,1979 940,2124 960,2232C968,2272 966,2272 1034,2226ZM3786,2176C3764,2004 3547,1824 3229,1712C3115,1672 2915,1624 2793,1607C2744,1600 2689,1593 2671,1590C2639,1585 2634,1590 2579,1669C2547,1715 2492,1783 2457,1820L2395,1887L2528,1894C3006,1918 3445,2044 3733,2240L3774,2268L3783,2244C3788,2231 3790,2201 3786,2176ZM857,1741C942,1655 1094,1550 1219,1492C1267,1469 1376,1427 1460,1398C1634,1339 1705,1306 1759,1259C1816,1208 1858,1125 1884,1006C1891,973 1910,972 1694,1014C1280,1095 918,1271 748,1473C645,1596 617,1749 675,1874L704,1936L744,1875C767,1841 817,1781 857,1741ZM4072,1872C4095,1822 4100,1796 4100,1732C4100,1663 4095,1642 4064,1579C3996,1441 3811,1285 3599,1187C3418,1104 3103,1012 2928,991C2869,984 2868,985 2847,1018C2836,1036 2821,1081 2814,1116C2808,1152 2796,1204 2788,1231C2771,1289 2757,1281 2919,1308C3212,1356 3509,1464 3710,1595C3812,1662 3946,1790 3997,1870C4019,1904 4039,1932 4040,1932C4042,1932 4056,1905 4072,1872Z" style="fill-rule:nonzero;"/>
<path d="M2168,4709C1894,4666 1703,4555 1616,4389C1580,4321 1580,4184 1616,4116C1686,3983 1795,3902 1990,3838C2374,3713 2898,3806 3075,4030C3139,4112 3155,4156 3155,4253C3155,4349 3139,4393 3075,4475C2930,4659 2521,4765 2168,4709ZM2531,4395C2672,4372 2780,4328 2829,4275C2848,4253 2848,4252 2829,4230C2762,4157 2554,4095 2373,4095C2192,4095 1984,4157 1917,4230C1898,4252 1898,4253 1917,4275C2010,4378 2296,4434 2531,4395Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M10.588,3.008L7.706,0.126C7.626,0.046 7.517,0 7.404,0L1.714,0C1.477,0 1.286,0.192 1.286,0.429L1.286,11.571C1.286,11.808 1.477,12 1.714,12L10.286,12C10.523,12 10.714,11.808 10.714,11.571L10.714,3.312C10.714,3.198 10.669,3.088 10.588,3.008ZM9.726,3.509L7.205,3.509L7.205,0.988L9.726,3.509ZM9.75,11.036L2.25,11.036L2.25,0.964L6.295,0.964L6.295,3.857C6.295,4.166 6.549,4.42 6.857,4.42L9.75,4.42L9.75,11.036Z" style="fill-rule:nonzero;"/>
<g transform="matrix(2.2954,0,0,2.35148,-5.18023,-13.3842)">
<path d="M4.727,6.742L4.34,6.742C4.338,6.723 4.333,6.705 4.324,6.687C4.316,6.669 4.304,6.653 4.288,6.639C4.273,6.624 4.254,6.613 4.232,6.605C4.21,6.596 4.184,6.592 4.156,6.592C4.089,6.592 4.038,6.616 4.004,6.664C3.97,6.712 3.953,6.782 3.953,6.872L3.953,7.01C3.953,7.058 3.959,7.104 3.97,7.148C3.981,7.192 4.002,7.228 4.032,7.256C4.063,7.283 4.108,7.297 4.166,7.297C4.208,7.297 4.243,7.289 4.27,7.273C4.298,7.257 4.318,7.237 4.331,7.213C4.345,7.189 4.351,7.164 4.351,7.14L4.351,7.124L4.189,7.124L4.189,6.859L4.727,6.859L4.727,7.103C4.727,7.167 4.716,7.229 4.696,7.291C4.675,7.352 4.642,7.408 4.597,7.457C4.552,7.506 4.493,7.545 4.42,7.574C4.347,7.603 4.258,7.618 4.155,7.618C4.045,7.618 3.952,7.602 3.874,7.569C3.797,7.536 3.734,7.491 3.686,7.435C3.638,7.378 3.603,7.313 3.581,7.24C3.559,7.167 3.547,7.091 3.547,7.012L3.547,6.867C3.547,6.753 3.57,6.652 3.617,6.562C3.663,6.472 3.731,6.401 3.822,6.349C3.913,6.297 4.025,6.271 4.159,6.271C4.256,6.271 4.34,6.285 4.412,6.312C4.484,6.339 4.543,6.375 4.589,6.421C4.636,6.466 4.67,6.517 4.693,6.573C4.715,6.628 4.727,6.685 4.727,6.742Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(2.72314,0,0,2.03633,-10.3459,-7.95312)">
<path d="M5.298,6.885L5.298,7.006C5.298,7.068 5.305,7.121 5.32,7.163C5.335,7.206 5.357,7.238 5.385,7.26C5.413,7.282 5.448,7.293 5.488,7.293C5.527,7.293 5.56,7.284 5.587,7.264C5.614,7.245 5.634,7.218 5.647,7.185C5.661,7.152 5.667,7.115 5.667,7.074L6.046,7.074L6.046,7.132C6.046,7.23 6.022,7.315 5.974,7.388C5.926,7.461 5.86,7.518 5.775,7.558C5.691,7.598 5.593,7.618 5.484,7.618C5.355,7.618 5.247,7.592 5.158,7.542C5.069,7.491 5.002,7.419 4.956,7.327C4.91,7.235 4.887,7.127 4.887,7.003L4.887,6.885C4.887,6.761 4.91,6.652 4.956,6.561C5.002,6.469 5.069,6.398 5.158,6.347C5.247,6.297 5.355,6.271 5.484,6.271C5.566,6.271 5.642,6.283 5.71,6.305C5.779,6.328 5.838,6.361 5.888,6.403C5.938,6.446 5.977,6.497 6.005,6.557C6.033,6.617 6.046,6.684 6.046,6.758L6.046,6.815L5.667,6.815C5.667,6.773 5.661,6.736 5.647,6.703C5.634,6.67 5.614,6.644 5.587,6.625C5.56,6.606 5.527,6.596 5.488,6.596C5.448,6.596 5.413,6.607 5.385,6.629C5.357,6.651 5.335,6.683 5.32,6.726C5.305,6.769 5.298,6.822 5.298,6.885Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(2.17112,0,0,2.03633,-6.93951,-7.85288)">
<path d="M7.023,6.877C7.023,6.815 7.015,6.762 6.998,6.719C6.982,6.677 6.959,6.644 6.928,6.622C6.898,6.6 6.862,6.589 6.819,6.589C6.776,6.589 6.74,6.6 6.709,6.622C6.679,6.644 6.655,6.677 6.638,6.719C6.622,6.762 6.614,6.815 6.614,6.877L6.614,7.013C6.614,7.075 6.622,7.128 6.638,7.171C6.655,7.214 6.679,7.246 6.709,7.267C6.74,7.289 6.776,7.3 6.819,7.3C6.862,7.3 6.898,7.289 6.928,7.267C6.959,7.246 6.982,7.214 6.998,7.171C7.015,7.128 7.023,7.075 7.023,7.013L7.023,6.877ZM7.434,7.011C7.434,7.132 7.41,7.239 7.363,7.33C7.316,7.421 7.247,7.492 7.156,7.542C7.064,7.593 6.952,7.618 6.819,7.618C6.687,7.618 6.574,7.593 6.483,7.542C6.391,7.492 6.322,7.421 6.274,7.33C6.227,7.239 6.203,7.132 6.203,7.011L6.203,6.883C6.203,6.759 6.227,6.652 6.274,6.56C6.322,6.468 6.391,6.397 6.483,6.347C6.574,6.297 6.687,6.271 6.819,6.271C6.952,6.271 7.064,6.297 7.156,6.348C7.247,6.399 7.316,6.47 7.363,6.561C7.41,6.653 7.434,6.76 7.434,6.883L7.434,7.011Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(2.53247,0,0,1.97582,-16.199,-4.47337)">
<path d="M7.648,6.292L8.214,6.292C8.355,6.292 8.47,6.319 8.558,6.373C8.646,6.426 8.71,6.501 8.751,6.598C8.792,6.694 8.813,6.807 8.813,6.936C8.813,7.067 8.792,7.182 8.75,7.282C8.708,7.381 8.643,7.458 8.555,7.513C8.466,7.568 8.353,7.596 8.214,7.596L7.648,7.596L7.648,6.292ZM8.049,6.601L8.049,7.288L8.141,7.288C8.188,7.288 8.229,7.281 8.262,7.267C8.296,7.253 8.323,7.231 8.343,7.203C8.363,7.175 8.378,7.139 8.388,7.097C8.397,7.054 8.402,7.004 8.402,6.948C8.402,6.872 8.394,6.808 8.376,6.757C8.359,6.705 8.332,6.666 8.293,6.64C8.255,6.614 8.204,6.601 8.141,6.601L8.049,6.601Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(2.80773,0,0,1.97582,-18.7952,-4.47337)">
<path d="M9.948,7.288L9.948,7.596L9.019,7.596L9.019,6.292L9.948,6.292L9.948,6.601L9.415,6.601L9.415,6.806L9.913,6.806L9.913,7.085L9.415,7.085L9.415,7.288L9.948,7.288Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 137 131" version="1.1" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<rect x="0" y="0" width="136.703" height="130.444" style="fill-opacity:0;"/>
<g transform="matrix(1,0,0,1,-2.84998,0)">
<g transform="matrix(1.23802,0,0,1.23802,-23.9496,-22.499)">
<path d="M90.477,76.266C90.445,76.498 90.441,76.738 90.441,76.978C90.441,80.143 91.058,83.165 92.205,85.92C86.275,82.028 78.252,79.212 68.352,79.212C45.043,79.212 32.145,94.751 32.145,103.13C32.145,104.197 32.703,104.552 34.023,104.552L100.801,104.552L100.801,112.209C100.625,112.219 100.435,112.22 100.242,112.22L36.41,112.22C27.981,112.22 23.969,109.681 23.969,104.095C23.969,90.791 40.777,71.545 68.352,71.545C76.799,71.545 84.237,73.357 90.477,76.266ZM90.289,41.38C90.289,54.482 80.438,65.197 68.352,65.197C56.266,65.197 46.414,54.533 46.414,41.482C46.414,28.584 56.316,18.173 68.352,18.173C80.488,18.173 90.289,28.38 90.289,41.38ZM54.59,41.482C54.59,50.47 60.938,57.529 68.352,57.529C75.816,57.529 82.113,50.369 82.113,41.38C82.113,32.494 75.918,25.841 68.352,25.841C60.836,25.841 54.59,32.646 54.59,41.482Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.23802,0,0,1.23802,-23.9496,-22.499)">
<path d="M113.598,60.83C104.559,60.83 97.398,68.091 97.398,76.978C97.398,83.884 101.461,89.724 107.707,92.162L107.707,117.298C107.707,117.908 108.012,118.365 108.418,118.873L112.684,123.138C113.191,123.646 113.902,123.697 114.461,123.138L122.535,115.115C123.043,114.556 123.043,113.845 122.535,113.337L117.508,108.31L124.465,101.505C124.973,101.048 124.973,100.287 124.363,99.677L117.559,92.923C125.379,89.724 129.746,84.037 129.746,76.978C129.746,68.091 122.535,60.83 113.598,60.83ZM113.547,68.396C116.289,68.396 118.523,70.63 118.523,73.373C118.523,76.216 116.289,78.451 113.547,78.451C110.805,78.451 108.52,76.216 108.52,73.373C108.52,70.63 110.754,68.396 113.547,68.396Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,29 @@
/* AuthPage.css */
.auth-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-image: url('../../../public/wallpaper.webp');
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.auth-form > h2 {
text-align: center;
}
.auth-form {
width: 300px;
border-radius: 8px;
}
.auth-form-button {
width: 100%;
}
.ant-spin-blur {
filter: blur(2.5px);
}

View File

@ -0,0 +1,223 @@
// src/contexts/AuthContext.js
import React, { createContext, useState, useEffect, useCallback } from "react";
import axios from "axios";
import { message } from "antd";
import {
startAuthentication,
startRegistration,
} from "@simplewebauthn/browser";
const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [messageApi, contextHolder] = message.useMessage();
const [token, setToken] = useState(
localStorage.getItem("access_token") || null
);
const [loading, setLoading] = useState(false);
const validateToken = useCallback(async (token) => {
if (!token) {
return false;
}
setLoading(true);
try {
const reponse = await axios.post(
"http://localhost:8080/auth/validate-token",
{
token,
}
);
if (reponse.data.status === "OK") {
setLoading(false);
return true;
}
} catch (error) {
console.error("Invalid token", error);
messageApi.error("Session invalid.");
setToken(null);
localStorage.removeItem("access_token");
}
setLoading(false);
return false;
}, [messageApi]);
const getAuthMode = useCallback(async (email) => {
if (!email) {
return { successful: false };
}
setLoading(true);
try {
const response = await axios.post("http://localhost:8080/auth/modes", {
email,
});
const { authModes } = response.data;
setLoading(false);
return { successful: true, authModes };
} catch (error) {
if (error.response === undefined) {
messageApi.error(
"An error occoured obtaining the auth mode: " + error.message
);
} else {
if (error.response.status === 400) {
messageApi.error(error.response.data.error);
} else {
messageApi.error(
"An unexpected error occoured obtaining the auth mode. (" +
error.response.status +
")"
);
}
}
}
setLoading(false);
return { successful: false };
}, [messageApi]);
const handleLoginFinished = (user, access_token) => {
setToken(access_token);
localStorage.setItem("access_token", access_token);
messageApi.info("Welcome, " + user.name + ".");
return { successful: true, hasPasskey: user.hasPasskey };
};
const loginWithPassword = useCallback(async (email, password) => {
if (!email || !password) {
return { successful: false };
}
setLoading(true);
try {
const response = await axios.post("http://localhost:8080/auth/login", {
email,
password,
});
const { user, access_token } = response.data;
return handleLoginFinished(user, access_token);
} catch (error) {
if (error.response === undefined) {
messageApi.error("An error occoured: " + error.message);
} else {
if (error.response.status === 400) {
messageApi.error(error.response.data.error);
} else {
messageApi.error(
"An unexpected error occoured. (" + error.response.status + ")"
);
}
}
}
setLoading(false);
return { successful: false };
}, [messageApi]);
const loginWithPasskey = useCallback(async (email) => {
if (!email) {
return { successful: false };
}
setLoading(true);
try {
const loginOptionsResponse = await axios.post(
"http://localhost:8080/auth/passkey/login",
{ email },
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const loginOptions = loginOptionsResponse.data;
console.log(loginOptions);
const attestationResponse = await startAuthentication(loginOptions);
const loginResponse = await axios.post(
"http://localhost:8080/auth/passkey/login", { email, attestationResponse },
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const { user, access_token } = loginResponse.data;
return handleLoginFinished(user, access_token);
} catch (error) {
console.log(error);
messageApi.error("An error occoured: " + error.name);
}
setLoading(false);
return { successful: false };
}, [messageApi]);
const logout = useCallback(() => {
setToken(null);
localStorage.removeItem("access_token");
messageApi.info("Sucessfully logged out.");
}, [messageApi]);
const registerPasskey = useCallback(async () => {
if (!token) {
return { successful: false };
}
setLoading(true);
try {
const registerOptionsResponse = await axios.post(
"http://localhost:8080/auth/passkey/register",
{ token },
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const registerOptions = registerOptionsResponse.data;
console.log(registerOptions);
const attestationResponse = await startRegistration(registerOptions);
await axios.post(
"http://localhost:8080/auth/passkey/register",
attestationResponse,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
messageApi.success("Passkey registered!");
setLoading(false);
return { successful: true };
} catch (error) {
console.log(error);
messageApi.error("An error occoured: " + error.name);
}
setLoading(false);
return { successful: false };
}, [messageApi]);
useEffect(() => {
console.log("Token changed!" + token)
validateToken(token);
}, [token]); // if token changes, validate it.
return (
<>
{contextHolder}
<AuthContext.Provider
value={{
token,
loginWithPassword,
loginWithPasskey,
logout,
loading,
registerPasskey,
getAuthMode,
}}
>
{children}
</AuthContext.Provider>
</>
);
};
export { AuthContext, AuthProvider };

View File

@ -0,0 +1,29 @@
import React, { useContext } from "react";
import { Spin, Flex, Card } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "./AuthContext";
import AuthParticles from "./AuthParticles";
import "./Auth.css";
const AuthLayout = ({ children }) => {
const { loading } = useContext(AuthContext);
return (
<>
<AuthParticles />
<Flex
horizontal="true"
align="center"
justify="center"
style={{ paddingTop: "35px" }}
>
<Card style={{ maxWidth: 350 }}>
<Spin spinning={loading} indicator={<LoadingOutlined spin />} size="large">
{children}
</Spin>
</Card>
</Flex>
</>
);
};
export default AuthLayout;

View File

@ -0,0 +1,115 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import "./Auth.css";
const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
return (
<Particles
id="tsparticles"
particlesLoaded={particlesLoaded}
options={options}
/>
);
});
const AuthParticles = () => {
const [init, setInit] = useState(false);
// this should be run only once per application lifetime
useEffect(() => {
initParticlesEngine(async (engine) => {
await loadSlim(engine);
}).then(() => {
setInit(true);
});
}, []);
const particlesLoaded = useCallback((container) => {
console.log(container);
}, []);
const options = useMemo(
() => ({
background: {
color: {
value: "#141414",
},
},
fpsLimit: 120,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: true,
mode: "repulse",
},
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#ffffff",
},
links: {
color: "#ffffff",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
move: {
direction: "none",
enable: true,
outModes: {
default: "bounce",
},
random: false,
speed: 1,
straight: false,
},
number: {
density: {
enable: true,
},
value: 160,
},
opacity: {
value: 0.5,
},
shape: {
type: "circle",
},
size: {
value: { min: 1, max: 5 },
},
},
detectRetina: true,
}),
[]
);
return (
<>
{init && (
<ParticlesComponent
options={options}
particlesLoaded={particlesLoaded}
/>
)}
</>
);
};
export default AuthParticles;

View File

@ -0,0 +1,178 @@
import React, { useState, useContext, useEffect, useMemo, useRef } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import {
Form,
Input,
Button,
Checkbox,
Spin,
Divider,
Typography,
Flex,
Card,
Space,
message,
} from "antd";
import {
UserOutlined,
LockOutlined,
LoginOutlined,
UserAddOutlined,
ArrowRightOutlined,
} from "@ant-design/icons";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import { AuthContext } from "./AuthContext";
import AuthLayout from "./AuthLayout";
import PassKeysIcon from "../Icons/PassKeysIcon"; // Adjust the path if necessary
import "./Auth.css";
const { Title, Text } = Typography;
const LoginUser = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [authModes, setAuthModes] = useState([]);
const [error, setError] = useState("");
const navigate = useNavigate();
const { loginWithPassword, loginWithPasskey, getAuthMode } =
useContext(AuthContext);
const handleLogin = async (e) => {
if (email === "") {
return;
}
if (authModes.length === 0) {
const result = await getAuthMode(email);
if (result.successful === true) {
setAuthModes(result.authModes);
}
return;
}
var result;
if (password.length > 0) {
result = await loginWithPassword(email, password);
} else {
result = await loginWithPasskey(email);
}
if (result.successful === true) {
if (authModes.includes("passkey")) {
setTimeout(() => {
navigate("/dashboard/overview");
}, 200);
} else {
setTimeout(() => {
navigate("/login/register-passkey");
}, 200);
}
}
};
return (
<AuthLayout>
<Flex vertical="true" align="center" style={{ marginBottom: 25 }}>
<img
src="/logo512@2x.png"
style={{ width: "100px" }}
alt="Farm Control Logo"
></img>
<h1 style={{ marginTop: 10, marginBottom: 10 }}>Farm Control</h1>
<Text style={{ textAlign: "center" }}>
Please sign in using your credentials below.
</Text>
</Flex>
<Form
name="authmode-form"
className="auth-form"
initialValues={{ remember: true }}
onFinish={(e) => {
handleLogin(e);
}}
>
<Form.Item
name="email"
rules={[{ required: true, message: "Please input your Email!" }]}
>
<Space.Compact style={{ width: "100%" }}>
<Input
autoFocus={true}
prefix={<UserOutlined className="site-form-item-icon" />} // Use UserOutlined icon
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
disabled={authModes.length > 0 ? true : false}
/>
<Button
type="primary"
icon={<ArrowRightOutlined />}
htmlType="submit"
disabled={authModes.length > 0 ? true : false}
/>
</Space.Compact>
</Form.Item>
</Form>
{authModes.includes("password") ? (
<Form
name="auth-form"
className="auth-form"
initialValues={{ remember: true }}
onFinish={(e) => {
handleLogin(e);
}}
>
<Form.Item
name="password"
>
<Input
autoFocus={true}
prefix={<LockOutlined className="site-form-item-icon" />} // Use LockOutlined icon
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
</Form.Item>
<Form.Item>
<Flex style={{ width: "100%" }} gap="middle">
{authModes.includes("passkey") ? (
<Button
htmlType="submit"
icon={<PassKeysIcon />}
style={{ flexGrow: 3 }}
type="primary"
>
Use Passkey
</Button>
) : (
<></>
)}
<Button
htmlType="submit"
icon={<LoginOutlined />}
style={{ flexGrow: 1 }}
type={authModes.includes("passkey") ? "" : "primary"}
disabled={password.length > 0 ? false : true}
>
Login
</Button>
</Flex>
{error && <p style={{ color: "red" }}>{error}</p>}
</Form.Item>
</Form>
) : (
<></>
)}
<Divider plain></Divider>
<Button className="auth-form-button" icon={<UserAddOutlined />}>
Register
</Button>
</AuthLayout>
);
};
export default LoginUser;

View File

@ -0,0 +1,69 @@
import React, { useState, useContext, useEffect, useMemo } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import {
Form,
Input,
Button,
Checkbox,
Spin,
Divider,
Typography,
Flex,
Card,
Space,
} from "antd";
import { LockOutlined } from "@ant-design/icons";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import { AuthContext } from "./AuthContext";
import PassKeysIcon from "../Icons/PassKeysIcon"; // Adjust the path if necessary
import "./Auth.css";
import AuthLayout from "./AuthLayout";
const { Title, Text } = Typography;
const RegisterPasskey = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const navigate = useNavigate();
const { registerPasskey } = useContext(AuthContext);
const [init, setInit] = useState(false);
const handleRegisterPasskey = async (e) => {
const result = await registerPasskey(email, password);
if (result.successful === true) {
setTimeout(() => {
navigate("/dashboard/overview");
}, 500);
} else {}
};
return (
<AuthLayout>
<Flex vertical="true" align="center" style={{ marginBottom: 25 }}>
<PassKeysIcon style={{ fontSize: "64px" }} />
<h1 style={{ marginTop: 10, marginBottom: 10 }}>Register a Passkey</h1>
<Text style={{ textAlign: "center" }}>
Please setup a passkey in order to continue. The passkey may use
another device for encryption.
</Text>
</Flex>
<Button
type="primary"
className="auth-form-button"
icon={<LockOutlined />}
onClick={() => {
handleRegisterPasskey();
}}
>
Continue
</Button>
</AuthLayout>
);
};
export default RegisterPasskey;

View File

@ -0,0 +1,205 @@
import React, { useEffect, useState, useContext } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Skeleton, ColorPicker, Upload, Descriptions, Badge, Popconfirm } from "antd";
import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
const { Title, Text } = Typography;
const EditFillament = ({ id, onOk }) => {
const [messageApi, contextHolder] = message.useMessage();
const [dataLoading, setDataLoading] = useState(false);
const [editFillamentLoading, setEditFillamentLoading] = useState(false);
const [deleteFillamentLoading, setDeleteFillamentLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [fillament, setFillament] = useState(null);
const [imageList, setImageList] = useState([]);
const [image, setImage] = useState("");
const [editFillamentForm] = Form.useForm();
const [editFillamentFormValues, setEditFillamentFormValues] = useState({});
const { token } = useContext(AuthContext);
useEffect(() => {
// Fetch printer details when the component mounts
const fetchFillamentDetails = async () => {
if (id) {
try {
setDataLoading(true);
const response = await axios.get(`http://localhost:8080/fillaments/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setDataLoading(false);
editFillamentForm.setFieldsValue(response.data); // Set form values with fetched data
setEditFillamentFormValues(response.data);
} catch (error) {
messageApi.error('Error fetching printer details:' + error.message);
}
}
};
fetchFillamentDetails();
}, [id, editFillamentForm, token]);
const handleEditFillament = async () => {
setEditFillamentLoading(true);
// Exclude the 'online' field from the submission
try {
await axios.put(`http://localhost:8080/fillaments/${id}`, editFillamentFormValues, {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Fillament details updated successfully.');
onOk();
} catch (error) {
messageApi.error('Error updating fillament details: ' + error.message);
} finally {
setEditFillamentLoading(false);
}
};
const handleDeleteFillament = async () => {
setDeleteFillamentLoading(true);
try {
await axios.delete(`http://localhost:8080/fillaments/${id}`, "", {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Fillament deleted successfully.');
onOk();
} catch (error) {
messageApi.error('Error updating fillament details: ' + error.message);
} finally {
setDeleteFillamentLoading(false);
}
};
const handleImageUpload = ({ file, onSuccess }) => {
const reader = new FileReader();
reader.onload = (e) => {
console.log("Setting image buffer", e.target.result);
//setImage(e.target.result);
onSuccess("ok");
};
reader.readAsDataURL(file);
};
return (
<>
{contextHolder}
<Spin spinning={dataLoading} indicator={<LoadingOutlined spin />} size="large">
<Form
name="editFillamentForm"
autoComplete="off"
form={editFillamentForm}
initialValues={editFillamentFormValues}
onFinish={handleEditFillament}
onValuesChange={(changedValues) => setEditFillamentFormValues((prevValues) => ({
...prevValues,
...changedValues,
}))}
>
<Form.Item
label="Name"
name="name"
>
<Input />
</Form.Item>
<Form.Item
label="Brand"
name="brand"
>
<Input />
</Form.Item>
<Form.Item label="Material" name="type">
<Select>
<Select.Option value="PLA">PLA</Select.Option>
<Select.Option value="PETG">PETG</Select.Option>
<Select.Option value="ABS">ABS</Select.Option>
<Select.Option value="ASA">ASA</Select.Option>
<Select.Option value="HIPS">HIPS</Select.Option>
<Select.Option value="TPU">TPU</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Price"
name="price"
>
<InputNumber controls={false} formatter={(value) => {
if (!value) return '£';
return `£${value}`;
}} step={0.01} style={{ width: "100%" }} addonAfter="per kg" />
</Form.Item>
<Form.Item
label="Colour"
name="color"
getValueFromEvent={(color) => {
return "#" + color.toHex();
}}
>
<ColorPicker showText disabledAlpha/>
</Form.Item>
<Form.Item label="Diamenter" name="diameter">
<Select>
<Select.Option value="1.75">1.75mm</Select.Option>
<Select.Option value="2.85">2.85mm</Select.Option>
<Select.Option value="3.00">3.00mm</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Image" name="image">
<Upload
listType="picture"
maxCount={1}
className="upload-list-inline"
fileList={imageList}
customRequest={handleImageUpload}
onChange={({ fileList }) => { setImageList(fileList) }}
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>
<Form.Item label="URL" name="url">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
<Form.Item label="Barcode" name="barcode">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
<Form.Item>
<Flex gap="middle" horizontal="true">
<Button type="primary" htmlType="submit" loading={editFillamentLoading}>
Update
</Button>
<Popconfirm
title="Delete Fillament"
description={"Are you sure you want to delete " + editFillamentFormValues.name + "?"}
onConfirm={handleDeleteFillament}
okText="Yes"
cancelText="No"
>
<Button danger>Delete</Button>
</Popconfirm>
</Flex>
</Form.Item>
</Form>
</Spin>
</>
);
};
export default EditFillament;

View File

@ -0,0 +1,170 @@
// src/fillaments.js
import React, { useEffect, useState, useReducer, useContext } from "react";
import axios from "axios";
import moment from "moment";
import { useNavigate, useOutletContext } from "react-router-dom";
import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, Drawer, message } from "antd";
import { InfoCircleOutlined, EditOutlined, LoadingOutlined, ControlOutlined, PlusOutlined, CopyOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
import NewFillament from "./NewFillament";
import EditFillament from "./EditFillament";
const { Title } = Typography;
const Fillaments = () => {
const [messageApi, contextHolder] = message.useMessage();
const initialState = {
error: null,
};
const [fillamentsData, setFillamentsData] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [newFillamentOpen, setNewFillamentOpen] = useState(false);
const [newFillament, setNewFillament] = useState(null);
const [loading, setLoading] = useState(true);
const [editFillamentOpen, setEditFillamentOpen] = useState(false);
const [editFillament, setEditFillament] = useState(null);
const { token, logout } = useContext(AuthContext);
const { socket } = useContext(SocketContext);
const navigate = useNavigate();
const fetchFillamentsData = async () => {
try {
const response = await axios.get("http://localhost:8080/fillaments", {
params: {
page: 1,
limit: 25,
},
headers: {
Authorization: `Bearer ${token}`,
},
});
setFillamentsData(response.data);
setLoading(false);
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
} catch (err) {
console.error(err);
}
};
useEffect(() => {
// Fetch initial data
fetchFillamentsData();
}, [token]);
// Column definitions
const columns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Brand",
dataIndex: "brand",
key: "brand",
},
{
title: "Material",
dataIndex: "type",
key: "type",
},
{
title: "Price",
dataIndex: "price",
key: "type",
render: (price) => {
return "£" + price + " per kg";
},
},
{
title: "Colour",
dataIndex: "color",
key: "color",
render: (color) => {
return <Badge color={color} text={color}/>;
},
},
{
title: "Updated At",
dataIndex: "updated_at",
key: "updated_at",
render: (updated_at) => {
if (updated_at !== null) {
const formattedDate = moment(updated_at.$date).format(
"YYYY-MM-DD HH:mm:ss"
);
return <span>{formattedDate}</span>;
} else {
return "n/a";
}
},
},
{
title: "Actions",
key: "operation",
fixed: "right",
width: 100,
render: (text, record) => {
return (
<Flex gap="small" horizontal="true">
<Button type="link" icon={<EditOutlined />} onClick={() => { handleEdit(record.id); }}/>
</Flex>);
},
},
];
const handleNew = () => {
setNewFillamentOpen(true);
};
const handleEdit = (id) => {
setEditFillament(<EditFillament id={id} onOk={() => { setEditFillamentOpen(false); fetchFillamentsData(); }} />);
setEditFillamentOpen(true);
};
return (
<>
<Flex vertical={"true"} gap="large">
{contextHolder}
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={handleNew}>New Fillament</Button>
</Space>
<Table
dataSource={fillamentsData}
columns={columns}
pagination={pagination}
rowKey="id"
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
/>
</Flex>
<Modal open={newFillamentOpen} footer={null} width={700} onCancel={() => { setNewFillamentOpen(false); }}>
<NewFillament onOk={() => { setNewFillamentOpen(false); fetchFillamentsData(); }} reset={newFillamentOpen}/>
</Modal>
<Drawer open={editFillamentOpen} title={"Edit Fillament"} onClose={() => { setEditFillamentOpen(false); }}>
{editFillament}
</Drawer>
</>
);
};
export default Fillaments;

View File

@ -0,0 +1,331 @@
import React, { useEffect, useState, useContext } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Divider, ColorPicker, Upload, Descriptions, Badge } from "antd";
import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
const { Title, Text } = Typography;
const initialNewFillamentForm = {
name: "",
brand: "",
type: "",
price: 0,
color: "#FFFFFF",
diameter: "1.75",
image: null,
url: "",
barcode: "",
};
const NewFillament = ({ onOk, reset }) => {
const [messageApi, contextHolder] = message.useMessage();
const [newFillamentLoading, setNewFillamentLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [nextEnabled, setNextEnabled] = useState(false);
const [newFillamentForm] = Form.useForm();
const [newFillamentFormValues, setNewFillamentFormValues] = useState(initialNewFillamentForm);
const [imageList, setImageList] = useState([]);
const newFillamentFormUpdateValues = Form.useWatch([], newFillamentForm);
const { token } = useContext(AuthContext);
React.useEffect(() => {
newFillamentForm
.validateFields({
validateOnly: true,
})
.then(() => setNextEnabled(true))
.catch(() => setNextEnabled(false));
}, [newFillamentForm, newFillamentFormUpdateValues]);
const summaryItems = [
{
key: 'name',
label: 'Name',
children: newFillamentFormValues.name,
},
{
key: 'brand',
label: 'Brand',
children: newFillamentFormValues.brand,
},
{
key: 'type',
label: 'Material',
children: newFillamentFormValues.type,
},
{
key: 'price',
label: 'Price',
children: "£" + newFillamentFormValues.price + " per kg",
},
{
key: 'color',
label: 'Colour',
children: (<Badge color={newFillamentFormValues.color} text={newFillamentFormValues.color} />)
},
{
key: 'diameter',
label: 'Diameter',
children: newFillamentFormValues.diameter + "mm",
},
{
key: 'image',
label: 'Image',
children: (<img src={newFillamentFormValues.image} style={{width: 128}}></img>),
},
{
key: 'url',
label: 'URL',
children: newFillamentFormValues.url,
},
{
key: 'barcode',
label: 'Barcode',
children: newFillamentFormValues.barcode,
},
];
React.useEffect(() => {
console.log("reset changed")
if (reset) {
console.log("resetting")
newFillamentForm.resetFields();
}
}, [reset, newFillamentForm])
const handleNewFillament = async () => {
setNewFillamentLoading(true);
try {
await axios.post(`http://localhost:8080/fillaments`, newFillamentFormValues, {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('New fillament created successfully.');
onOk();
} catch (error) {
messageApi.error('Error creating new fillament: ' + error.message);
} finally {
setNewFillamentLoading(false);
}
};
const getBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};
const handleImageUpload = async ({ file, fileList }) => {
console.log(fileList);
if (fileList.length == 0) {
setImageList(fileList)
newFillamentForm.setFieldsValue({ image: "" });
return;
}
const base64 = await getBase64(file);
setNewFillamentFormValues((prevValues) => ({
...prevValues,
image: base64,
}));
fileList[0].name = "Fillament Image"
setImageList(fileList)
newFillamentForm.setFieldsValue({ image: base64 });
};
const steps = [
{
title: 'Required',
key: 'required',
content: (
<>
<Form.Item>
<Text>Required information:</Text>
</Form.Item>
<Form.Item
label="Name"
name="name"
rules={[
{
required: true,
message: 'Please enter a name.',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Brand"
name="brand"
rules={[
{
required: true,
message: 'Please enter a brand.',
},
]}
>
<Input />
</Form.Item>
<Form.Item label="Material" name="type" rules={[
{
required: true,
message: 'Please select a material.',
},
]}>
<Select>
<Select.Option value="PLA">PLA</Select.Option>
<Select.Option value="PETG">PETG</Select.Option>
<Select.Option value="ABS">ABS</Select.Option>
<Select.Option value="ASA">ASA</Select.Option>
<Select.Option value="HIPS">HIPS</Select.Option>
<Select.Option value="TPU">TPU</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Price"
name="price"
rules={[
{
required: true,
message: 'Please enter a price.',
}]}
>
<InputNumber controls={false} formatter={(value) => {
if (!value) return '£';
return `£${value}`;
}} step={0.01} style={{ width: "100%" }} addonAfter="per kg" />
</Form.Item>
</>
),
},
{
title: 'Optional',
key: 'optional',
content: (
<>
<Form.Item>
<Text>Optional information:</Text>
</Form.Item>
<Form.Item
label="Colour"
name="color"
getValueFromEvent={(color) => {
return "#" + color.toHex();
}}
>
<ColorPicker showText disabledAlpha />
</Form.Item>
<Form.Item label="Diamenter" name="diameter">
<Select>
<Select.Option value="1.75">1.75mm</Select.Option>
<Select.Option value="2.85">2.85mm</Select.Option>
<Select.Option value="3.00">3.00mm</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Image" name="image" getValueFromEvent={(e) => (Array.isArray(e) ? e : e && e.fileList)}>
<Upload
listType="picture"
name="Fillament Picture"
maxCount={1}
className="upload-list-inline"
fileList={imageList}
beforeUpload={() => false} // Prevent automatic upload
onChange={handleImageUpload}
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>
<Form.Item label="URL" name="url">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
<Form.Item label="Barcode" name="barcode">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
</>
),
},
{
title: 'Summary',
key: 'done',
content: (
<Form.Item>
<Descriptions column={1} items={summaryItems} size={"small"} />
</Form.Item>
),
},
];
return (
<Row>
{contextHolder}
<Col flex={1}>
<Steps current={currentStep} items={steps} direction="vertical" style={{ width: "fit-content" }} />
</Col>
<Col>
<Divider type={"vertical"} style={{ height: "100%" }} />
</Col>
<Col flex={3} style={{ paddingLeft: 15, maxWidth: 450 }}>
<Flex vertical={"true"}>
<Title level={2} style={{ marginTop: 0 }}>New Fillament</Title>
<Form
name="basic"
autoComplete="off"
form={newFillamentForm}
onFinish={handleNewFillament}
onValuesChange={(changedValues) => setNewFillamentFormValues((prevValues) => ({
...prevValues,
...changedValues,
}))}
initialValues={initialNewFillamentForm}
>
{steps[currentStep].content}
<Flex justify={"end"}>
<Button
style={{
margin: '0 8px',
}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!(currentStep > 0)}>
Previous
</Button>
{currentStep < steps.length - 1 && (
<Button type="primary" disabled={!nextEnabled} onClick={() => { setCurrentStep(currentStep + 1) }}>
Next
</Button>
)}
{currentStep === steps.length - 1 && (
<Button type="primary" htmlType="submit" loading={newFillamentLoading}>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Col>
</Row>
);
};
export default NewFillament;

View File

@ -0,0 +1,205 @@
import React, { useEffect, useState, useContext } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Skeleton, ColorPicker, Upload, Descriptions, Badge, Popconfirm } from "antd";
import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
const { Title, Text } = Typography;
const EditFillament = ({ id, onOk }) => {
const [messageApi, contextHolder] = message.useMessage();
const [dataLoading, setDataLoading] = useState(false);
const [editFillamentLoading, setEditFillamentLoading] = useState(false);
const [deleteFillamentLoading, setDeleteFillamentLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [fillament, setFillament] = useState(null);
const [imageList, setImageList] = useState([]);
const [image, setImage] = useState("");
const [editFillamentForm] = Form.useForm();
const [editFillamentFormValues, setEditFillamentFormValues] = useState({});
const { token } = useContext(AuthContext);
useEffect(() => {
// Fetch printer details when the component mounts
const fetchFillamentDetails = async () => {
if (id) {
try {
setDataLoading(true);
const response = await axios.get(`http://localhost:8080/fillaments/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setDataLoading(false);
editFillamentForm.setFieldsValue(response.data); // Set form values with fetched data
setEditFillamentFormValues(response.data);
} catch (error) {
messageApi.error('Error fetching printer details:' + error.message);
}
}
};
fetchFillamentDetails();
}, [id, editFillamentForm]);
const handleEditFillament = async () => {
setEditFillamentLoading(true);
// Exclude the 'online' field from the submission
try {
await axios.put(`http://localhost:8080/fillaments/${id}`, editFillamentFormValues, {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Fillament details updated successfully.');
onOk();
} catch (error) {
messageApi.error('Error updating fillament details: ' + error.message);
} finally {
setEditFillamentLoading(false);
}
};
const handleDeleteFillament = async () => {
setDeleteFillamentLoading(true);
try {
await axios.delete(`http://localhost:8080/fillaments/${id}`, "", {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Fillament deleted successfully.');
onOk();
} catch (error) {
messageApi.error('Error updating fillament details: ' + error.message);
} finally {
setDeleteFillamentLoading(false);
}
};
const handleImageUpload = ({ file, onSuccess }) => {
const reader = new FileReader();
reader.onload = (e) => {
console.log("Setting image buffer", e.target.result);
//setImage(e.target.result);
onSuccess("ok");
};
reader.readAsDataURL(file);
};
return (
<>
{contextHolder}
<Spin spinning={dataLoading} indicator={<LoadingOutlined spin />} size="large">
<Form
name="editFillamentForm"
autoComplete="off"
form={editFillamentForm}
initialValues={editFillamentFormValues}
onFinish={handleEditFillament}
onValuesChange={(changedValues) => setEditFillamentFormValues((prevValues) => ({
...prevValues,
...changedValues,
}))}
>
<Form.Item
label="Name"
name="name"
>
<Input />
</Form.Item>
<Form.Item
label="Brand"
name="brand"
>
<Input />
</Form.Item>
<Form.Item label="Material" name="type">
<Select>
<Select.Option value="PLA">PLA</Select.Option>
<Select.Option value="PETG">PETG</Select.Option>
<Select.Option value="ABS">ABS</Select.Option>
<Select.Option value="ASA">ASA</Select.Option>
<Select.Option value="HIPS">HIPS</Select.Option>
<Select.Option value="TPU">TPU</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Price"
name="price"
>
<InputNumber controls={false} formatter={(value) => {
if (!value) return '£';
return `£${value}`;
}} step={0.01} style={{ width: "100%" }} addonAfter="per kg" />
</Form.Item>
<Form.Item
label="Colour"
name="color"
getValueFromEvent={(color) => {
return "#" + color.toHex();
}}
>
<ColorPicker showText disabledAlpha/>
</Form.Item>
<Form.Item label="Diamenter" name="diameter">
<Select>
<Select.Option value="1.75">1.75mm</Select.Option>
<Select.Option value="2.85">2.85mm</Select.Option>
<Select.Option value="3.00">3.00mm</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Image" name="image">
<Upload
listType="picture"
maxCount={1}
className="upload-list-inline"
fileList={imageList}
customRequest={handleImageUpload}
onChange={({ fileList }) => { setImageList(fileList) }}
>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>
<Form.Item label="URL" name="url">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
<Form.Item label="Barcode" name="barcode">
<Input
prefix={<LinkOutlined />}
/>
</Form.Item>
<Form.Item>
<Flex gap="middle" horizontal="true">
<Button type="primary" htmlType="submit" loading={editFillamentLoading}>
Update
</Button>
<Popconfirm
title="Delete Fillament"
description={"Are you sure you want to delete " + editFillamentFormValues.name + "?"}
onConfirm={handleDeleteFillament}
okText="Yes"
cancelText="No"
>
<Button danger>Delete</Button>
</Popconfirm>
</Flex>
</Form.Item>
</Form>
</Spin>
</>
);
};
export default EditFillament;

View File

@ -0,0 +1,170 @@
// src/gcodefiles.js
import React, { useEffect, useState, useReducer, useContext } from "react";
import axios from "axios";
import moment from "moment";
import { useNavigate, useOutletContext } from "react-router-dom";
import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, Drawer, message } from "antd";
import { InfoCircleOutlined, EditOutlined, LoadingOutlined, ControlOutlined, PlusOutlined, CopyOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
import NewGCodeFile from "./NewGCodeFile";
import EditGCodeFile from "./EditGCodeFile";
const { Title } = Typography;
const GCodeFiles = () => {
const [messageApi, contextHolder] = message.useMessage();
const initialState = {
error: null,
};
const [gcodeFilesData, setGCodeFilesData] = useState([]);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false);
const [newGCodeFile, setNewGCodeFile] = useState(null);
const [loading, setLoading] = useState(true);
const [editGCodeFileOpen, setEditGCodeFileOpen] = useState(false);
const [editGCodeFile, setEditGCodeFile] = useState(null);
const { token, logout } = useContext(AuthContext);
const { socket } = useContext(SocketContext);
const navigate = useNavigate();
const fetchGCodeFilesData = async () => {
try {
const response = await axios.get("http://localhost:8080/gcodefiles", {
params: {
page: 1,
limit: 25,
},
headers: {
Authorization: `Bearer ${token}`,
},
});
setGCodeFilesData(response.data);
setLoading(false);
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
} catch (err) {
console.error(err);
}
};
useEffect(() => {
// Fetch initial data
fetchGCodeFilesData();
}, [token]);
// Column definitions
const columns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Brand",
dataIndex: "brand",
key: "brand",
},
{
title: "Material",
dataIndex: "type",
key: "type",
},
{
title: "Price",
dataIndex: "price",
key: "type",
render: (price) => {
return "£" + price + " per kg";
},
},
{
title: "Colour",
dataIndex: "color",
key: "color",
render: (color) => {
return <Badge color={color} text={color}/>;
},
},
{
title: "Updated At",
dataIndex: "updated_at",
key: "updated_at",
render: (updated_at) => {
if (updated_at !== null) {
const formattedDate = moment(updated_at.$date).format(
"YYYY-MM-DD HH:mm:ss"
);
return <span>{formattedDate}</span>;
} else {
return "n/a";
}
},
},
{
title: "Actions",
key: "operation",
fixed: "right",
width: 100,
render: (text, record) => {
return (
<Flex gap="small" horizontal="true">
<Button type="link" icon={<EditOutlined />} onClick={() => { handleEdit(record.id); }}/>
</Flex>);
},
},
];
const handleNew = () => {
setNewGCodeFileOpen(true);
};
const handleEdit = (id) => {
setEditGCodeFile(<EditGCodeFile id={id} onOk={() => { setEditGCodeFileOpen(false); fetchGCodeFilesData(); }} />);
setEditGCodeFileOpen(true);
};
return (
<>
<Flex vertical={"true"} gap="large">
{contextHolder}
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={handleNew}>New GCodeFile</Button>
</Space>
<Table
dataSource={gcodeFilesData}
columns={columns}
pagination={pagination}
rowKey="id"
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
/>
</Flex>
<Modal open={newGCodeFileOpen} footer={null} width={700} onCancel={() => { setNewGCodeFileOpen(false); }}>
<NewGCodeFile onOk={() => { setNewGCodeFileOpen(false); fetchGCodeFilesData(); }} reset={newGCodeFileOpen}/>
</Modal>
<Drawer open={editGCodeFileOpen} title={"Edit GCodeFile"} onClose={() => { setEditGCodeFileOpen(false); }}>
{editGCodeFile}
</Drawer>
</>
);
};
export default GCodeFiles;

View File

@ -0,0 +1,295 @@
import React, { useEffect, useState, useContext } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Divider, ColorPicker, Upload, Descriptions, Badge, } from "antd";
import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import GCodeFileIcon from '../../Icons/GCodeFileIcon';
import FillamentSelect from '../common/FillamentSelect';
const { Dragger } = Upload;
const { Title, Text } = Typography;
const initialNewGCodeFileForm = {
name: "",
brand: "",
type: "",
price: 0,
color: "#FFFFFF",
diameter: "1.75",
image: null,
url: "",
barcode: "",
};
const NewGCodeFile = ({ onOk, reset }) => {
const [messageApi, contextHolder] = message.useMessage();
const [newGCodeFileLoading, setNewGCodeFileLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [nextEnabled, setNextEnabled] = useState(false);
const [newGCodeFileForm] = Form.useForm();
const [newGCodeFileFormValues, setNewGCodeFileFormValues] = useState(initialNewGCodeFileForm);
const [imageList, setImageList] = useState([]);
const [gcode, setGCode] = useState("");
const newGCodeFileFormUpdateValues = Form.useWatch([], newGCodeFileForm);
const { token } = useContext(AuthContext);
React.useEffect(() => {
newGCodeFileForm
.validateFields({
validateOnly: true,
})
.then(() => setNextEnabled(true))
.catch(() => setNextEnabled(false));
}, [newGCodeFileForm, newGCodeFileFormUpdateValues]);
const summaryItems = [
{
key: 'name',
label: 'Name',
children: newGCodeFileFormValues.name,
},
{
key: 'brand',
label: 'Brand',
children: newGCodeFileFormValues.brand,
},
{
key: 'type',
label: 'Material',
children: newGCodeFileFormValues.type,
},
{
key: 'price',
label: 'Price',
children: "£" + newGCodeFileFormValues.price + " per kg",
},
{
key: 'color',
label: 'Colour',
children: (<Badge color={newGCodeFileFormValues.color} text={newGCodeFileFormValues.color} />)
},
{
key: 'diameter',
label: 'Diameter',
children: newGCodeFileFormValues.diameter + "mm",
},
{
key: 'image',
label: 'Image',
children: (<img src={newGCodeFileFormValues.image} style={{width: 128}}></img>),
},
{
key: 'url',
label: 'URL',
children: newGCodeFileFormValues.url,
},
{
key: 'barcode',
label: 'Barcode',
children: newGCodeFileFormValues.barcode,
},
];
React.useEffect(() => {
console.log("reset changed")
if (reset) {
console.log("resetting")
newGCodeFileForm.resetFields();
}
}, [reset, newGCodeFileForm])
const handleNewGCodeFile = async () => {
setNewGCodeFileLoading(true);
try {
await axios.post(`http://localhost:8080/gcodefiles`, newGCodeFileFormValues, {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('New G Code file created successfully.');
onOk();
} catch (error) {
messageApi.error('Error creating new gcode file: ' + error.message);
} finally {
setNewGCodeFileLoading(false);
}
};
const getBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};
const handleImageUpload = async ({ file, fileList }) => {
console.log(fileList);
if (fileList.length == 0) {
setImageList(fileList)
newGCodeFileForm.setFieldsValue({ image: "" });
return;
}
const base64 = await getBase64(file);
setNewGCodeFileFormValues((prevValues) => ({
...prevValues,
image: base64,
}));
fileList[0].name = "GCodeFile Image"
setImageList(fileList)
newGCodeFileForm.setFieldsValue({ image: base64 });
};
const steps = [
{
title: 'Details',
key: 'details',
content: (
<>
<Form.Item>
<Text>Please provide the following information:</Text>
</Form.Item>
<Form.Item
label="Name"
name="name"
rules={[
{
required: true,
message: 'Please enter a name.',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Material"
name="material"
rules={[
{
required: true,
message: 'Please provide a materal.',
},
]}
>
<FillamentSelect />
</Form.Item>
</>
),
},
{
title: 'Upload',
key: 'upload',
content: (
<>
<Form.Item name="gcodefile" rules={[
{
required: true,
message: 'Please upload a gcode file.',
},
]} getValueFromEvent={(e) => (Array.isArray(e) ? e : e && e.fileList)}>
<Dragger name="G Code File"
maxCount={1} showUploadList={false}>
<p className="ant-upload-drag-icon">
<GCodeFileIcon />
</p>
<p className="ant-upload-text">Click or drag .gcode or .g file here.</p>
<p className="ant-upload-hint">
Support for a single or bulk upload. Strictly prohibited from uploading company data or other
banned files.
</p>
</Dragger>
</Form.Item>
</>
),
},
{
title: 'Preview',
key: 'preview',
content: (
<>
</>
),
},
{
title: 'Summary',
key: 'done',
content: (
<Form.Item>
<Descriptions column={1} items={summaryItems} size={"small"} />
</Form.Item>
),
},
];
return (
<Row>
{contextHolder}
<Col flex={1}>
<Steps current={currentStep} items={steps} direction="vertical" style={{ width: "fit-content" }} />
</Col>
<Col>
<Divider type={"vertical"} style={{ height: "100%" }} />
</Col>
<Col flex={3} style={{ paddingLeft: 15, maxWidth: 450 }}>
<Flex vertical={"true"}>
<Title level={2} style={{ marginTop: 0 }}>New G Code File</Title>
<Form
name="basic"
autoComplete="off"
form={newGCodeFileForm}
onFinish={handleNewGCodeFile}
onValuesChange={(changedValues) => setNewGCodeFileFormValues((prevValues) => ({
...prevValues,
...changedValues,
}))}
initialValues={initialNewGCodeFileForm}
>
{steps[currentStep].content}
<Flex justify={"end"}>
<Button
style={{
margin: '0 8px',
}}
onClick={() => setCurrentStep(currentStep - 1)}
disabled={!(currentStep > 0)}>
Previous
</Button>
{currentStep < steps.length - 1 && (
<Button type="primary" disabled={!nextEnabled} onClick={() => { setCurrentStep(currentStep + 1) }}>
Next
</Button>
)}
{currentStep === steps.length - 1 && (
<Button type="primary" htmlType="submit" loading={newGCodeFileLoading}>
Done
</Button>
)}
</Flex>
</Form>
</Flex>
</Col>
</Row>
);
};
export default NewGCodeFile;

View File

@ -0,0 +1,44 @@
import React, { useEffect, useState } from 'react';
import DashboardLayout from './common/DashboardLayout';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
const Overview = ({ setToken }) => {
const [user, setUser] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchUserData = async () => {
const access_token = localStorage.getItem('access_token');
if (access_token) {
try {
const response = await axios.get('http://localhost:8080/overview', {
headers: {
Authorization: `Bearer ${access_token}`
}
});
//setUser(response.data);
} catch (err) {
console.error(err);
}
}
};
fetchUserData();
}, [setToken, navigate]);
return (
<div>
<h2>Overview</h2>
{user ? (
<div>
<p>Welcome, {user.username}!</p>
<button >Logout</button>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
};
export default Overview;

View File

@ -0,0 +1,56 @@
import React, { useEffect, useState, useContext } from 'react';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, Button, message, Spin, Typography, Tag, Flex, Steps } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
const { Title } = Typography;
const NewPrintJob = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(false);
const { token } = useContext(AuthContext);
const handleFormSubmit = async (values) => {
setLoading(true);
// Exclude the 'online' field from the submission
const { online, remoteAddress, hostId, ...rest } = values;
try {
await axios.put(`http://localhost:8080/printers/${remoteAddress}`, rest, {
headers: {
Authorization: `Bearer ${token}`,
}
});
message.success('Printer details updated successfully');
} catch (error) {
message.error('Error updating printer details');
} finally {
setLoading(false);
}
};
return (
<div>
<Title>Select G-Code</Title>
<Steps
current={currentStep}
items={[
{
title: 'Select G-Code',
},
{
title: 'Done',
},
]}
/>
</div>
);
};
export default NewPrintJob;

View File

@ -0,0 +1,285 @@
// src/PrintJobs.js
import React, { useEffect, useState, useReducer, useContext } from "react";
import axios from "axios";
import moment from "moment";
import { useNavigate, useOutletContext } from "react-router-dom";
import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, message } from "antd";
import { InfoCircleOutlined, EditOutlined, ControlOutlined, PlusOutlined, CopyOutlined, LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
import NewPrintJob from "./NewPrintJob";
const { Title } = Typography;
// Action types for reducer
const actionTypes = {
UPDATE_PRINTER_DATA: 'UPDATE_PRINTER_DATA',
FETCH_DATA_SUCCESS: 'FETCH_DATA_SUCCESS',
FETCH_DATA_FAILURE: 'FETCH_DATA_FAILURE',
};
// Reducer function to manage state updates
const reducer = (state, action) => {
switch (action.type) {
case actionTypes.UPDATE_PRINTER_DATA:
return {
...state,
printerData: updatePrinterData(state.printerData, action.payload),
};
case actionTypes.FETCH_DATA_SUCCESS:
return {
...state,
printJobsData: action.payload,
error: null,
};
case actionTypes.FETCH_DATA_FAILURE:
return {
...state,
error: action.payload,
};
default:
return state;
}
};
// Helper function to update printerData based on wsData
const updatePrinterData = (printerData, newData) => {
const updatedData = [...printerData]; // Copy current state
const existingIndex = updatedData.findIndex(printer => printer.remoteAddress === newData.remoteAddress);
if (existingIndex !== -1) {
// Update existing entry
const existingEntry = updatedData[existingIndex];
const updatedEntry = { ...existingEntry };
// Update only the parameters that exist in newData
for (const param in newData) {
if (newData.hasOwnProperty(param)) {
updatedEntry[param] = newData[param];
}
}
updatedData[existingIndex] = updatedEntry;
} else {
// Add new entry
updatedData.push(newData);
}
return updatedData;
};
const PrintJobs = () => {
const [messageApi, contextHolder] = message.useMessage();
const initialState = {
printJobsData: [],
error: null,
};
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [state, dispatch] = useReducer(reducer, initialState);
const [newPrintJobOpen, setNewPrintJobOpen] = useState(false);
const [loading, setLoading] = useState(true);
const { token, logout } = useContext(AuthContext);
const { socket } = useContext(SocketContext);
const navigate = useNavigate();
useEffect(() => {
// Fetch initial data
const fetchData = async () => {
try {
const response = await axios.get("http://localhost:8080/printjobs", {
params: {
page: 1,
limit: 25,
},
headers: {
Authorization: `Bearer ${token}`,
},
});
setLoading(false);
dispatch({ type: actionTypes.FETCH_DATA_SUCCESS, payload: response.data });
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
} catch (err) {
console.error(err);
}
};
fetchData();
}, [token]);
useEffect(() => {
if (socket) {
socket.on("status", (statusUpdate) => {
console.log("Received status:", statusUpdate);
dispatch({ type: "UPDATE_PRINTER_DATA", payload: statusUpdate });
});
return () => {
socket.off("status");
};
}
}, [socket]);
const handleTableChange = (pagination, filters, sorter) => {
setPagination(pagination); // Update pagination state on table change
};
// Column definitions
const columns = [
{
title: "ID",
dataIndex: "id",
key: "id",
render: (text) => (
<span>
{text.slice(-8)}
<Tooltip title="Copy ID" arrow={false}>
<Button
icon={<CopyOutlined />}
type="link"
style={{ marginLeft: 8 }}
onClick={() => {
navigator.clipboard.writeText(text).then(() => {
messageApi.success('ID copied to clipboard');
}).catch(() => {
messageApi.error('Failed to copy ID');
});
}}
/>
</Tooltip>
</span>
)
},
{
title: 'Status',
key: 'status',
dataIndex: 'status',
render: (status) => {
let badgeStatus;
let badgeText;
switch (status.type) {
case 'Queued':
badgeStatus = 'warning';
badgeText = 'Queued';
break;
case 'Draft':
badgeStatus = 'default';
badgeText = 'Draft';
break;
case 'Printing':
badgeStatus = 'processing';
badgeText = 'Printing';
break;
case 'Error':
badgeStatus = 'error';
badgeText = 'Error';
break;
default:
badgeStatus = 'default';
badgeText = 'Unknown';
}
return (
<Badge status={badgeStatus} text={badgeText} />
);
},
},
{
title: "Print Job",
dataIndex: "status",
key: "printJob",
width: "15%",
render: (status) => {
if (status.type == "Printing") {
return (
<Progress percent={status.percent} />
);
}
},
},
{
title: "Print Job Started At",
dataIndex: "started_at",
key: "started_at",
render: (started_at) => {
if (started_at !== null) {
const formattedDate = moment(started_at.$date).format(
"YYYY-MM-DD HH:mm:ss"
);
return <span>{formattedDate}</span>;
} else {
return "n/a";
}
},
},
{
title: "Created At",
dataIndex: "created_at",
key: "created_at",
render: (created_at) => {
if (created_at !== null) {
const formattedDate = moment(created_at.$date).format(
"YYYY-MM-DD HH:mm:ss"
);
return <span>{formattedDate}</span>;
} else {
return "n/a";
}
},
},
{
title: "Actions",
key: "operation",
fixed: "right",
width: 100,
render: (text, record) => {
return (
<Flex gap="small" horizontal="true">
<Button type="link" icon={<EditOutlined />} onClick={() => navigate(`/dashboard/printjobs/edit?id=${record.id}`)} />
</Flex>);
},
},
];
const showNewPrintJobModal = () => {
setNewPrintJobOpen(true);
};
return (
<>
<Flex vertical={"true"} gap="large">
{contextHolder}
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={showNewPrintJobModal}>New Print Job</Button>
</Space>
<Table
dataSource={state.printJobsData}
columns={columns}
pagination={pagination}
rowKey="remoteAddress"
onChange={handleTableChange}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
/>
</Flex>
<Modal open={newPrintJobOpen} footer={null}>
<NewPrintJob/>
</Modal>
</>
);
};
export default PrintJobs;

View File

@ -0,0 +1,232 @@
import React, {
useEffect,
useState,
useContext,
useReducer,
useRef,
} from "react";
import axios from "axios";
import { useLocation, useNavigate } from "react-router-dom";
import {
Form,
Input,
Button,
message,
Spin,
Typography,
Tag,
Flex,
Col,
Row,
Dropdown,
Space,
Card,
Upload,
} from "antd";
import { LoadingOutlined, UploadOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
import DashboardTemperaturePanel from "../common/DashboardTemperaturePanel";
import DashboardMovementPanel from "../common/DashboardMovementPanel";
const { Title } = Typography;
// Action types for reducer
const actionTypes = {
UPDATE_PRINTER_DATA: "UPDATE_PRINTER_DATA",
FETCH_DATA_SUCCESS: "FETCH_DATA_SUCCESS",
FETCH_DATA_FAILURE: "FETCH_DATA_FAILURE",
};
// Reducer function to manage state updates
const reducer = (state, action) => {
switch (action.type) {
case actionTypes.UPDATE_PRINTER_DATA:
return {
...state,
printerData: updatePrinterData(state.printerData, action.payload),
};
case actionTypes.FETCH_DATA_SUCCESS:
return {
...state,
printerData: action.payload,
error: null,
};
case actionTypes.FETCH_DATA_FAILURE:
return {
...state,
error: action.payload,
};
default:
return state;
}
};
// Helper function to update printerData based on wsData
const updatePrinterData = (printerData, newData) => {
const updatedData = [...printerData]; // Copy current state
const existingIndex = updatedData.findIndex(
(printer) => printer.remoteAddress === newData.remoteAddress
);
if (existingIndex !== -1) {
// Update existing entry
updatedData[existingIndex] = { ...updatedData[existingIndex], ...newData };
} else {
// Add new entry
updatedData.push(newData);
}
return updatedData;
};
// Helper function to parse query parameters
const useQuery = () => {
return new URLSearchParams(useLocation().search);
};
const ControlPrinter = () => {
const initialState = {
printerData: [],
error: null,
};
const query = useQuery();
const remoteAddress = query.get("remoteAddress");
const navigate = useNavigate();
const [printer, setPrinter] = useState(null);
const { token, logout } = useContext(AuthContext);
const { socket } = useContext(SocketContext);
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
// Fetch printer details when the component mounts
const fetchPrinterDetails = async () => {
if (remoteAddress) {
try {
const response = await axios.get(
`http://localhost:8080/printers/${remoteAddress}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
setPrinter(response.data);
} catch (error) {
message.error("Error fetching printer details");
}
}
};
fetchPrinterDetails();
}, [token, logout, remoteAddress]);
useEffect(() => {
const joinPrinterRoom = () => {
if (socket) {
socket.on("status", (statusUpdate) => {
console.log("Received status:", statusUpdate);
dispatch({ type: "UPDATE_WS_DATA", payload: statusUpdate });
});
socket.emit("join", { remoteAddress });
return () => {
socket.off("status");
socket.emit("leave", { remoteAddress });
};
}
};
joinPrinterRoom();
}, [socket, remoteAddress]);
const sendCommand = (type, data) => {
const commandData = {
remoteAddress,
type,
data,
};
socket.emit("command", commandData);
};
const handleUpload = (file) => {
const reader = new FileReader();
reader.onload = () => {
sendCommand("writeToSD", {
filename: "test.g",
gcode: reader.result,
});
message.success("File uploaded successfully");
};
reader.readAsText(file);
};
const handleUploadFileButtonClick = () => {};
const uploadProps = {
beforeUpload: (file) => {
const isGCODE = file.name.endsWith(".gcode");
if (!isGCODE) {
message.error(`${file.name} is not a gcode file`);
}
return isGCODE || Upload.LIST_IGNORE;
},
onChange: (info) => {
},
};
return (
<Flex gap="large" vertical="true">
<Flex gap="middle" horizontal="true">
<Upload
{...uploadProps}
customRequest={({ file, onSuccess }) => {
handleUpload(file);
setTimeout(() => {
onSuccess("ok");
}, 0);
}}
>
<Button icon={<UploadOutlined />}>Select File</Button>
</Upload>
<Button
type="primary"
onClick={() => handleUploadFileButtonClick()}
style={{ marginRight: 8 }}
>
Upload
</Button>
</Flex>
{printer ? (
<Row gutter={16}>
<Col span={8}>
<Card title="Temperature" bordered={false}>
<DashboardTemperaturePanel
remoteAddress={remoteAddress}
></DashboardTemperaturePanel>
</Card>
</Col>
<Col span={8}>
<Card title="Movement" bordered={false}>
<DashboardMovementPanel
remoteAddress={remoteAddress}
></DashboardMovementPanel>
</Card>
</Col>
<Col span={8}>
<Card title="Card title" bordered={false}>
Card content
</Card>
</Col>
</Row>
) : (
<Spin indicator={<LoadingOutlined spin />} size="large" />
)}
</Flex>
);
};
export default ControlPrinter;

View File

@ -0,0 +1,129 @@
import React, { useEffect, useState, useContext } from 'react';
import DashboardLayout from '../common/DashboardLayout';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, Button, message, Spin, Typography, Tag, Flex, Popconfirm, Skeleton } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
const { Title } = Typography;
const EditPrinter = ({ remoteAddress, onOk }) => {
const [messageApi, contextHolder] = message.useMessage();
const navigate = useNavigate();
const [editPrinterForm] = Form.useForm();
const [editLoading, setEditLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [printer, setPrinter] = useState(null);
const { token, logout } = useContext(AuthContext);
useEffect(() => {
// Fetch printer details when the component mounts
const fetchPrinterDetails = async () => {
if (remoteAddress) {
try {
const response = await axios.get(`http://localhost:8080/printers/${remoteAddress}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setPrinter(response.data);
editPrinterForm.setFieldsValue(response.data); // Set form values with fetched data
} catch (error) {
messageApi.error('Error fetching printer details:' + error.message);
}
}
};
fetchPrinterDetails();
}, [remoteAddress, editPrinterForm]);
const handleEdit = async (values) => {
setEditLoading(true);
// Exclude the 'online' field from the submission
const { online, remoteAddress, hostId, ...rest } = values;
try {
await axios.put(`http://localhost:8080/printers/${remoteAddress}`, rest, {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Printer details updated successfully.');
onOk();
} catch (error) {
messageApi.error('Error updating printer details: ' + error.message);
} finally {
setEditLoading(false);
}
};
const handleDelete = async () => {
setDeleteLoading(true);
try {
await axios.delete(`http://localhost:8080/printers/${remoteAddress}`, "", {
headers: {
Authorization: `Bearer ${token}`,
}
});
messageApi.success('Printer details updated successfully.');
} catch (error) {
messageApi.error('Error updating printer details: ' + error.message);
} finally {
setDeleteLoading(false);
}
};
return (
<>
{contextHolder}
<Skeleton loading={printer == null} active>
<Form form={editPrinterForm} layout="vertical" onFinish={handleEdit}>
<Form.Item
label="Name"
name="friendlyName"
rules={[{ required: true, message: 'Name is required' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Remote Address"
name="remoteAddress"
rules={[{ required: true, message: 'Remote Address is required' }]}
>
<Input disabled />
</Form.Item>
<Form.Item
label="Host ID"
name="hostId"
>
<Input disabled />
</Form.Item>
<Form.Item>
<Flex gap="middle" horizontal="true">
<Button type="primary" htmlType="submit" loading={editLoading}>
Update
</Button>
<Popconfirm
title="Delete Printer"
description={"Are you sure you want to delete " + remoteAddress + "?"}
onConfirm={handleDelete}
okText="Yes"
cancelText="No"
>
<Button danger>Delete</Button>
</Popconfirm>
</Flex>
</Form.Item>
</Form>
</Skeleton>
</>
);
};
export default EditPrinter;

View File

@ -0,0 +1,270 @@
// src/Printers.js
import React, { useEffect, useState, useReducer, useContext } from "react";
import axios from "axios";
import moment from "moment";
import { useNavigate, useOutletContext } from "react-router-dom";
import { Table, Typography, Badge, Button, Flex, Progress, Space, Drawer } from "antd";
import { InfoCircleOutlined, EditOutlined, ControlOutlined, LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { SocketContext } from "../context/SocketContext";
import EditPrinter from "./EditPrinter"
const { Title } = Typography;
// Action types for reducer
const actionTypes = {
UPDATE_PRINTER_DATA: 'UPDATE_PRINTER_DATA',
FETCH_DATA_SUCCESS: 'FETCH_DATA_SUCCESS',
FETCH_DATA_FAILURE: 'FETCH_DATA_FAILURE',
};
// Reducer function to manage state updates
const reducer = (state, action) => {
switch (action.type) {
case actionTypes.UPDATE_PRINTER_DATA:
return {
...state,
printerData: updatePrinterData(state.printerData, action.payload),
};
case actionTypes.FETCH_DATA_SUCCESS:
return {
...state,
printerData: action.payload,
error: null,
};
case actionTypes.FETCH_DATA_FAILURE:
return {
...state,
error: action.payload,
};
default:
return state;
}
};
// Helper function to update printerData based on wsData
const updatePrinterData = (printerData, newData) => {
const updatedData = [...printerData]; // Copy current state
const existingIndex = updatedData.findIndex(printer => printer.remoteAddress === newData.remoteAddress);
if (existingIndex !== -1) {
// Update existing entry
const existingEntry = updatedData[existingIndex];
const updatedEntry = { ...existingEntry };
// Update only the parameters that exist in newData
for (const param in newData) {
if (newData.hasOwnProperty(param)) {
updatedEntry[param] = newData[param];
}
}
updatedData[existingIndex] = updatedEntry;
} else {
// Add new entry
updatedData.push(newData);
}
return updatedData;
};
const Printers = () => {
const initialState = {
printerData: [],
error: null,
};
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
});
const [state, dispatch] = useReducer(reducer, initialState);
const [loading, setLoading] = useState(true);
const [editPrinterOpen, setEditPrinterOpen] = useState(false);
const [editPrinter, setEditPrinter] = useState(null);
const { token, logout } = useContext(AuthContext);
const { socket } = useContext(SocketContext);
const navigate = useNavigate();
const fetchPrintersData = async () => {
try {
const response = await axios.get("http://localhost:8080/printers", {
params: {
page: 1,
limit: 25,
},
headers: {
Authorization: `Bearer ${token}`,
},
});
setLoading(false);
dispatch({ type: actionTypes.FETCH_DATA_SUCCESS, payload: response.data });
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
} catch (err) {
console.error(err);
}
};
useEffect(() => {
// Fetch initial data
fetchPrintersData();
}, [token]);
useEffect(() => {
if (socket) {
socket.on("status", (statusUpdate) => {
console.log("Received status:", statusUpdate);
dispatch({ type: "UPDATE_PRINTER_DATA", payload: statusUpdate });
});
return () => {
socket.off("status");
};
}
}, [socket]);
const handleTableChange = (pagination, filters, sorter) => {
setPagination(pagination); // Update pagination state on table change
};
const handleEdit = (remoteAddress) => {
setEditPrinter(<EditPrinter remoteAddress={remoteAddress} onOk={() => { setEditPrinterOpen(false); fetchPrintersData(); }} />);
setEditPrinterOpen(true);
};
// Column definitions
const columns = [
{
title: "Name",
dataIndex: "friendlyName",
key: "friendlyName",
},
{
title: "Remote Addresss",
dataIndex: "remoteAddress",
key: "remoteAddress",
},
{
title: "Host",
dataIndex: "hostId",
key: "hostId",
},
{
title: 'Status',
key: 'status',
dataIndex: 'status',
render: (status) => {
let badgeStatus;
let badgeText;
switch (status.type) {
case 'Online':
badgeStatus = 'success';
badgeText = 'Online';
break;
case 'Offline':
badgeStatus = 'default';
badgeText = 'Offline';
break;
case 'Initializing':
badgeStatus = 'warning';
badgeText = 'Initializing';
break;
case 'Printing':
badgeStatus = 'processing';
badgeText = 'Printing';
break;
case 'Processing':
badgeStatus = 'processing';
badgeText = 'Processing';
break;
case 'Idle':
badgeStatus = 'success';
badgeText = 'Idle';
break;
case 'Error':
badgeStatus = 'error';
badgeText = 'Error';
break;
default:
badgeStatus = 'default';
badgeText = 'Unknown';
}
return (
<Badge status={badgeStatus} text={badgeText} />
);
},
},
{
title: "Print Job",
dataIndex: "status",
key: "printJob",
width: "15%",
render: (status) => {
if (status.type == "Printing") {
return (
<Progress percent={status.percent} />
);
}
},
},
{
title: "Connected At",
dataIndex: "connectedAt",
key: "connectedAt",
render: (connectedAt) => {
if (connectedAt !== null) {
const formattedDate = moment(connectedAt.$date).format(
"YYYY-MM-DD HH:mm:ss"
);
return <span>{formattedDate}</span>;
} else {
return "n/a";
}
},
},
{
title: "Actions",
key: "operation",
fixed: "right",
width: 100,
render: (text, record) => {
return (
<Flex gap="small" horizontal="true">
<Button type="link" icon={<InfoCircleOutlined />} />
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record.remoteAddress)} />
<Button type="link" icon={<ControlOutlined />} onClick={() => navigate(`/dashboard/printers/control?remoteAddress=${record.remoteAddress}`)} disabled={record.status.type === "Offline" || record.status.type === "Initializing"} />
</Flex>);
},
},
];
return (
<>
<Table
dataSource={state.printerData}
columns={columns}
pagination={pagination}
rowKey="remoteAddress"
onChange={handleTableChange}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
/>
<Drawer open={editPrinterOpen} title={"Edit Printer"} onClose={() => { setEditPrinterOpen(false); }}>
{editPrinter}
</Drawer>
</>
);
};
export default Printers;

View File

@ -0,0 +1,108 @@
import React, { useEffect, useState, useContext } from 'react';
import DashboardLayout from './common/DashboardLayout';
import axios from 'axios';
import { useLocation, useNavigate } from 'react-router-dom';
import { Form, Input, Button, message, Spin, Typography, Tag, Flex } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { AuthContext } from "../Auth/AuthContext";
const { Title } = Typography;
const Profile = () => {
const navigate = useNavigate();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [printer, setPrinter] = useState(null);
const { token } = useContext(AuthContext);
useEffect(() => {
// Fetch printer details when the component mounts
const fetchPrinterDetails = async () => {
if (token) {
try {
const response = await axios.get(`http://localhost:8080/profile`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setPrinter(response.data);
form.setFieldsValue(response.data); // Set form values with fetched data
} catch (error) {
message.error('Error fetching printer details');
}
}
};
fetchPrinterDetails();
}, [form]);
const handleFormSubmit = async (values) => {
setLoading(true);
// Exclude the 'online' field from the submission
const { online, remoteAddress, hostId, ...rest } = values;
try {
await axios.put(`http://localhost:8080/profile`, rest, {
headers: {
Authorization: `Bearer ${token}`,
}
});
message.success('Profile updated successfully.');
} catch (error) {
message.error('Error updating profile: ' + error.message);
} finally {
setLoading(false);
}
};
return (
<div>
<Title>Edit Printer</Title>
{printer ? (
<Form form={form} layout="vertical" onFinish={handleFormSubmit}>
<Form.Item
label="Name"
name="friendlyName"
rules={[{ required: true, message: 'Name is required' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Remote Address"
name="remoteAddress"
rules={[{ required: true, message: 'Remote Address is required' }]}
>
<Input disabled />
</Form.Item>
<Form.Item
label="Host ID"
name="hostId"
>
<Input disabled />
</Form.Item>
<Form.Item
label="Status"
name="online"
valuePropName="checked"
>
{printer.online ? <Tag color="green">Online</Tag> : <Tag color="red">Offline</Tag>}
</Form.Item>
<Form.Item>
<Flex gap="middle" horizontal="true">
<Button type="primary" htmlType="submit" loading={loading}>
Update Printer
</Button>
<Button onClick={() => navigate(-1)}>Back</Button>
</Flex>
</Form.Item>
</Form>
) : (
<Spin indicator={<LoadingOutlined spin />} size="large" />
)}
</div>
);
};
export default Profile;

View File

@ -0,0 +1,14 @@
// Dashboard.js
import React, { useEffect, useState } from 'react';
import DashboardLayout from './DashboardLayout';
import { useNavigate, Outlet } from 'react-router-dom';
const Dashboard = () => {
return (
<DashboardLayout>
<Outlet />
</DashboardLayout>
);
};
export default Dashboard;

View File

@ -0,0 +1,37 @@
// DashboardBreadcrumb.js
import React from 'react';
import { Breadcrumb } from 'antd';
import { Link, useLocation } from 'react-router-dom';
const breadcrumbNameMap = {
'/dashboard': 'Dashboard',
'/dashboard/overview': 'Overview',
'/dashboard/printers': 'Printers',
'/dashboard/printers/control': 'Control Printer',
'/dashboard/printers/edit': 'Edit Printer',
'/dashboard/printjobs': 'Print Jobs',
'/dashboard/fillaments': 'Fillaments',
'/dashboard/gcodefiles': 'G Code Files',
};
const DashboardBreadcrumb = () => {
const location = useLocation();
const pathSnippets = location.pathname.split('/').filter(i => i);
const breadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{breadcrumbNameMap[url]}</Link>
</Breadcrumb.Item>
);
});
return (
<Breadcrumb>
{breadcrumbItems}
</Breadcrumb>
);
};
export default DashboardBreadcrumb;

View File

@ -0,0 +1,47 @@
// DashboardLayout.js
import React, { useContext } from "react";
import { Layout, theme, Flex, Spin } from "antd";
import { LoadingOutlined } from '@ant-design/icons';
import DashboardNavigation from "./DashboardNavigation";
import DashboardSidebar from "./DashboardSidebar";
import DashboardBreadcrumb from "./DashboardBreadcrumb";
import { SocketContext } from "../context/SocketContext";
const { Content } = Layout;
const DashboardLayout = ({ children }) => {
const { connecting } = useContext(SocketContext);
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
return (
<Layout style={{ minHeight: "100vh" }}>
<DashboardNavigation />
<Layout>
<DashboardSidebar />
<Layout style={{ padding: "24px" }}>
<Content>
<Flex justify={"space-between"}>
<DashboardBreadcrumb style={{ margin: "16px 0" }} />
{connecting ? (
<Spin indicator={<LoadingOutlined spin />} size="middle" style={{ color: "#808080" }} />
) : (
<></>
)}
</Flex>
<div
style={{
minHeight: 280,
padding: "24px 0",
}}
>
{children}
</div>
</Content>
</Layout>
</Layout>
</Layout>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,199 @@
// DashboardMovementPanel.js
import React, { useContext, useEffect, useState } from "react";
import {
Layout,
Progress,
Typography,
Spin,
Flex,
Space,
Collapse,
InputNumber,
Button,
Row,
Col,
Divider,
ButtonGroup,
Radio,
Slider,
Dropdown,
Card,
} from "antd";
import {
LoadingOutlined,
ArrowUpOutlined,
ArrowLeftOutlined,
HomeOutlined,
ArrowRightOutlined,
ArrowDownOutlined,
LineOutlined,
} from "@ant-design/icons";
import { SocketContext } from "../context/SocketContext";
import styled from "styled-components";
const { Text, Link } = Typography;
const { Panel } = Collapse;
const { Header } = Layout;
const CustomCollapse = styled(Collapse)`
.ant-collapse-header {
padding: 0 !important;
}
.ant-collapse-content-box {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
`;
const DashboardMovementPanel = ({ remoteAddress }) => {
const [posValue, setPosValue] = useState(10);
const [rateValue, setRateValue] = useState(1000);
const { socket } = useContext(SocketContext);
useEffect(() => {
if (socket) {
}
}, [socket]);
const sendCommand = (type, data) => {
const commandData = {
remoteAddress,
type,
data,
};
console.log(commandData);
socket.emit("command", commandData);
};
const handleSetTemperatureClick = (target, value) => {
sendCommand("setTemperature", { target, value });
};
const handlePosRadioChange = (e) => {
const value = e.target.value;
setPosValue(value); // Update posValue state when radio button changes
};
const handlePosInputChange = (value) => {
setPosValue(value); // Update posValue state when input changes
};
const handleRateInputChange = (value) => {
setRateValue(value); // Update rateValue state when input changes
};
const handleHomeAxisClick = (axis) => {
sendCommand("homeAxis", { axis });
};
const handleMoveAxisClick = (axis, minus) => {
var pos;
const rate = rateValue;
if (minus) {
pos = posValue;
} else {
pos = posValue * -1
}
sendCommand("moveAxis", { axis, pos, rate });
};
const homeAxisButtonItems = [
{
key: "homeXYZ",
label: "Home XYZ",
onClick: () => handleHomeAxisClick("ALL"),
},
{
key: "homeXY",
label: "Home XY",
onClick: () => handleHomeAxisClick("X Y"),
},
{
key: "homeX",
label: "Home X",
onClick: () => handleHomeAxisClick("X"),
},
{
key: "homeY",
label: "Home Y",
onClick: () => handleHomeAxisClick("Y"),
},
{
key: "homeZ",
label: "Home Z",
onClick: () => handleHomeAxisClick("Z"),
},
];
return (
<div style={{ minWidth: 190 }}>
<Flex vertical gap={"large"}>
<Flex horizontal="true" gap="small">
<Card size="small" title="XY">
<Flex vertical align="center" justify="center" gap="small" style={{ height: "100%"}}>
<Button icon={<ArrowUpOutlined />} onClick={() => { handleMoveAxisClick("Y", false); }}/>
<Space>
<Button icon={<ArrowLeftOutlined />} onClick={() => { handleMoveAxisClick("X", false); }}/>
<Dropdown menu={{ items: homeAxisButtonItems }} placement="bottom">
<Button icon={<HomeOutlined />}></Button>
</Dropdown>
<Button icon={<ArrowRightOutlined />} onClick={() => { handleMoveAxisClick("X", true); }}/>
</Space>
<Button icon={<ArrowDownOutlined />} onClick={() => { handleMoveAxisClick("Y", true); }}></Button>
</Flex>
</Card>
<Card size="small" title="Z">
<Flex vertical align="center" justify="center" gap="small" style={{ height: "100%" }}>
<Button icon={<ArrowUpOutlined />} />
<Button icon={<LineOutlined />} />
<Button icon={<ArrowDownOutlined />}></Button>
</Flex>
</Card>
<Card size="small" title="E">
<Flex vertical align="center" justify="center" gap="small" >
<Button icon={<ArrowUpOutlined />} />
<Button icon={<ArrowDownOutlined />}></Button>
</Flex>
</Card>
</Flex>
<Flex vertical gap="small">
<Radio.Group
onChange={handlePosRadioChange}
value={posValue}
name="posRadio"
>
<Radio.Button value={0.1}>0.1</Radio.Button>
<Radio.Button value={1}>1</Radio.Button>
<Radio.Button value={10}>10</Radio.Button>
<Radio.Button value={100}>100</Radio.Button>
</Radio.Group>
<Flex horizontal="true" gap="small">
<InputNumber
min={0.1}
max={100}
value={posValue}
formatter={(value) => `${value} mm`}
parser={(value) => value?.replace(" mm", "")}
onChange={handlePosInputChange}
placeholder="10 mm"
name="posInput"
/>
<InputNumber
min={1}
max={5000}
value={rateValue}
formatter={(value) => `${value} mm/s`}
parser={(value) => value?.replace(" mm/s", "")}
onChange={handleRateInputChange}
placeholder="100 mm/s"
name="rateInput"
/>
</Flex>
</Flex>
</Flex>
</div>
);
};
export default DashboardMovementPanel;

View File

@ -0,0 +1,48 @@
// DashboardNavigation.js
import React, { useContext, useEffect, useState } from "react";
import { Layout, Menu, message } from "antd";
import { UserOutlined, LogoutOutlined } from "@ant-design/icons";
import { AuthContext } from "../../Auth/AuthContext";
import { useNavigate } from "react-router-dom";
import Title from "antd/es/skeleton/Title";
const { Header } = Layout;
const DashboardNavigation = () => {
const { logout } = useContext(AuthContext);
const navigate = useNavigate();
const handleLogout = async (e) => {
logout();
setTimeout(() => {
navigate('/login')
}, 500)
};
const menuItems = [
{
key: "1",
label: "Profile",
icon: <UserOutlined />,
},
{
key: "2",
label: "Logout",
icon: <LogoutOutlined />,
onClick: () => {handleLogout();},
},
];
return (
<Header className="header">
<Menu
theme="dark"
mode="horizontal"
items={menuItems}
style={{float: 'right'}}
/>
</Header>
);
};
export default DashboardNavigation;

View File

@ -0,0 +1,237 @@
// DashboardTemperaturePanel.js
import React, { useContext, useEffect, useState } from "react";
import {
Layout,
Progress,
Typography,
Spin,
Flex,
Space,
Collapse,
InputNumber,
Button,
} from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { SocketContext } from "../context/SocketContext";
import styled from "styled-components";
const { Text, Link } = Typography;
const { Panel } = Collapse;
const { Header } = Layout;
const CustomCollapse = styled(Collapse)`
.ant-collapse-header {
padding: 0 !important;
}
.ant-collapse-content-box {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
`;
const DashboardTemperaturePanel = ({
remoteAddress,
showControls = true,
showMoreInfo = true,
}) => {
const [loading, setLoading] = React.useState(false);
const [hotEndTemperature, setHotEndTemperature] = useState(0);
const [heatedBedTemperature, setHeatedBedTemperature] = useState(0);
const [temperatureData, setTemperatureData] = useState(null);
const { socket } = useContext(SocketContext);
useEffect(() => {
if (socket) {
socket.on("temperature", (data) => {
setTemperatureData(data.temperatures);
});
return () => {
socket.off("temperature");
};
}
}, [socket]);
const sendCommand = (type, data) => {
const commandData = {
remoteAddress,
type,
data,
};
console.log(commandData);
socket.emit("command", commandData);
};
const handleSetTemperatureClick = (target, value) => {
sendCommand("setTemperature", { target, value });
};
const moreInfoItems = [
{
key: "1",
label: "More Temperature Data",
children:
<Panel>
{temperatureData ? (
<>
{typeof temperatureData.hotendPower !== "undefined" && (
<Flex vertical gap={0}>
<Text>
Hot End Power:{" "}
{Math.round((temperatureData.hotendPower / 127) * 100)}%
</Text>
<Progress
percent={(temperatureData.hotendPower / 127) * 100}
showInfo={false}
/>
</Flex>
)}
{typeof temperatureData.bedPower !== "undefined" && (
<Flex vertical gap={0}>
<Text>
Bed Power:{" "}
{Math.round((temperatureData.bedPower / 127) * 100)}%
</Text>
<Progress
percent={(temperatureData.bedPower / 127) * 100}
showInfo={false}
/>
</Flex>
)}
{typeof temperatureData.pindaTemp !== "undefined" && (
<Flex vertical gap={0}>
<Text>Pinda Temp: {temperatureData.pindaTemp}°C</Text>
</Flex>
)}
{typeof temperatureData.ambiantActual !== "undefined" && (
<Flex vertical gap={0}>
<Text>Ambient Actual: {temperatureData.ambiantActual}°C</Text>
</Flex>
)}
</>
) : (
<Flex justify="centre">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</Flex>
)}
</Panel>
},
];
return (
<div style={{ minWidth: 190 }}>
{temperatureData ? (
<Flex vertical gap="middle">
{temperatureData.hotEnd && (
<Flex vertical gap={0}>
<Text>
Hot End: {temperatureData.hotEnd.current}°C /{" "}
{temperatureData.hotEnd.target}°C
</Text>
<Progress
percent={(temperatureData.hotEnd.target / 300) * 100}
strokeColor="#FF392F1D"
success={{
percent: (temperatureData.hotEnd.current / 300) * 100,
strokeColor: "#FF3B2F",
}}
showInfo={false}
/>
{showControls === true && (
<Space direction="horizontal" style={{ marginTop: 5 }}>
<Space.Compact block size="small">
<InputNumber
value={hotEndTemperature}
min={0}
max={300}
formatter={(value) => `${value}°C`}
parser={(value) => value.replace("°C", "")}
onChange={(value) => setHotEndTemperature(value)}
/>
<Button
type="default"
onClick={() =>
handleSetTemperatureClick("hotEnd", hotEndTemperature)
}
>
Set
</Button>
</Space.Compact>
<Button
type="default"
size="small"
onClick={() => handleSetTemperatureClick("hotEnd", 0)}
>
Off
</Button>
</Space>
)}
</Flex>
)}
{temperatureData.heatedBed && (
<Flex vertical gap={0}>
<Text>
Heated Bed: {temperatureData.heatedBed.current}°C /{" "}
{temperatureData.heatedBed.target}°C
</Text>
<Progress
percent={(temperatureData.heatedBed.target / 300) * 100}
strokeColor="#FF392F1D"
success={{
percent: (temperatureData.heatedBed.current / 300) * 100,
strokeColor: "#FF3B2F",
}}
showInfo={false}
/>
{showControls === true && (
<Space direction="horizontal" style={{ marginTop: 5 }}>
<Space.Compact block size="small">
<InputNumber
value={heatedBedTemperature}
min={0}
max={300}
formatter={(value) => `${value}°C`}
parser={(value) => value.replace("°C", "")}
onChange={(value) => setHeatedBedTemperature(value)}
/>
<Button
type="default"
onClick={() =>
handleSetTemperatureClick(
"heatedBed",
heatedBedTemperature
)
}
>
Set
</Button>
</Space.Compact>
<Button
type="default"
size="small"
onClick={() => handleSetTemperatureClick("heatedBed", 0)}
>
Off
</Button>
</Space>
)}
</Flex>
)}
{showMoreInfo === true && (
<CustomCollapse ghost size="small" items={moreInfoItems} />
)}
</Flex>
) : (
<Flex justify="centre">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</Flex>
)}
</div>
);
};
export default DashboardTemperaturePanel;

View File

@ -0,0 +1,5 @@
.ant-layout-sider {
width: 250px; /* Adjust the width as per your preference */
min-width: 250px; /* Ensure a minimum width to avoid collapsing */
max-width: 400px; /* Optionally set a maximum width */
}

View File

@ -0,0 +1,58 @@
// Sidebar.js
import React from "react";
import { Link } from "react-router-dom";
import { Layout, Menu } from "antd";
import {
DashboardOutlined,
PrinterOutlined,
PlayCircleOutlined,
FileOutlined,
} from "@ant-design/icons";
import FillamentIcon from "../../Icons/FillamentIcon"
import GCodeFileIcon from "../../Icons/GCodeFileIcon";
const { Sider } = Layout;
const Sidebar = () => {
const items = [
{
key: "overview",
label: <Link to="/dashboard/overview">Overview</Link>,
icon: <DashboardOutlined />,
},
{
key: "printers",
label: <Link to="/dashboard/printers">Printers</Link>,
icon: <PrinterOutlined />,
},
{
key: "jobs",
label: <Link to="/dashboard/printjobs">Print Jobs</Link>,
icon: <PlayCircleOutlined />,
},
{
key: "fillaments",
label: <Link to="/dashboard/fillaments">Fillaments</Link>,
icon: <FillamentIcon />,
},
{
key: "gcodefiles",
label: <Link to="/dashboard/gcodefiles">G Code Files</Link>,
icon: <GCodeFileIcon />,
},
];
return (
<Sider width={250} className="site-layout-background" collapsible>
<Menu
mode="inline"
defaultSelectedKeys={["1"]}
defaultOpenKeys={["sub1"]}
style={{ height: "100%", borderRight: 0 }}
items={items}
/>
</Sider>
);
};
export default Sidebar;

View File

@ -0,0 +1,237 @@
// DashboardTemperaturePanel.js
import React, { useContext, useEffect, useState } from "react";
import {
Layout,
Progress,
Typography,
Spin,
Flex,
Space,
Collapse,
InputNumber,
Button,
} from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import { SocketContext } from "../context/SocketContext";
import styled from "styled-components";
const { Text, Link } = Typography;
const { Panel } = Collapse;
const { Header } = Layout;
const CustomCollapse = styled(Collapse)`
.ant-collapse-header {
padding: 0 !important;
}
.ant-collapse-content-box {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
`;
const DashboardTemperaturePanel = ({
remoteAddress,
showControls = true,
showMoreInfo = true,
}) => {
const [loading, setLoading] = React.useState(false);
const [hotEndTemperature, setHotEndTemperature] = useState(0);
const [heatedBedTemperature, setHeatedBedTemperature] = useState(0);
const [temperatureData, setTemperatureData] = useState(null);
const { socket } = useContext(SocketContext);
useEffect(() => {
if (socket) {
socket.on("temperature", (data) => {
setTemperatureData(data.temperatures);
});
return () => {
socket.off("temperature");
};
}
}, [socket]);
const sendCommand = (type, data) => {
const commandData = {
remoteAddress,
type,
data,
};
console.log(commandData);
socket.emit("command", commandData);
};
const handleSetTemperatureClick = (target, value) => {
sendCommand("setTemperature", { target, value });
};
const moreInfoItems = [
{
key: "1",
label: "More Temperature Data",
children:
<Panel>
{temperatureData ? (
<>
{typeof temperatureData.hotendPower !== "undefined" && (
<Flex vertical gap={0}>
<Text>
Hot End Power:{" "}
{Math.round((temperatureData.hotendPower / 127) * 100)}%
</Text>
<Progress
percent={(temperatureData.hotendPower / 127) * 100}
showInfo={false}
/>
</Flex>
)}
{typeof temperatureData.bedPower !== "undefined" && (
<Flex vertical gap={0}>
<Text>
Bed Power:{" "}
{Math.round((temperatureData.bedPower / 127) * 100)}%
</Text>
<Progress
percent={(temperatureData.bedPower / 127) * 100}
showInfo={false}
/>
</Flex>
)}
{typeof temperatureData.pindaTemp !== "undefined" && (
<Flex vertical gap={0}>
<Text>Pinda Temp: {temperatureData.pindaTemp}°C</Text>
</Flex>
)}
{typeof temperatureData.ambiantActual !== "undefined" && (
<Flex vertical gap={0}>
<Text>Ambient Actual: {temperatureData.ambiantActual}°C</Text>
</Flex>
)}
</>
) : (
<Flex justify="centre">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</Flex>
)}
</Panel>
},
];
return (
<div style={{ minWidth: 190 }}>
{temperatureData ? (
<Flex vertical gap="middle">
{temperatureData.hotEnd && (
<Flex vertical gap={0}>
<Text>
Hot End: {temperatureData.hotEnd.current}°C /{" "}
{temperatureData.hotEnd.target}°C
</Text>
<Progress
percent={(temperatureData.hotEnd.target / 300) * 100}
strokeColor="#FF392F1D"
success={{
percent: (temperatureData.hotEnd.current / 300) * 100,
strokeColor: "#FF3B2F",
}}
showInfo={false}
/>
{showControls === true && (
<Space direction="horizontal" style={{ marginTop: 5 }}>
<Space.Compact block size="small">
<InputNumber
value={hotEndTemperature}
min={0}
max={300}
formatter={(value) => `${value}°C`}
parser={(value) => value.replace("°C", "")}
onChange={(value) => setHotEndTemperature(value)}
/>
<Button
type="default"
onClick={() =>
handleSetTemperatureClick("hotEnd", hotEndTemperature)
}
>
Set
</Button>
</Space.Compact>
<Button
type="default"
size="small"
onClick={() => handleSetTemperatureClick("hotEnd", 0)}
>
Off
</Button>
</Space>
)}
</Flex>
)}
{temperatureData.heatedBed && (
<Flex vertical gap={0}>
<Text>
Heated Bed: {temperatureData.heatedBed.current}°C /{" "}
{temperatureData.heatedBed.target}°C
</Text>
<Progress
percent={(temperatureData.heatedBed.target / 300) * 100}
strokeColor="#FF392F1D"
success={{
percent: (temperatureData.heatedBed.current / 300) * 100,
strokeColor: "#FF3B2F",
}}
showInfo={false}
/>
{showControls === true && (
<Space direction="horizontal" style={{ marginTop: 5 }}>
<Space.Compact block size="small">
<InputNumber
value={heatedBedTemperature}
min={0}
max={300}
formatter={(value) => `${value}°C`}
parser={(value) => value.replace("°C", "")}
onChange={(value) => setHeatedBedTemperature(value)}
/>
<Button
type="default"
onClick={() =>
handleSetTemperatureClick(
"heatedBed",
heatedBedTemperature
)
}
>
Set
</Button>
</Space.Compact>
<Button
type="default"
size="small"
onClick={() => handleSetTemperatureClick("heatedBed", 0)}
>
Off
</Button>
</Space>
)}
</Flex>
)}
{showMoreInfo === true && (
<CustomCollapse ghost size="small" items={moreInfoItems} />
)}
</Flex>
) : (
<Flex justify="centre">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</Flex>
)}
</div>
);
};
export default DashboardTemperaturePanel;

View File

@ -0,0 +1,150 @@
// FillamentSelect.js
import { TreeSelect, Badge } from 'antd';
import React, { useEffect, useState, useContext, useRef } from 'react';
import axios from 'axios';
import { AuthContext } from "../../Auth/AuthContext";
const propertyOrder = ['diameter', 'type', 'brand'];
const FillamentSelect = ({ onChange }) => {
const [fillamentsData, setFillamentsData] = useState([]);
const [fillamentsTreeData, setFillamentsTreeData] = useState([]);
const { token, logout } = useContext(AuthContext);
const tokenRef = useRef(token);
const [loading, setLoading] = useState(true);
const fetchFillamentsData = async (property, filter) => {
console.log("Current ref", tokenRef);
try {
const response = await axios.get("http://localhost:8080/fillaments", {
params: {
...filter,
property,
},
headers: {
Authorization: `Bearer ${tokenRef.current}`,
},
});
setLoading(false);
return response.data;
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
} catch (err) {
console.error(err);
}
};
const getFilter = (node) => {
var filter = {};
var currentId = node.id;
while (currentId != 0) {
const currentNode = fillamentsTreeData.filter(treeData => treeData['id'] == currentId)[0]
filter[propertyOrder[currentNode.propertyId]] = currentNode.value.split("-")[0];
currentId = currentNode.pId;
}
return filter;
}
const generateFillamentTreeNodes = async (node = null) => {
if (!node) {
return;
}
const fillamentData = await fetchFillamentsData(null, getFilter(node));
let newNodeList = [];
for (var i = 0; i < fillamentData.length; i++) {
const fillament = fillamentData[i];
const random = Math.random().toString(36).substring(2, 6);
const newNode = {
id: random,
pId: node.id,
value: fillament._id,
key: fillament._id,
title: (<Badge color={fillament.color} text={fillament.name}/>),
isLeaf: true
}
newNodeList.push(newNode);
}
setFillamentsTreeData(fillamentsTreeData.concat(newNodeList))
console.log(newNodeList);
};
const generateFillamentCategoryTreeNodes = async (node = null) => {
var filter = {};
console.log("Init node: ", node);
var propertyId = 0;
if (!node) {
node = {};
node.id = 0;
} else {
filter = getFilter(node);
propertyId = node.propertyId + 1;
}
const propertyName = propertyOrder[propertyId];
console.log("Next Property Id", propertyId)
console.log("Filter", filter);
const propertyData = await fetchFillamentsData(propertyName, filter)
let newNodeList = [];
for (var i = 0; i < propertyData.length; i++) {
const property = propertyData[i][propertyName];
const random = Math.random().toString(36).substring(2, 6);
const newNode = {
id: random,
pId: node.id,
value: property + "-" + random,
key: property + "-" + random,
propertyId: propertyId,
title: property,
isLeaf: false,
selectable: false
}
newNodeList.push(newNode);
}
setFillamentsTreeData(fillamentsTreeData.concat(newNodeList))
console.log(newNodeList);
};
const handleFillamentsTreeLoad = async (node) => {
console.log(node);
if (node) {
if (node.propertyId != propertyOrder.length - 1) {
generateFillamentCategoryTreeNodes(node);
} else {
console.log("Generating printer node...");
generateFillamentTreeNodes(node); // End of properties
}
} else {
generateFillamentCategoryTreeNodes(null); // First property
}
};
useEffect(() => {
if (fillamentsTreeData.length == 0) {
handleFillamentsTreeLoad(null)
}
}, [token]);
return (
<TreeSelect treeDataSimpleMode loadData={handleFillamentsTreeLoad} treeData={fillamentsTreeData} onChange={onChange}>
</TreeSelect>
);
};
export default FillamentSelect;

View File

@ -0,0 +1,67 @@
import * as GCodePreview from 'gcode-preview';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import * as THREE from 'three';
function GCodePreviewUI(props, ref) {
const {
topLayerColor = '',
lastSegmentColor = '',
startLayer,
endLayer,
lineWidth
} = props;
const canvasRef = useRef(null);
const [preview, setPreview] = useState();
const resizePreview = () => {
preview?.resize();
};
useImperativeHandle(ref, () => ({
getLayerCount() {
return preview?.layers.length;
},
processGCode(gcode) {
preview?.processGCode(gcode);
}
}));
useEffect(() => {
setPreview(
GCodePreview.init({
canvas: canvasRef.current,
startLayer,
endLayer,
lineWidth,
topLayerColor: new THREE.Color(topLayerColor).getHex(),
lastSegmentColor: new THREE.Color(lastSegmentColor).getHex(),
buildVolume: { x: 250, y: 220, z: 150 },
initialCameraPosition: [0, 400, 450],
allowDragNDrop: false
})
);
window.addEventListener('resize', resizePreview);
return () => {
window.removeEventListener('resize', resizePreview);
};
}, []);
return (
<div className="gcode-preview">
<canvas ref={canvasRef}></canvas>
<div>
<div>topLayerColor: {topLayerColor}</div>
<div>lastSegmentColor: {lastSegmentColor}</div>
<div>startLayer: {startLayer}</div>
<div>endLayer: {endLayer}</div>
<div>lineWidth: {lineWidth}</div>
</div>
</div>
);
}
export default forwardRef(GCodePreviewUI);

View File

@ -0,0 +1,75 @@
// src/contexts/SocketContext.js
import React, { createContext, useEffect, useState, useContext, useRef } from "react";
import io from "socket.io-client";
import { message } from "antd";
import { AuthContext } from "../../Auth/AuthContext";
const SocketContext = createContext();
const SocketProvider = ({ children }) => {
const { token } = useContext(AuthContext);
const socketRef = useRef(null);
const [connecting, setConnecting] = useState(false);
const [error, setError] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
useEffect(() => {
if (token) {
console.log("Token is available, connecting to web socket server...");
const newSocket = io("http://localhost:5050", {
reconnectionAttempts: 3,
timeout: 3000,
query: { token },
});
setConnecting(true);
newSocket.on("connect", () => {
console.log("Socket connected");
setConnecting(false);
setError(null);
});
newSocket.on("disconnect", () => {
console.log("Socket disconnected");
setError("Socket disconnected");
});
newSocket.on("connect_error", (err) => {
console.error("Socket connection error:", err);
messageApi.error("Socket connection error: " + err.message);
setError("Socket connection error");
});
newSocket.on("error", (err) => {
console.error("Socket error:", err);
setError("Socket error");
});
socketRef.current = newSocket;
// Clean up function
return () => {
if (socketRef.current) {
console.log("Cleaning up socket connection...");
socketRef.current.disconnect();
socketRef.current = null;
}
};
} else if (!token && socketRef.current) {
console.log("Token not available, disconnecting socket...");
socketRef.current.disconnect();
socketRef.current = null;
}
}, [token, messageApi]);
return (
<SocketContext.Provider value={{ socket: socketRef.current, error, connecting }}>
{contextHolder}
{children}
</SocketContext.Provider>
);
};
export { SocketContext, SocketProvider };

View File

@ -0,0 +1,9 @@
import React from 'react';
import Icon from '@ant-design/icons';
import { ReactComponent as CustomIconSvg } from '../../assets/icons/fillamenticon.svg';
const FillamentIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
);
export default FillamentIcon;

View File

@ -0,0 +1,9 @@
import React from 'react';
import Icon from '@ant-design/icons';
import { ReactComponent as CustomIconSvg } from '../../assets/icons/gcodefileicon.svg';
const GCodeFileIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
);
export default GCodeFileIcon;

View File

@ -0,0 +1,9 @@
import React from 'react';
import Icon from '@ant-design/icons';
import { ReactComponent as CustomIconSvg } from '../../assets/icons/passkeysicon.svg';
const PassKeysIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
);
export default PassKeysIcon;

View File

@ -0,0 +1,8 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
const PrivateRoute = ({ component: Component }) => {
return localStorage.getItem('access_token') ? <Component /> : <Navigate to="/login" />;
};
export default PrivateRoute;

View File

@ -0,0 +1,8 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
const PublicRoute = ({ component: Component }) => {
return !localStorage.getItem('access_token') ? <Component /> : <Navigate to="/dashboard/overview" />;
};
export default PublicRoute;

13
src/index.css Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
src/index.js Normal file
View File

@ -0,0 +1,17 @@
import reportWebVitals from "./reportWebVitals";
import React from "react";
import ReactDOM from "react-dom/client";
import FarmControlApp from "./App";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// <React.StrictMode>
<FarmControlApp />
// </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

13
src/reportWebVitals.js Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';