Added more functionality
This commit is contained in:
parent
bde5b58a70
commit
f3129f7fa3
1208
package-lock.json
generated
1208
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -7,13 +7,18 @@
|
||||
"@tsparticles/react": "^3.0.0",
|
||||
"@tsparticles/slim": "^3.5.0",
|
||||
"antd": "^5.19.2",
|
||||
"antd-style": "^3.7.1",
|
||||
"axios": "*",
|
||||
"dotenv": "^16.5.0",
|
||||
"gcode-preview": "^2.17.0",
|
||||
"keycloak-js": "^26.1.5",
|
||||
"moment": "*",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-router-dom": "*",
|
||||
"react-scripts": "*",
|
||||
"react-stl-viewer": "^2.5.0",
|
||||
"socket.io-client": "*",
|
||||
"styled-components": "*",
|
||||
"three": "^0.166.1",
|
||||
@ -21,7 +26,7 @@
|
||||
"web-vitals": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"dev": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
@ -45,6 +50,13 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"standard": "^17.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 13 KiB |
156
src/App.jsx
156
src/App.jsx
@ -1,89 +1,131 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import React 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";
|
||||
Navigate
|
||||
} from 'react-router-dom'
|
||||
import { App, ConfigProvider, theme } from 'antd'
|
||||
import AuthLayout from './components/Auth/AuthLayout.jsx'
|
||||
import ProductionOverview from './components/Dashboard/Production/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 Printers from './components/Dashboard/Production/Printers'
|
||||
import ControlPrinter from './components/Dashboard/Production/Printers/ControlPrinter.jsx'
|
||||
import PrinterInfo from './components/Dashboard/Production/Printers/PrinterInfo.jsx'
|
||||
|
||||
import PrintJobs from "./components/Dashboard/PrintJobs/PrintJobs.jsx";
|
||||
import PrintJobs from './components/Dashboard/Production/PrintJobs.jsx'
|
||||
import PrintJobInfo from './components/Dashboard/Production/PrintJobs/PrintJobInfo.jsx'
|
||||
|
||||
import Fillaments from "./components/Dashboard/Fillaments/Fillaments.jsx";
|
||||
import Spools from './components/Dashboard/Inventory/Spools'
|
||||
|
||||
import GCodeFiles from "./components/Dashboard/GCodeFiles/GCodeFiles.jsx";
|
||||
import Filaments from './components/Dashboard/Management/Filaments'
|
||||
import FilamentInfo from './components/Dashboard/Management/Filaments/FilamentInfo.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";
|
||||
import GCodeFiles from './components/Dashboard/Production/GCodeFiles'
|
||||
import GCodeFileInfo from './components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
|
||||
|
||||
import Parts from './components/Dashboard/Management/Parts.jsx'
|
||||
import PartInfo from './components/Dashboard/Management/Parts/PartInfo.jsx'
|
||||
|
||||
import Products from './components/Dashboard/Management/Products.jsx'
|
||||
import ProductInfo from './components/Dashboard/Management/Products/ProductInfo.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'
|
||||
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js'
|
||||
import Vendors from './components/Dashboard/Management/Vendors'
|
||||
import VendorInfo from './components/Dashboard/Management/Vendors/VendorInfo'
|
||||
|
||||
const FarmControlApp = () => {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
// 1. Use dark algorithm
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorBgBase: '#191919',
|
||||
colorPrimary: '#0A84FF',
|
||||
colorPrimary: '#007AFF',
|
||||
colorSuccess: '#32D74B',
|
||||
colorWarning: '#FF9F0A',
|
||||
colorInfo: '#0A84FF',
|
||||
colorInfo: '#007AFF',
|
||||
colorLink: '#5AC8F5',
|
||||
borderRadius: '10px',
|
||||
borderRadius: '10px'
|
||||
},
|
||||
// 2. Combine dark algorithm and compact algorithm
|
||||
// algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: '#141414'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<App>
|
||||
<AuthProvider>
|
||||
<SocketProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path="login"
|
||||
element={<PublicRoute component={() => <LoginUser />} />}
|
||||
/>
|
||||
<SpotlightProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<PrivateRoute
|
||||
component={() => (
|
||||
<Navigate to='/production/overview' replace />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='login'
|
||||
element={<PublicRoute component={() => <AuthLayout />} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="login/register-passkey"
|
||||
element={
|
||||
<PrivateRoute component={() => <RegisterPasskey />} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/production'
|
||||
element={<PrivateRoute component={() => <Dashboard />} />}
|
||||
>
|
||||
<Route path='overview' element={<ProductionOverview />} />
|
||||
<Route path='printers' element={<Printers />} />
|
||||
<Route
|
||||
path='printers/control'
|
||||
element={<ControlPrinter />}
|
||||
/>
|
||||
<Route path='printers/info' element={<PrinterInfo />} />
|
||||
<Route path='printjobs' element={<PrintJobs />} />
|
||||
<Route path='printjobs/info' element={<PrintJobInfo />} />
|
||||
<Route path='gcodefiles' element={<GCodeFiles />} />
|
||||
<Route path='gcodefiles/info' element={<GCodeFileInfo />} />
|
||||
</Route>
|
||||
|
||||
<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>
|
||||
<Route
|
||||
path='/inventory'
|
||||
element={<PrivateRoute component={() => <Dashboard />} />}
|
||||
>
|
||||
<Route path='spools' element={<Spools />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path='/management'
|
||||
element={<PrivateRoute component={() => <Dashboard />} />}
|
||||
>
|
||||
<Route path='filaments' element={<Filaments />} />
|
||||
<Route path='filaments/info' element={<FilamentInfo />} />
|
||||
<Route path='parts' element={<Parts />} />
|
||||
<Route path='parts/info' element={<PartInfo />} />
|
||||
<Route path='products' element={<Products />} />
|
||||
<Route path='products/info' element={<ProductInfo />} />
|
||||
<Route path='vendors' element={<Vendors />} />
|
||||
<Route path='vendors/info' element={<VendorInfo />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
</SpotlightProvider>
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
</App>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default FarmControlApp;
|
||||
export default FarmControlApp
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
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.
@ -1,8 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@ -1,223 +1,266 @@
|
||||
// src/contexts/AuthContext.js
|
||||
import React, { createContext, useState, useEffect, useCallback } from "react";
|
||||
import axios from "axios";
|
||||
import { message } from "antd";
|
||||
import React, { createContext, useState, useCallback, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { message, Modal, notification, Progress, Button, Space } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
} from "@simplewebauthn/browser";
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
const AuthContext = createContext();
|
||||
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 [messageApi, contextHolder] = message.useMessage()
|
||||
const [notificationApi, notificationContextHolder] =
|
||||
notification.useNotification()
|
||||
const [authenticated, setAuthenticated] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [token, setToken] = useState(null)
|
||||
const [expiresAt, setExpiresAt] = useState(null)
|
||||
const [userProfile, setUserProfile] = useState(null)
|
||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
|
||||
const [showUnauthorizedModal, setShowUnauthorizedModal] = 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 logout = useCallback((redirectUri = '/login') => {
|
||||
setAuthenticated(false)
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
window.location.href = `http://localhost:8080/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
}, [])
|
||||
|
||||
const getAuthMode = useCallback(async (email) => {
|
||||
if (!email) {
|
||||
return { successful: false };
|
||||
}
|
||||
setLoading(true);
|
||||
// Login using query parameters
|
||||
const loginWithSSO = useCallback(
|
||||
(redirectUri = window.location.pathname + window.location.search) => {
|
||||
messageApi.info('Logging in with tombutcher.work')
|
||||
window.location.href = `http://localhost:8080/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
},
|
||||
[messageApi]
|
||||
)
|
||||
// Function to check if the user is logged in
|
||||
const checkAuthStatus = useCallback(async () => {
|
||||
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
|
||||
);
|
||||
// Make a call to your backend to check auth status
|
||||
const response = await axios.get('http://localhost:8080/auth/user', {
|
||||
withCredentials: true // Important for including cookies
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
console.log('User is authenticated!')
|
||||
setAuthenticated(true)
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
} 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 +
|
||||
")"
|
||||
);
|
||||
}
|
||||
setAuthenticated(false)
|
||||
}
|
||||
}
|
||||
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);
|
||||
console.log('Auth check failed', error)
|
||||
if (error.response?.status === 401) {
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
setAuthenticated(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshToken = useCallback(async () => {
|
||||
try {
|
||||
const response = await axios.get('http://localhost:8080/auth/refresh', {
|
||||
withCredentials: true
|
||||
})
|
||||
if (response.status === 200 && response.data) {
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const showTokenExpirationMessage = useCallback(
|
||||
(expiresAt) => {
|
||||
const now = new Date()
|
||||
const expirationDate = new Date(expiresAt)
|
||||
const timeRemaining = expirationDate - now
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
if (authenticated) {
|
||||
setShowSessionExpiredModal(true)
|
||||
setAuthenticated(false)
|
||||
notificationApi.destroy('token-expiration')
|
||||
}
|
||||
} else {
|
||||
if (error.response.status === 400) {
|
||||
messageApi.error(error.response.data.error);
|
||||
} else {
|
||||
messageApi.error(
|
||||
"An unexpected error occoured. (" + error.response.status + ")"
|
||||
);
|
||||
const minutes = Math.floor(timeRemaining / 60000)
|
||||
const seconds = Math.floor((timeRemaining % 60000) / 1000)
|
||||
|
||||
// Only show notification in the final minute
|
||||
if (minutes === 0) {
|
||||
const totalSeconds = 60
|
||||
const remainingSeconds = totalSeconds - seconds
|
||||
const progress = (remainingSeconds / totalSeconds) * 100
|
||||
|
||||
notificationApi.info({
|
||||
message: 'Session Expiring Soon',
|
||||
description: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
Your session will expire in {seconds} seconds
|
||||
</div>
|
||||
<Progress
|
||||
percent={progress}
|
||||
size='small'
|
||||
status='active'
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
duration: 0,
|
||||
key: 'token-expiration',
|
||||
icon: null,
|
||||
placement: 'bottomRight',
|
||||
style: {
|
||||
width: 360
|
||||
},
|
||||
className: 'token-expiration-notification',
|
||||
closeIcon: null,
|
||||
onClose: () => {},
|
||||
btn: (
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
notificationApi.destroy('token-expiration')
|
||||
refreshToken()
|
||||
}}
|
||||
>
|
||||
Extend Session
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
} else if (minutes === 1) {
|
||||
// Clear any existing notification when we enter the final minute
|
||||
notificationApi.destroy('token-expiration')
|
||||
}
|
||||
}
|
||||
},
|
||||
[authenticated, notificationApi]
|
||||
)
|
||||
|
||||
const handleSessionExpiredModalOk = () => {
|
||||
setShowSessionExpiredModal(false)
|
||||
loginWithSSO()
|
||||
}
|
||||
|
||||
// Initialize on component mount
|
||||
useEffect(() => {
|
||||
let intervalId
|
||||
|
||||
const tokenRefreshInterval = () => {
|
||||
if (expiresAt) {
|
||||
showTokenExpirationMessage(expiresAt)
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
return { successful: false };
|
||||
}, [messageApi]);
|
||||
|
||||
const loginWithPasskey = useCallback(async (email) => {
|
||||
if (!email) {
|
||||
return { successful: false };
|
||||
if (authenticated) {
|
||||
intervalId = setInterval(tokenRefreshInterval, 1000)
|
||||
}
|
||||
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);
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}, [expiresAt, authenticated, showTokenExpirationMessage])
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Token changed!" + token)
|
||||
validateToken(token);
|
||||
}, [token]); // if token changes, validate it.
|
||||
checkAuthStatus()
|
||||
}, [checkAuthStatus])
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
{notificationContextHolder}
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
authenticated,
|
||||
loginWithSSO,
|
||||
token,
|
||||
loginWithPassword,
|
||||
loginWithPasskey,
|
||||
logout,
|
||||
loading,
|
||||
registerPasskey,
|
||||
getAuthMode,
|
||||
userProfile,
|
||||
logout
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
<Modal
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
<InfoCircleOutlined />
|
||||
Session Expired
|
||||
</Space>
|
||||
}
|
||||
open={showSessionExpiredModal}
|
||||
onOk={handleSessionExpiredModalOk}
|
||||
okText='Log In'
|
||||
style={{ maxWidth: 430 }}
|
||||
closable={false}
|
||||
centered
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button
|
||||
key='submit'
|
||||
type='primary'
|
||||
onClick={handleSessionExpiredModalOk}
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
Your session has expired. Please log in again to continue.
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
<ExclamationCircleOutlined />
|
||||
Please log in to continue
|
||||
</Space>
|
||||
}
|
||||
open={showUnauthorizedModal}
|
||||
onOk={() => {
|
||||
setShowUnauthorizedModal(false)
|
||||
loginWithSSO()
|
||||
}}
|
||||
okText='Log In'
|
||||
style={{ maxWidth: 430 }}
|
||||
closable={false}
|
||||
centered
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button
|
||||
key='submit'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
setShowUnauthorizedModal(false)
|
||||
loginWithSSO()
|
||||
}}
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
You need to be logged in to access FarmControl. Please log in with
|
||||
tombutcher.work to continue.
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export { AuthContext, AuthProvider };
|
||||
AuthProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { AuthContext, AuthProvider }
|
||||
|
||||
@ -1,29 +1,38 @@
|
||||
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";
|
||||
import PropTypes from 'prop-types'
|
||||
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);
|
||||
const { loading } = useContext(AuthContext)
|
||||
return (
|
||||
<>
|
||||
<AuthParticles />
|
||||
<Flex
|
||||
horizontal="true"
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ paddingTop: "35px" }}
|
||||
horizontal='true'
|
||||
align='center'
|
||||
justify='center'
|
||||
style={{ paddingTop: '35px' }}
|
||||
>
|
||||
<Card style={{ maxWidth: 350 }}>
|
||||
<Spin spinning={loading} indicator={<LoadingOutlined spin />} size="large">
|
||||
<Spin
|
||||
spinning={loading}
|
||||
indicator={<LoadingOutlined spin />}
|
||||
size='large'
|
||||
>
|
||||
{children}
|
||||
</Spin>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthLayout;
|
||||
AuthLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export default AuthLayout
|
||||
|
||||
@ -1,105 +1,108 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||
import { loadSlim } from "@tsparticles/slim";
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import Particles, { initParticlesEngine } from '@tsparticles/react'
|
||||
import { loadSlim } from '@tsparticles/slim'
|
||||
|
||||
import "./Auth.css";
|
||||
import './Auth.css'
|
||||
|
||||
const ParticlesComponent = React.memo(({ options, particlesLoaded }) => {
|
||||
return (
|
||||
<Particles
|
||||
id="tsparticles"
|
||||
particlesLoaded={particlesLoaded}
|
||||
id='tsparticles'
|
||||
options={options}
|
||||
particlesLoaded={particlesLoaded}
|
||||
/>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
ParticlesComponent.displayName = 'ParticlesComponent'
|
||||
|
||||
const AuthParticles = () => {
|
||||
const [init, setInit] = useState(false);
|
||||
const [init, setInit] = useState(false)
|
||||
|
||||
// this should be run only once per application lifetime
|
||||
useEffect(() => {
|
||||
initParticlesEngine(async (engine) => {
|
||||
await loadSlim(engine);
|
||||
await loadSlim(engine)
|
||||
}).then(() => {
|
||||
setInit(true);
|
||||
});
|
||||
}, []);
|
||||
setInit(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const particlesLoaded = useCallback((container) => {
|
||||
console.log(container);
|
||||
}, []);
|
||||
const particlesLoaded = useCallback(() => {
|
||||
console.log('Particles Loaded!')
|
||||
}, [])
|
||||
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
background: {
|
||||
color: {
|
||||
value: "#141414",
|
||||
},
|
||||
value: '#141414'
|
||||
}
|
||||
},
|
||||
fpsLimit: 120,
|
||||
interactivity: {
|
||||
events: {
|
||||
onClick: {
|
||||
enable: true,
|
||||
mode: "push",
|
||||
mode: 'push'
|
||||
},
|
||||
onHover: {
|
||||
enable: true,
|
||||
mode: "repulse",
|
||||
},
|
||||
mode: 'repulse'
|
||||
}
|
||||
},
|
||||
modes: {
|
||||
push: {
|
||||
quantity: 4,
|
||||
quantity: 4
|
||||
},
|
||||
repulse: {
|
||||
distance: 200,
|
||||
duration: 0.4,
|
||||
},
|
||||
},
|
||||
duration: 0.4
|
||||
}
|
||||
}
|
||||
},
|
||||
particles: {
|
||||
color: {
|
||||
value: "#ffffff",
|
||||
value: '#ffffff'
|
||||
},
|
||||
links: {
|
||||
color: "#ffffff",
|
||||
color: '#ffffff',
|
||||
distance: 150,
|
||||
enable: true,
|
||||
opacity: 0.5,
|
||||
width: 1,
|
||||
width: 1
|
||||
},
|
||||
move: {
|
||||
direction: "none",
|
||||
direction: 'none',
|
||||
enable: true,
|
||||
outModes: {
|
||||
default: "bounce",
|
||||
default: 'bounce'
|
||||
},
|
||||
random: false,
|
||||
speed: 1,
|
||||
straight: false,
|
||||
straight: false
|
||||
},
|
||||
number: {
|
||||
density: {
|
||||
enable: true,
|
||||
enable: true
|
||||
},
|
||||
value: 160,
|
||||
value: 160
|
||||
},
|
||||
opacity: {
|
||||
value: 0.5,
|
||||
value: 0.5
|
||||
},
|
||||
shape: {
|
||||
type: "circle",
|
||||
type: 'circle'
|
||||
},
|
||||
size: {
|
||||
value: { min: 1, max: 5 },
|
||||
},
|
||||
value: { min: 1, max: 5 }
|
||||
}
|
||||
},
|
||||
detectRetina: true,
|
||||
detectRetina: true
|
||||
}),
|
||||
[]
|
||||
);
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{init && (
|
||||
@ -109,7 +112,12 @@ const AuthParticles = () => {
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthParticles;
|
||||
ParticlesComponent.propTypes = {
|
||||
options: PropTypes.object.isRequired,
|
||||
particlesLoaded: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default AuthParticles
|
||||
|
||||
@ -1,178 +1,52 @@
|
||||
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 React, { useContext } from 'react'
|
||||
//import { useNavigate } from 'react-router-dom'
|
||||
import { Form, Button, Divider, Typography, Flex } from 'antd'
|
||||
import { UserAddOutlined } from '@ant-design/icons'
|
||||
import { AuthContext } from './AuthContext'
|
||||
import AuthLayout from './AuthLayout'
|
||||
|
||||
import PassKeysIcon from "../Icons/PassKeysIcon"; // Adjust the path if necessary
|
||||
import './Auth.css'
|
||||
|
||||
import "./Auth.css";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
//const [error] = useState('')
|
||||
//const navigate = useNavigate()
|
||||
const { loginWithSSO } = useContext(AuthContext)
|
||||
const handleLogin = async () => {
|
||||
loginWithSSO('/production/overview')
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Flex vertical="true" align="center" style={{ marginBottom: 25 }}>
|
||||
<Flex vertical='true' align='center' style={{ marginBottom: 25 }}>
|
||||
<img
|
||||
src="/logo512@2x.png"
|
||||
style={{ width: "100px" }}
|
||||
alt="Farm Control Logo"
|
||||
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>
|
||||
<Text style={{ textAlign: 'center' }}>Please sign in 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 name='loginForm' className='login-form' onFinish={handleLogin}>
|
||||
<Form.Item>
|
||||
<Button
|
||||
className='auth-form-button'
|
||||
type='primary'
|
||||
style={{ width: '250px' }}
|
||||
htmlType='submit'
|
||||
>
|
||||
Login with auth.tombutcher.work
|
||||
</Button>
|
||||
</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 />}>
|
||||
<Button className='auth-form-button' icon={<UserAddOutlined />}>
|
||||
Register
|
||||
</Button>
|
||||
</AuthLayout>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginUser;
|
||||
export default LoginUser
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
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;
|
||||
@ -1,205 +0,0 @@
|
||||
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;
|
||||
@ -1,170 +0,0 @@
|
||||
// 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;
|
||||
@ -1,331 +0,0 @@
|
||||
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;
|
||||
@ -1,205 +0,0 @@
|
||||
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;
|
||||
@ -1,170 +0,0 @@
|
||||
// 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;
|
||||
@ -1,295 +0,0 @@
|
||||
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;
|
||||
@ -1,44 +0,0 @@
|
||||
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;
|
||||
@ -1,56 +0,0 @@
|
||||
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;
|
||||
@ -1,285 +0,0 @@
|
||||
// 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;
|
||||
@ -1,232 +0,0 @@
|
||||
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;
|
||||
@ -1,129 +0,0 @@
|
||||
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;
|
||||
@ -1,270 +0,0 @@
|
||||
// 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;
|
||||
@ -1,108 +0,0 @@
|
||||
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;
|
||||
@ -1,14 +1,14 @@
|
||||
// Dashboard.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import { useNavigate, Outlet } from 'react-router-dom';
|
||||
import React from 'react'
|
||||
import DashboardLayout from './DashboardLayout'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Outlet />
|
||||
<Outlet />
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard
|
||||
|
||||
@ -1,37 +1,68 @@
|
||||
// DashboardBreadcrumb.js
|
||||
import React from 'react';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import React from 'react'
|
||||
import { Breadcrumb, Button, Flex, Space } from 'antd'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons'
|
||||
|
||||
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',
|
||||
};
|
||||
'/production': 'Production',
|
||||
'/management': 'Management',
|
||||
'/production/overview': 'Overview',
|
||||
'/production/printers': 'Printers',
|
||||
'/production/printers/control': 'Control',
|
||||
'/production/printers/info': 'Info',
|
||||
'/production/printjobs': 'Print Jobs',
|
||||
'/production/printjobs/info': 'Info',
|
||||
'/production/gcodefiles': 'G Code Files',
|
||||
'/production/gcodefiles/info': 'Info',
|
||||
'/management/filaments': 'Filaments',
|
||||
'/management/filaments/info': 'Info',
|
||||
'/management/parts': 'Parts',
|
||||
'/management/parts/info': 'Info',
|
||||
'/management/products': 'Products',
|
||||
'/management/products/info': 'Info',
|
||||
'/management/vendors': 'Vendors',
|
||||
'/management/vendors/info': 'Info'
|
||||
}
|
||||
|
||||
const DashboardBreadcrumb = () => {
|
||||
const location = useLocation();
|
||||
const pathSnippets = location.pathname.split('/').filter(i => i);
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
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>
|
||||
);
|
||||
});
|
||||
const breadcrumbItems = pathSnippets.map((_, index) => {
|
||||
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
|
||||
return {
|
||||
title: (
|
||||
<Link to={url} style={{ padding: '0 12px' }}>
|
||||
{breadcrumbNameMap[url]}
|
||||
</Link>
|
||||
),
|
||||
key: url
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
{breadcrumbItems}
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Flex align='center' gap={'large'}>
|
||||
<Flex gap={'small'}>
|
||||
<Space.Compact>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<ArrowLeftOutlined style={{ fontSize: '14px' }} />}
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ padding: '0 2px', height: '22px' }}
|
||||
/>
|
||||
<Button
|
||||
type='text'
|
||||
icon={<ArrowRightOutlined style={{ fontSize: '14px' }} />}
|
||||
onClick={() => navigate(1)}
|
||||
style={{ padding: '0 2px', height: '22px' }}
|
||||
/>
|
||||
</Space.Compact>
|
||||
</Flex>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardBreadcrumb;
|
||||
export default DashboardBreadcrumb
|
||||
|
||||
@ -1,47 +1,65 @@
|
||||
// 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";
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useContext } from 'react'
|
||||
import { Layout, Flex, Spin } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import DashboardNavigation from './DashboardNavigation'
|
||||
import ProductionSidebar from './ProductionSidebar'
|
||||
import InventorySidebar from './InventorySidebar'
|
||||
import ManagementSidebar from './ManagementSidebar'
|
||||
import DashboardBreadcrumb from './DashboardBreadcrumb'
|
||||
import { SocketContext } from '../context/SocketContext'
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Content } = Layout
|
||||
|
||||
const DashboardLayout = ({ children }) => {
|
||||
const { connecting } = useContext(SocketContext);
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
const { connecting } = useContext(SocketContext)
|
||||
const location = useLocation()
|
||||
const isProduction = location.pathname.startsWith('/production')
|
||||
const isInventory = location.pathname.startsWith('/inventory')
|
||||
const isManagement = location.pathname.startsWith('/management')
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<DashboardNavigation />
|
||||
<Layout>
|
||||
<DashboardSidebar />
|
||||
<Layout style={{ padding: "24px" }}>
|
||||
{isProduction ? (
|
||||
<ProductionSidebar />
|
||||
) : isInventory ? (
|
||||
<InventorySidebar />
|
||||
) : isManagement ? (
|
||||
<ManagementSidebar />
|
||||
) : (
|
||||
<ProductionSidebar /> // Default to production sidebar
|
||||
)}
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
<Flex vertical style={{ height: '100%' }} gap='20px'>
|
||||
<Flex justify='space-between'>
|
||||
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
|
||||
{connecting ? (
|
||||
<Spin
|
||||
indicator={<LoadingOutlined spin />}
|
||||
size='middle'
|
||||
style={{ color: '#808080' }}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</Flex>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardLayout;
|
||||
DashboardLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export default DashboardLayout
|
||||
|
||||
@ -1,199 +0,0 @@
|
||||
// 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;
|
||||
@ -1,48 +1,193 @@
|
||||
// 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";
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
Menu,
|
||||
Flex,
|
||||
Tag,
|
||||
Space,
|
||||
Dropdown,
|
||||
Button,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
PrinterOutlined,
|
||||
SettingOutlined,
|
||||
ProductOutlined,
|
||||
ShoppingCartOutlined,
|
||||
PoundOutlined,
|
||||
MailOutlined,
|
||||
SearchOutlined,
|
||||
BellOutlined,
|
||||
DisconnectOutlined,
|
||||
MenuOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { AuthContext } from '../../Auth/AuthContext'
|
||||
import { SocketContext } from '../context/SocketContext'
|
||||
import { SpotlightContext } from '../context/SpotlightContext'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Header } from 'antd/es/layout/layout'
|
||||
|
||||
const { Header } = Layout;
|
||||
const { Text } = Typography
|
||||
|
||||
const DashboardNavigation = () => {
|
||||
const { logout } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const { logout, userProfile } = useContext(AuthContext)
|
||||
const { showSpotlight } = useContext(SpotlightContext)
|
||||
const { socket } = useContext(SocketContext)
|
||||
const [socketConnected, setSocketConnected] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [selectedKey, setSelectedKey] = useState('production')
|
||||
|
||||
const handleLogout = async (e) => {
|
||||
logout();
|
||||
setTimeout(() => {
|
||||
navigate('/login')
|
||||
}, 500)
|
||||
};
|
||||
useEffect(() => {
|
||||
const pathParts = location.pathname.split('/').filter(Boolean)
|
||||
if (pathParts.length > 1) {
|
||||
setSelectedKey(pathParts[0]) // Return the section (production/management)
|
||||
}
|
||||
}, [location.pathname])
|
||||
|
||||
const menuItems = [
|
||||
useEffect(() => {
|
||||
setSocketConnected(socket?.connected)
|
||||
}, [socket?.connected])
|
||||
|
||||
const mainMenuItems = [
|
||||
{
|
||||
key: "1",
|
||||
label: "Profile",
|
||||
icon: <UserOutlined />,
|
||||
key: 'production',
|
||||
label: 'Production',
|
||||
icon: <PrinterOutlined />
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Logout",
|
||||
icon: <LogoutOutlined />,
|
||||
onClick: () => {handleLogout();},
|
||||
key: 'inventory',
|
||||
label: 'Inventory',
|
||||
icon: <ProductOutlined />
|
||||
},
|
||||
];
|
||||
{
|
||||
key: 'shop',
|
||||
label: 'Commerce',
|
||||
icon: <ShoppingCartOutlined />
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: 'Finance',
|
||||
icon: <PoundOutlined />
|
||||
},
|
||||
|
||||
{
|
||||
key: 'management',
|
||||
label: 'Management',
|
||||
icon: <SettingOutlined />
|
||||
}
|
||||
]
|
||||
|
||||
const userMenuItems = {
|
||||
items: [
|
||||
{
|
||||
key: 'username',
|
||||
label: userProfile?.username,
|
||||
icon: <UserOutlined />,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: userProfile?.email,
|
||||
icon: <MailOutlined />,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: 'Logout',
|
||||
icon: <LogoutOutlined />
|
||||
}
|
||||
],
|
||||
onClick: (key) => {
|
||||
if (key === 'profile') {
|
||||
navigate('/profile')
|
||||
} else if (key === 'logout') {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMainMenuClick = ({ key }) => {
|
||||
if (key === 'production') {
|
||||
navigate('/production/overview')
|
||||
} else if (key === 'inventory') {
|
||||
navigate('/inventory/spools')
|
||||
} else if (key === 'management') {
|
||||
navigate('/management/filaments')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Header className="header">
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
items={menuItems}
|
||||
style={{float: 'right'}}
|
||||
/>
|
||||
<Header
|
||||
style={{ width: '100vw', padding: 0 }}
|
||||
theme='light'
|
||||
className='ant-menu-horizontal'
|
||||
>
|
||||
<Flex
|
||||
gap={'middle'}
|
||||
align='center'
|
||||
className='ant-menu-light ant-menu-horizontal'
|
||||
style={{ padding: '0 24px', lineHeight: '64px', height: '100%' }}
|
||||
>
|
||||
<img src='/logo.svg' alt='Logo' width={180} />
|
||||
<Menu
|
||||
mode='horizontal'
|
||||
items={mainMenuItems}
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
flexGrow: 1
|
||||
}}
|
||||
onClick={handleMainMenuClick}
|
||||
selectedKeys={[selectedKey]}
|
||||
overflowedIndicator={<MenuOutlined />}
|
||||
/>
|
||||
<Flex gap={'middle'} align='center'>
|
||||
<Tooltip title={<Text keyboard>⌘ ⇧ P</Text>} arrow={false}>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
type='text'
|
||||
style={{ marginTop: '2px' }}
|
||||
onClick={() => showSpotlight()}
|
||||
></Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon={<BellOutlined />}
|
||||
type='text'
|
||||
style={{ marginTop: '2px' }}
|
||||
onClick={() => showSpotlight()}
|
||||
></Button>
|
||||
{!socketConnected ? (
|
||||
<Space>
|
||||
<Tag
|
||||
color='error'
|
||||
style={{ marginRight: 0 }}
|
||||
icon={<DisconnectOutlined />}
|
||||
>
|
||||
Disconnected
|
||||
</Tag>
|
||||
</Space>
|
||||
) : null}
|
||||
<Space>
|
||||
<Tag color='yellow' style={{ marginRight: 0 }}>
|
||||
Dev
|
||||
</Tag>
|
||||
</Space>
|
||||
{userProfile ? (
|
||||
<Space>
|
||||
<Dropdown menu={userMenuItems} placement='bottomRight'>
|
||||
<Tag style={{ marginRight: 0 }} icon={<UserOutlined />}>
|
||||
{userProfile?.name ? userProfile.name : userProfile.username}
|
||||
</Tag>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardNavigation;
|
||||
export default DashboardNavigation
|
||||
|
||||
@ -1,237 +0,0 @@
|
||||
// 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;
|
||||
@ -1,58 +1,57 @@
|
||||
// Sidebar.js
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Layout, Menu } from "antd";
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Layout, Menu } from 'antd'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
PrinterOutlined,
|
||||
PlayCircleOutlined,
|
||||
FileOutlined,
|
||||
} from "@ant-design/icons";
|
||||
PlayCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
import FillamentIcon from "../../Icons/FillamentIcon"
|
||||
import GCodeFileIcon from "../../Icons/GCodeFileIcon";
|
||||
import FilamentIcon from '../../Icons/FilamentIcon'
|
||||
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
|
||||
|
||||
const { Sider } = Layout;
|
||||
const { Sider } = Layout
|
||||
|
||||
const Sidebar = () => {
|
||||
const items = [
|
||||
{
|
||||
key: "overview",
|
||||
label: <Link to="/dashboard/overview">Overview</Link>,
|
||||
icon: <DashboardOutlined />,
|
||||
key: 'overview',
|
||||
label: <Link to='/dashboard/overview'>Overview</Link>,
|
||||
icon: <DashboardOutlined />
|
||||
},
|
||||
{
|
||||
key: "printers",
|
||||
label: <Link to="/dashboard/printers">Printers</Link>,
|
||||
icon: <PrinterOutlined />,
|
||||
key: 'printers',
|
||||
label: <Link to='/dashboard/printers'>Printers</Link>,
|
||||
icon: <PrinterOutlined />
|
||||
},
|
||||
{
|
||||
key: "jobs",
|
||||
label: <Link to="/dashboard/printjobs">Print Jobs</Link>,
|
||||
icon: <PlayCircleOutlined />,
|
||||
key: 'jobs',
|
||||
label: <Link to='/dashboard/printjobs'>Print Jobs</Link>,
|
||||
icon: <PlayCircleOutlined />
|
||||
},
|
||||
{
|
||||
key: "fillaments",
|
||||
label: <Link to="/dashboard/fillaments">Fillaments</Link>,
|
||||
icon: <FillamentIcon />,
|
||||
key: 'filaments',
|
||||
label: <Link to='/dashboard/filaments'>Filaments</Link>,
|
||||
icon: <FilamentIcon />
|
||||
},
|
||||
{
|
||||
key: "gcodefiles",
|
||||
label: <Link to="/dashboard/gcodefiles">G Code Files</Link>,
|
||||
icon: <GCodeFileIcon />,
|
||||
},
|
||||
];
|
||||
key: 'gcodefiles',
|
||||
label: <Link to='/dashboard/gcodefiles'>G Code Files</Link>,
|
||||
icon: <GCodeFileIcon />
|
||||
}
|
||||
]
|
||||
return (
|
||||
<Sider width={250} className="site-layout-background" collapsible>
|
||||
<Sider width={250}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultSelectedKeys={["1"]}
|
||||
defaultOpenKeys={["sub1"]}
|
||||
style={{ height: "100%", borderRight: 0 }}
|
||||
mode='inline'
|
||||
defaultSelectedKeys={['1']}
|
||||
defaultOpenKeys={['sub1']}
|
||||
style={{ height: '100%' }}
|
||||
items={items}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
export default Sidebar
|
||||
|
||||
@ -1,237 +0,0 @@
|
||||
// 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;
|
||||
@ -1,150 +0,0 @@
|
||||
// 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;
|
||||
@ -1,67 +1,72 @@
|
||||
import * as GCodePreview from 'gcode-preview';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import * as GCodePreview from 'gcode-preview'
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import * as THREE from 'three'
|
||||
|
||||
function GCodePreviewUI(props, ref) {
|
||||
function GCodePreviewUI(props, ref, initialGCode) {
|
||||
const {
|
||||
topLayerColor = '',
|
||||
lastSegmentColor = '',
|
||||
startLayer,
|
||||
endLayer,
|
||||
lineWidth
|
||||
} = props;
|
||||
const canvasRef = useRef(null);
|
||||
const [preview, setPreview] = useState();
|
||||
topLayerColor = '',
|
||||
lastSegmentColor = '',
|
||||
startLayer,
|
||||
endLayer,
|
||||
lineWidth
|
||||
} = props
|
||||
const canvasRef = useRef(null)
|
||||
const [preview, setPreview] = useState()
|
||||
|
||||
const resizePreview = () => {
|
||||
preview?.resize();
|
||||
};
|
||||
preview?.resize()
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getLayerCount() {
|
||||
return preview?.layers.length;
|
||||
},
|
||||
processGCode(gcode) {
|
||||
preview?.processGCode(gcode);
|
||||
}
|
||||
}));
|
||||
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
|
||||
})
|
||||
);
|
||||
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);
|
||||
window.addEventListener('resize', resizePreview)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizePreview);
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizePreview)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="gcode-preview">
|
||||
<canvas ref={canvasRef}></canvas>
|
||||
<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>
|
||||
);
|
||||
<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);
|
||||
|
||||
export default forwardRef(GCodePreviewUI)
|
||||
|
||||
@ -1,75 +1,96 @@
|
||||
// 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";
|
||||
import React, {
|
||||
createContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useContext,
|
||||
useRef
|
||||
} from 'react'
|
||||
import io from 'socket.io-client'
|
||||
import { message, notification } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AuthContext } from '../../Auth/AuthContext'
|
||||
|
||||
const SocketContext = createContext();
|
||||
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();
|
||||
const { token } = useContext(AuthContext)
|
||||
const socketRef = useRef(null)
|
||||
const [connecting, setConnecting] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [notificationApi] = notification.useNotification()
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
console.log("Token is available, connecting to web socket server...");
|
||||
console.log('Token is available, connecting to web socket server...')
|
||||
|
||||
const newSocket = io("http://localhost:5050", {
|
||||
const newSocket = io('http://localhost:8081', {
|
||||
reconnectionAttempts: 3,
|
||||
timeout: 3000,
|
||||
query: { token },
|
||||
});
|
||||
auth: { token: token }
|
||||
})
|
||||
|
||||
setConnecting(true);
|
||||
setConnecting(true)
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
console.log("Socket connected");
|
||||
setConnecting(false);
|
||||
setError(null);
|
||||
});
|
||||
newSocket.on('connect', () => {
|
||||
console.log('Socket connected')
|
||||
setConnecting(false)
|
||||
setError(null)
|
||||
})
|
||||
|
||||
newSocket.on("disconnect", () => {
|
||||
console.log("Socket disconnected");
|
||||
setError("Socket disconnected");
|
||||
});
|
||||
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('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");
|
||||
});
|
||||
newSocket.on('bridge.notification', (data) => {
|
||||
notificationApi[data.type]({
|
||||
title: data.title,
|
||||
message: data.message
|
||||
})
|
||||
})
|
||||
|
||||
socketRef.current = newSocket;
|
||||
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;
|
||||
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;
|
||||
console.log('Token not available, disconnecting socket...')
|
||||
socketRef.current.disconnect()
|
||||
socketRef.current = null
|
||||
}
|
||||
}, [token, messageApi]);
|
||||
}, [token, messageApi])
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={{ socket: socketRef.current, error, connecting }}>
|
||||
<SocketContext.Provider
|
||||
value={{ socket: socketRef.current, error, connecting }}
|
||||
>
|
||||
{contextHolder}
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export { SocketContext, SocketProvider };
|
||||
SocketProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { SocketContext, SocketProvider }
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
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;
|
||||
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import Icon from '@ant-design/icons';
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/gcodefileicon.svg';
|
||||
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} />
|
||||
);
|
||||
const GCodeFileIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default GCodeFileIcon;
|
||||
export default GCodeFileIcon
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import Icon from '@ant-design/icons';
|
||||
import { ReactComponent as CustomIconSvg } from '../../assets/icons/passkeysicon.svg';
|
||||
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} />
|
||||
);
|
||||
const PassKeysIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||
|
||||
export default PassKeysIcon;
|
||||
export default PassKeysIcon
|
||||
|
||||
@ -1,8 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
// PrivateRoute.js
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useContext } from 'react'
|
||||
//import { Navigate } from 'react-router-dom'
|
||||
import { AuthContext } from './Auth/AuthContext'
|
||||
|
||||
const PrivateRoute = ({ component: Component }) => {
|
||||
return localStorage.getItem('access_token') ? <Component /> : <Navigate to="/login" />;
|
||||
};
|
||||
const { authenticated, loading, showSessionExpiredModal } =
|
||||
useContext(AuthContext)
|
||||
|
||||
export default PrivateRoute;
|
||||
// Show loading state while auth state is being determined
|
||||
if (loading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
return authenticated || showSessionExpiredModal ? (
|
||||
<Component />
|
||||
) : (
|
||||
<Component />
|
||||
)
|
||||
}
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
component: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default PrivateRoute
|
||||
|
||||
@ -1,8 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
// PublicRoute.js
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useContext } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { AuthContext } from './Auth/AuthContext'
|
||||
|
||||
const PublicRoute = ({ component: Component }) => {
|
||||
return !localStorage.getItem('access_token') ? <Component /> : <Navigate to="/dashboard/overview" />;
|
||||
};
|
||||
const { authenticated, loading } = useContext(AuthContext)
|
||||
|
||||
export default PublicRoute;
|
||||
// Show loading state while auth state is being determined
|
||||
if (loading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
return !authenticated ? <Component /> : <Navigate to='/production/overview' />
|
||||
}
|
||||
|
||||
PublicRoute.propTypes = {
|
||||
component: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default PublicRoute
|
||||
|
||||
@ -11,3 +11,7 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.ant-modal-mask {
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
16
src/index.js
16
src/index.js
@ -1,17 +1,17 @@
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import FarmControlApp from "./App";
|
||||
import "./index.css";
|
||||
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"));
|
||||
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();
|
||||
reportWebVitals()
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
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);
|
||||
});
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default reportWebVitals;
|
||||
export default reportWebVitals
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
// 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';
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user