From 0634919bb685cb29beffcc48d973cbb4cbada487 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 29 Jun 2025 02:28:01 +0100 Subject: [PATCH] Refactored application to replace SocketContext with PrintServerContext for WebSocket communication. Updated related components to utilize print server events for real-time updates. Removed unused SocketContext and NotificationContext files. Updated package dependencies to use loglevel instead of log4js for improved logging capabilities. --- package-lock.json | 80 +- package.json | 2 +- src/App.css | 11 +- src/App.jsx | 18 +- src/assets/icons/lockicon.afdesign | Bin 0 -> 25337 bytes src/assets/icons/lockicon.min.svg | 1 + src/assets/icons/lockicon.svg | 7 + .../Dashboard/Developer/DeveloperSidebar.jsx | 16 +- ...tDebug.jsx => PrintServerContextDebug.jsx} | 14 +- .../Dashboard/Inventory/FilamentStocks.jsx | 14 +- .../FilamentStocks/FilamentStockInfo.jsx | 14 +- .../FilamentStocks/LoadFilamentStock.jsx | 18 +- .../FilamentStocks/UnloadFilamentStock.jsx | 18 +- .../Dashboard/Inventory/StockAudits.jsx | 14 +- .../Dashboard/Inventory/StockEvents.jsx | 14 +- .../Management/Filaments/FilamentInfo.jsx | 890 +++++++++--------- .../Production/GCodeFiles/NewGCodeFile.jsx | 2 +- src/components/Dashboard/Production/Jobs.jsx | 8 +- .../Dashboard/Production/Jobs/JobInfo.jsx | 14 +- .../Production/Printers/ControlPrinter.jsx | 42 +- .../Production/Printers/NewPrinter.jsx | 28 +- .../Dashboard/common/DashboardBreadcrumb.jsx | 2 +- .../Dashboard/common/DashboardNavigation.jsx | 156 +-- .../Dashboard/common/FilamentStockState.jsx | 14 +- .../Dashboard/common/InfoCollapse.jsx | 55 ++ src/components/Dashboard/common/JobState.jsx | 14 +- .../Dashboard/common/KeyboardShortcut.jsx | 115 +++ .../Dashboard/common/PrinterJobsTree.jsx | 16 +- .../Dashboard/common/PrinterMiscPanel.jsx | 38 +- .../Dashboard/common/PrinterMovementPanel.jsx | 12 +- .../Dashboard/common/PrinterPositionPanel.jsx | 34 +- .../Dashboard/common/PrinterState.jsx | 20 +- .../common/PrinterTemperaturePanel.jsx | 24 +- .../Dashboard/common/StockEventTable.jsx | 14 +- .../Dashboard/common/SubJobCounter.jsx | 14 +- .../Dashboard/common/SubJobState.jsx | 22 +- .../Dashboard/common/SubJobsTree.jsx | 18 +- .../Dashboard/context/ApiServerContext.js | 300 ++++++ .../Dashboard/context/NotificationContext.js | 212 ----- ...SocketContext.js => PrintServerContext.js} | 43 +- .../Dashboard/context/SpotlightContext.js | 18 - src/components/Icons/LockIcon.jsx | 7 + src/config.js | 10 +- 43 files changed, 1318 insertions(+), 1065 deletions(-) create mode 100644 src/assets/icons/lockicon.afdesign create mode 100644 src/assets/icons/lockicon.min.svg create mode 100644 src/assets/icons/lockicon.svg rename src/components/Dashboard/Developer/{SocketContextDebug.jsx => PrintServerContextDebug.jsx} (82%) create mode 100644 src/components/Dashboard/common/InfoCollapse.jsx create mode 100644 src/components/Dashboard/common/KeyboardShortcut.jsx create mode 100644 src/components/Dashboard/context/ApiServerContext.js delete mode 100644 src/components/Dashboard/context/NotificationContext.js rename src/components/Dashboard/context/{SocketContext.js => PrintServerContext.js} (59%) create mode 100644 src/components/Icons/LockIcon.jsx diff --git a/package-lock.json b/package-lock.json index 4bfaf39..005bd84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "gcode-preview": "^2.18.0", "keycloak-js": "^26.2.0", - "log4js": "^6.9.1", + "loglevel": "^1.9.2", "moment": "*", "prettier": "^3.5.3", "prettier-eslint": "^16.4.2", @@ -9986,15 +9986,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -16008,27 +15999,10 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, - "node_modules/log4js": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", - "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/loglevel": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -21486,12 +21460,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -22819,52 +22787,6 @@ "node": ">= 0.4" } }, - "node_modules/streamroller": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", - "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", - "license": "MIT", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/streamroller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index e1b964e..f7b0ca1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "gcode-preview": "^2.18.0", "keycloak-js": "^26.2.0", - "log4js": "^6.9.1", + "loglevel": "^1.9.2", "moment": "*", "prettier": "^3.5.3", "prettier-eslint": "^16.4.2", diff --git a/src/App.css b/src/App.css index 709352a..8c52b6d 100644 --- a/src/App.css +++ b/src/App.css @@ -22,11 +22,14 @@ .ant-input, .ant-input-number .ant-input-number-input, .ant-segmented-item-label, -.ant-badge-status-text { +.ant-badge-status-text, +.ant-tree-title, +.ant-select { font-family: 'DM Sans'; } -.ant-typography code { +.ant-typography code, +.ant-typography pre { font-family: 'DM Mono'; } @@ -115,6 +118,10 @@ code { padding: 0 !important; } +.ant-popover-inner:has(.keyboard-shortcut-tooltip) { + padding: 8px !important; +} + /* --- Start of src/index.css --- */ body { margin: 0; diff --git a/src/App.jsx b/src/App.jsx index 43b8132..d6e9d07 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -44,7 +44,7 @@ import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAu import Dashboard from './components/Dashboard/Dashboard.jsx' import PrivateRoute from './components/PrivateRoute' import './App.css' -import { SocketProvider } from './components/Dashboard/context/SocketContext.js' +import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.js' import { AuthProvider } from './components/Dashboard/context/AuthContext.js' import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js' import StockEvents from './components/Dashboard/Inventory/StockEvents.jsx' @@ -61,8 +61,8 @@ import NoteTypes from './components/Dashboard/Management/NoteTypes.jsx' import NoteTypeInfo from './components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx' import SessionStorage from './components/Dashboard/Developer/SessionStorage.jsx' import AuthContextDebug from './components/Dashboard/Developer/AuthContextDebug.jsx' -import SocketContextDebug from './components/Dashboard/Developer/SocketContextDebug.jsx' -import { NotificationProvider } from './components/Dashboard/context/NotificationContext.js' +import PrintServerContextDebug from './components/Dashboard/Developer/PrintServerContextDebug.jsx' +import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.js' import Users from './components/Dashboard/Management/Users.jsx' import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx' @@ -74,8 +74,8 @@ const AppContent = () => { - - + + { element={} /> } + path='developer/printservercontextdebug' + element={} /> { /> - - + + diff --git a/src/assets/icons/lockicon.afdesign b/src/assets/icons/lockicon.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..5ed851baa7ebf63a3fab8d52e7baee9ee104db47 GIT binary patch literal 25337 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 MpEfHycJb!_0*d$cr~m)} literal 0 HcmV?d00001 diff --git a/src/assets/icons/lockicon.min.svg b/src/assets/icons/lockicon.min.svg new file mode 100644 index 0000000..f07fb04 --- /dev/null +++ b/src/assets/icons/lockicon.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/lockicon.svg b/src/assets/icons/lockicon.svg new file mode 100644 index 0000000..cd9f0bc --- /dev/null +++ b/src/assets/icons/lockicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/components/Dashboard/Developer/DeveloperSidebar.jsx b/src/components/Dashboard/Developer/DeveloperSidebar.jsx index 1e359f8..c6c68a5 100644 --- a/src/components/Dashboard/Developer/DeveloperSidebar.jsx +++ b/src/components/Dashboard/Developer/DeveloperSidebar.jsx @@ -1,29 +1,35 @@ import React from 'react' import { useLocation } from 'react-router-dom' import DashboardSidebar from '../common/DashboardSidebar' +import { Typography } from 'antd' + +const { Text } = Typography const items = [ { key: 'sessionstorage', label: 'Session Storage', + icon: 🗃️, path: '/dashboard/developer/sessionstorage' }, { key: 'authcontextdebug', - label: 'Auth Context Debug', + label: 'Auth Debug', + icon: 🔐, path: '/dashboard/developer/authcontextdebug' }, { - key: 'socketcontextdebug', - label: 'Socket Context Debug', - path: '/dashboard/developer/socketcontextdebug' + key: 'printservercontextdebug', + label: 'Print Server Debug', + icon: 🖨️, + path: '/dashboard/developer/printservercontextdebug' } ] const routeKeyMap = { '/dashboard/developer/sessionstorage': 'sessionstorage', '/dashboard/developer/authcontext': 'authcontextdebug', - '/dashboard/developer/socketcontext': 'socketcontextdebug' + '/dashboard/developer/printservercontext': 'printservercontextdebug' } const DeveloperSidebar = (props) => { diff --git a/src/components/Dashboard/Developer/SocketContextDebug.jsx b/src/components/Dashboard/Developer/PrintServerContextDebug.jsx similarity index 82% rename from src/components/Dashboard/Developer/SocketContextDebug.jsx rename to src/components/Dashboard/Developer/PrintServerContextDebug.jsx index 54b0b53..e5f3b9b 100644 --- a/src/components/Dashboard/Developer/SocketContextDebug.jsx +++ b/src/components/Dashboard/Developer/PrintServerContextDebug.jsx @@ -9,13 +9,13 @@ import { message } from 'antd' import ReloadIcon from '../../Icons/ReloadIcon.jsx' -import { SocketContext } from '../context/SocketContext.js' +import { PrintServerContext } from '../context/PrintServerContext.js' import BoolDisplay from '../common/BoolDisplay.jsx' const { Text, Paragraph } = Typography -const SocketContextDebug = () => { - const { socket, error, connecting } = useContext(SocketContext) +const PrintServerContextDebug = () => { + const { printServer, error, connecting } = useContext(PrintServerContext) const [msgApi, contextHolder] = message.useMessage() const actionItems = { @@ -36,9 +36,9 @@ const SocketContextDebug = () => { // Helper to display socket info safely const getSocketInfo = () => { - if (!socket) return 'n/a' + if (!printServer) return 'n/a' // Only show safe properties - const { id, connected, disconnected, nsp } = socket + const { id, connected, disconnected, nsp } = printServer return JSON.stringify({ id, connected, disconnected, nsp }, null, 2) } @@ -55,7 +55,7 @@ const SocketContextDebug = () => {
- + @@ -74,4 +74,4 @@ const SocketContextDebug = () => { ) } -export default SocketContextDebug +export default PrintServerContextDebug diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index 5be217f..c9275a6 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -16,7 +16,7 @@ import { } from 'antd' import { AuthContext } from '../context/AuthContext' -import { SocketContext } from '../context/SocketContext' +import { PrintServerContext } from '../context/PrintServerContext' import NewFilamentStock from './FilamentStocks/NewFilamentStock' import IdText from '../common/IdText' @@ -41,7 +41,7 @@ const { Text } = Typography const FilamentStocks = () => { const [messageApi, contextHolder] = message.useMessage() const navigate = useNavigate() - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [initialized, setInitialized] = useState(false) const tableRef = useRef() @@ -213,9 +213,9 @@ const FilamentStocks = () => { ) React.useEffect(() => { - if (socket && !initialized) { + if (printServer && !initialized) { setInitialized(true) - socket.on('notify_filamentstock_update', (updateData) => { + printServer.on('notify_filamentstock_update', (updateData) => { console.log('Received filament stock update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) @@ -224,12 +224,12 @@ const FilamentStocks = () => { } return () => { - if (socket && initialized) { + if (printServer && initialized) { console.log('Deregistering filament stock update listener') - socket.off('notify_filamentstock_update') + printServer.off('notify_filamentstock_update') } } - }, [socket, initialized]) + }, [printServer, initialized]) const getFilamentStockActionItems = (id) => { return { diff --git a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx index 1e3a310..2f59597 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx @@ -19,7 +19,7 @@ import { } from 'antd' import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' import IdText from '../../common/IdText' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import FilamentStockState from '../../common/FilamentStockState' import StockEventTable from '../../common/StockEventTable' import useCollapseState from '../../hooks/useCollapseState' @@ -48,7 +48,7 @@ const FilamentStockInfo = () => { 'filamentStockId' ) const [form] = Form.useForm() - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [collapseState, updateCollapseState] = useCollapseState( 'FilamentStockInfo', { @@ -75,9 +75,9 @@ const FilamentStockInfo = () => { // Add WebSocket event listener for real-time updates useEffect(() => { - if (socket && !initialized && filamentStockId) { + if (printServer && !initialized && filamentStockId) { setInitialized(true) - socket.on('notify_filamentstock_update', (statusUpdate) => { + printServer.on('notify_filamentstock_update', (statusUpdate) => { console.log('GOT FILAMENT STOCK UPDATE', statusUpdate) setFilamentStockData((prevData) => { if (statusUpdate?._id === filamentStockId) { @@ -91,12 +91,12 @@ const FilamentStockInfo = () => { }) } return () => { - if (socket && initialized) { + if (printServer && initialized) { console.log('Deregistering filament stock update listener') - socket.off('notify_filamentstock_update') + printServer.off('notify_filamentstock_update') } } - }, [socket, initialized, filamentStockId]) + }, [printServer, initialized, filamentStockId]) const fetchFilamentStockDetails = async () => { try { diff --git a/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx b/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx index cb3598d..2a7b7d8 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx @@ -11,7 +11,7 @@ import { } from 'antd' import { useMediaQuery } from 'react-responsive' import PropTypes from 'prop-types' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import FilamentStockSelect from '../../common/FilamentStockSelect' import PrinterSelect from '../../common/PrinterSelect' @@ -38,7 +38,7 @@ const LoadFilamentStock = ({ filamentStockLoaded: PropTypes.bool } - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const initialLoadFilamentStockForm = { printer: printer, @@ -93,16 +93,16 @@ const LoadFilamentStock = ({ console.log(statusUpdate) } - socket.emit('printer.objects.subscribe', params) - socket.emit('printer.objects.query', params) - socket.on('notify_status_update', notifyStatusUpdate) + printServer.emit('printer.objects.subscribe', params) + printServer.emit('printer.objects.query', params) + printServer.on('notify_status_update', notifyStatusUpdate) return () => { - socket.off('notify_status_update', notifyStatusUpdate) - socket.emit('printer.objects.unsubscribe', params) + printServer.off('notify_status_update', notifyStatusUpdate) + printServer.emit('printer.objects.unsubscribe', params) } } - }, [socket, loadFilamentStockFormValues.printer]) + }, [printServer, loadFilamentStockFormValues.printer]) React.useEffect(() => { loadFilamentStockForm @@ -170,7 +170,7 @@ const LoadFilamentStock = ({ try { // Set the extruder temperature - await socket.emit('printer.filamentstock.load', { + await printServer.emit('printer.filamentstock.load', { printerId: loadFilamentStockFormValues.printer._id, filamentStockId: loadFilamentStockFormValues.filamentStock._id }) diff --git a/src/components/Dashboard/Inventory/FilamentStocks/UnloadFilamentStock.jsx b/src/components/Dashboard/Inventory/FilamentStocks/UnloadFilamentStock.jsx index 215880f..dd1f3df 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks/UnloadFilamentStock.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks/UnloadFilamentStock.jsx @@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect } from 'react' import { Form, Button, Typography, Flex, Steps, Divider, Alert } from 'antd' import { useMediaQuery } from 'react-responsive' import PropTypes from 'prop-types' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import PrinterSelect from '../../common/PrinterSelect' import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' @@ -18,7 +18,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => { printer: PropTypes.object } - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const isMobile = useMediaQuery({ maxWidth: 768 }) const initialUnloadFilamentStockForm = { @@ -66,16 +66,16 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => { } } - socket.emit('printer.objects.subscribe', params) - socket.emit('printer.objects.query', params) - socket.on('notify_status_update', notifyStatusUpdate) + printServer.emit('printer.objects.subscribe', params) + printServer.emit('printer.objects.query', params) + printServer.on('notify_status_update', notifyStatusUpdate) return () => { - socket.off('notify_status_update', notifyStatusUpdate) - socket.emit('printer.objects.unsubscribe', params) + printServer.off('notify_status_update', notifyStatusUpdate) + printServer.emit('printer.objects.unsubscribe', params) } } - }, [socket, unloadFilamentStockFormValues.printer]) + }, [printServer, unloadFilamentStockFormValues.printer]) React.useEffect(() => { if (reset) { @@ -109,7 +109,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => { const handleUnloadFilamentStock = async () => { setUnloadFilamentStockLoading(true) // Send G-code to retract the filament - await socket.emit('printer.gcode.script', { + await printServer.emit('printer.gcode.script', { printerId: unloadFilamentStockFormValues.printer._id, script: `_CLIENT_LINEAR_MOVE E=-200 F=1000` }) diff --git a/src/components/Dashboard/Inventory/StockAudits.jsx b/src/components/Dashboard/Inventory/StockAudits.jsx index 2febd96..28f0272 100644 --- a/src/components/Dashboard/Inventory/StockAudits.jsx +++ b/src/components/Dashboard/Inventory/StockAudits.jsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { Button, Flex, Space, message, Dropdown, Typography } from 'antd' import { AuthContext } from '../context/AuthContext' -import { SocketContext } from '../context/SocketContext' +import { PrintServerContext } from '../context/PrintServerContext' import IdText from '../common/IdText' import StockAuditIcon from '../../Icons/StockAuditIcon' @@ -20,16 +20,16 @@ const { Text } = Typography const StockAudits = () => { const [messageApi, contextHolder] = message.useMessage() const navigate = useNavigate() - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [initialized, setInitialized] = useState(false) const tableRef = useRef() const { authenticated } = useContext(AuthContext) React.useEffect(() => { - if (socket && !initialized) { + if (printServer && !initialized) { setInitialized(true) - socket.on('notify_stockaudit_update', (updateData) => { + printServer.on('notify_stockaudit_update', (updateData) => { console.log('Received stock audit update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) @@ -38,12 +38,12 @@ const StockAudits = () => { } return () => { - if (socket && initialized) { + if (printServer && initialized) { console.log('Deregistering stock audit update listener') - socket.off('notify_stockaudit_update') + printServer.off('notify_stockaudit_update') } } - }, [socket, initialized]) + }, [printServer, initialized]) const getStockAuditActionItems = (id) => { return { diff --git a/src/components/Dashboard/Inventory/StockEvents.jsx b/src/components/Dashboard/Inventory/StockEvents.jsx index 3431206..ce14d3a 100644 --- a/src/components/Dashboard/Inventory/StockEvents.jsx +++ b/src/components/Dashboard/Inventory/StockEvents.jsx @@ -11,7 +11,7 @@ import { } from 'antd' import { AuthContext } from '../context/AuthContext' -import { SocketContext } from '../context/SocketContext' +import { PrintServerContext } from '../context/PrintServerContext' import IdText from '../common/IdText' import TimeDisplay from '../common/TimeDisplay' import ReloadIcon from '../../Icons/ReloadIcon' @@ -31,7 +31,7 @@ import StockEventIcon from '../../Icons/StockEventIcon' const { Text } = Typography const StockEvents = () => { - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [initialized, setInitialized] = useState(false) const tableRef = useRef() const [viewMode, setViewMode] = useViewMode('StockEvents') @@ -225,9 +225,9 @@ const StockEvents = () => { React.useEffect(() => { // Add WebSocket event listener for real-time updates - if (socket && !initialized) { + if (printServer && !initialized) { setInitialized(true) - socket.on('notify_stockevent_update', (updateData) => { + printServer.on('notify_stockevent_update', (updateData) => { console.log('Received stock event update:', updateData) if (tableRef.current) { tableRef.current.updateData(updateData._id, updateData) @@ -236,12 +236,12 @@ const StockEvents = () => { } return () => { - if (socket && initialized) { + if (printServer && initialized) { console.log('Deregistering stock event update listener') - socket.off('notify_stockevent_update') + printServer.off('notify_stockevent_update') } } - }, [socket, initialized]) + }, [printServer, initialized]) const actionItems = { items: [ diff --git a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx index d52089b..adf0696 100644 --- a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx +++ b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useContext, useCallback } from 'react' import { useLocation } from 'react-router-dom' -import axios from 'axios' import { Descriptions, Spin, @@ -15,13 +14,15 @@ import { InputNumber, ColorPicker, Select, - Collapse, Dropdown, Popover, Checkbox, - Card + Card, + Tag } from 'antd' -import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' import IdText from '../../common/IdText' import ReloadIcon from '../../../Icons/ReloadIcon' import EditIcon from '../../../Icons/EditIcon.jsx' @@ -32,19 +33,25 @@ import VendorSelect from '../../common/VendorSelect' import useCollapseState from '../../hooks/useCollapseState' import AuditLogTable from '../../common/AuditLogTable' import DashboardNotes from '../../common/DashboardNotes' +import InfoCollapse from '../../common/InfoCollapse' -import config from '../../../../config.js' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import LockIcon from '../../../Icons/LockIcon.jsx' +import { ApiServerContext } from '../../context/ApiServerContext' -const { Title, Link, Text } = Typography +const log = loglevel.getLogger('FilamentInfo') +log.setLevel(config.logLevel) + +const { Link, Text } = Typography const FilamentInfo = () => { const [filamentData, setFilamentData] = useState(null) const [fetchLoading, setFetchLoading] = useState(true) const [editLoading, setEditLoading] = useState(false) - const [error, setError] = useState(null) + const [lockUser, setLockUser] = useState(null) + const [initialized, setInitialized] = useState(false) const location = useLocation() const [messageApi, contextHolder] = message.useMessage() const filamentId = new URLSearchParams(location.search).get('filamentId') @@ -59,12 +66,36 @@ const FilamentInfo = () => { auditLogs: true } ) + const { + apiServer, + fetchObjectInfo, + updateObjectInfo, + lockObject, + unlockObject, + onLockEvent, + onUpdateEvent, + fetchObjectLock, + showError + } = useContext(ApiServerContext) - useEffect(() => { - if (filamentId) { - fetchFilamentDetails() + // Define the event handler function + const lockEventHandler = useCallback((lockEvent) => { + if (lockEvent.locked === true) { + setLockUser(lockEvent.user) + } else { + setLockUser(null) } - }, [filamentId]) + }, []) + + // Cleanup effect for component unmount + useEffect(() => { + return () => { + if (filamentId) { + // Ensure any remaining locks are released when component unmounts + unlockObject(filamentId, 'filament') + } + } + }, [filamentId, unlockObject]) useEffect(() => { if (filamentData) { @@ -83,32 +114,84 @@ const FilamentInfo = () => { } }, [filamentData, form]) - const fetchFilamentDetails = async () => { + const fetchFilamentInfo = useCallback(async () => { try { setFetchLoading(true) - const response = await axios.get( - `${config.backendUrl}/filaments/${filamentId}`, - { - headers: { - Accept: 'application/json' - }, - withCredentials: true - } - ) - setFilamentData(response.data) - form.setFieldsValue(response.data) - setError(null) - } catch (err) { - setError('Failed to fetch filament details') - messageApi.error('Failed to fetch filament details') - } finally { + const data = await fetchObjectInfo(filamentId, 'filament') + const lockEvent = await fetchObjectLock(filamentId, 'filament') + setLockUser(lockEvent?.user || null) + setFilamentData(data) + form.setFieldsValue(data) setFetchLoading(false) + } catch (err) { + messageApi.error('Failed to fetch filament info') + // Show error modal with retry functionality + showError( + `Failed to fetch filament information. Message: ${err.message}. Code: ${err.code}`, + fetchFilamentInfo + ) } + }, [ + fetchObjectInfo, + fetchObjectLock, + filamentId, + form, + messageApi, + showError + ]) + + const updateFilamentInfo = async () => { + const values = form.getFieldsValue() + const updateValue = { + name: values.name, + vendor: values.vendor, + type: values.type, + cost: values.cost, + color: values.color, + diameter: values.diameter, + density: values.density, + url: values.url, + barcode: values.barcode, + emptySpoolWeight: values.emptySpoolWeight + } + await updateObjectInfo(filamentId, 'filament', updateValue) } + // Define the update event handler function + const updateEventHandler = useCallback( + (updateEvent) => { + log.debug('Update event received for filament:', updateEvent) + // Refresh the filament data when an update is received + fetchFilamentInfo() + }, + [fetchFilamentInfo] + ) + + useEffect(() => { + if (initialized == false && filamentId && apiServer?.connected === true) { + setInitialized(true) + fetchFilamentInfo() + } + }, [filamentId, apiServer?.connected, initialized, fetchFilamentInfo]) + + useEffect(() => { + if (filamentId) { + const cleanup = onLockEvent(filamentId, lockEventHandler) + return cleanup + } + }, [filamentId, onLockEvent, lockEventHandler]) + + useEffect(() => { + if (filamentId) { + const cleanup = onUpdateEvent(filamentId, updateEventHandler) + return cleanup + } + }, [filamentId, onUpdateEvent, updateEventHandler]) + const startEditing = () => { updateCollapseState('info', true) setIsEditing(true) + lockObject(filamentId, 'filament') } const cancelEditing = () => { @@ -128,34 +211,15 @@ const FilamentInfo = () => { }) } setIsEditing(false) + unlockObject(filamentId, 'filament') } - const updateFilamentInfo = async () => { + const handleUpdateFilamentInfo = async () => { try { const values = await form.validateFields() setEditLoading(true) - await axios.put( - `${config.backendUrl}/filaments/${filamentId}`, - { - name: values.name, - vendor: values.vendor, - type: values.type, - cost: values.cost, - color: values.color, - diameter: values.diameter, - density: values.density, - url: values.url, - barcode: values.barcode, - emptySpoolWeight: values.emptySpoolWeight - }, - { - headers: { - 'Content-Type': 'application/json' - }, - withCredentials: true - } - ) + await updateFilamentInfo() // Update the local state with the new values setFilamentData({ ...filamentData, ...values }) @@ -168,8 +232,13 @@ const FilamentInfo = () => { } console.error('Failed to update filament information:', err) messageApi.error('Failed to update filament information') + // Show error modal with retry functionality + showError( + `Failed to update filament information. Message: ${err.message}. Code: ${err.code}`, + () => handleUpdateFilamentInfo() + ) } finally { - fetchFilamentDetails() + fetchFilamentInfo() setEditLoading(false) } } @@ -184,7 +253,7 @@ const FilamentInfo = () => { ], onClick: ({ key }) => { if (key === 'reload') { - fetchFilamentDetails() + fetchFilamentInfo() } } } @@ -215,20 +284,6 @@ const FilamentInfo = () => { ) } - if (error) { - return ( - -

{error || 'Filament not found'}

- -
- ) - } - return ( <> {contextHolder} @@ -238,17 +293,34 @@ const FilamentInfo = () => { style={{ height: '100%', minHeight: 0 }} > - - - - - - - + + + + + + + + + + {lockUser && ( + + } + style={{ margin: 0 }} + color={'orange'} + /> + + + )} {isEditing ? ( @@ -256,7 +328,7 @@ const FilamentInfo = () => { - - ) : ( -
- - - updateCollapseState('info', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse no-t-padding-collapse' +
+ + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + className='no-t-padding-collapse' + key='info' + > +
- - - - Filament Information - - - } - key='1' - > - } spinning={fetchLoading}> + - } - spinning={fetchLoading} - > - - - {filamentData?._id ? ( - - ) : ( - n/a - )} - - - {filamentData?.createdAt ? ( - - ) : ( - n/a - )} - + + {filamentData?._id ? ( + + ) : ( + n/a + )} + + + {filamentData?.createdAt ? ( + + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.name ? ( - {filamentData.name} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.name ? ( + {filamentData.name} + ) : ( + n/a + )} + - - {filamentData?.updatedAt ? ( - - ) : ( - n/a - )} - + + {filamentData?.updatedAt ? ( + + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.vendor?.name ? ( - {filamentData.vendor.name} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.vendor?.name ? ( + {filamentData.vendor.name} + ) : ( + n/a + )} + - - {filamentData?.vendor?.id ? ( - - ) : ( - n/a - )} - + + {filamentData?.vendor?.id ? ( + + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.type ? ( - {filamentData.type} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.type ? ( + {filamentData.type} + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.cost ? ( - {`£${filamentData.cost}/kg`} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.cost ? ( + {`£${filamentData.cost}/kg`} + ) : ( + n/a + )} + - - - {isEditing ? ( - { - return '#' + color.toHex() - }} - > - - - ) : filamentData?.color ? ( - - ) : ( - n/a - )} - - + + + {isEditing ? ( + { + return '#' + color.toHex() + }} + > + + + ) : filamentData?.color ? ( + + ) : ( + n/a + )} + + - - {isEditing ? ( - - - - ) : filamentData?.diameter ? ( - {`${filamentData.diameter}mm`} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.diameter ? ( + {`${filamentData.diameter}mm`} + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.density ? ( - {`${filamentData.density}g/cm³`} - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.density ? ( + {`${filamentData.density}g/cm³`} + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.url ? ( - - {filamentData.url} - - ) : ( - n/a - )} - + + {isEditing ? ( + + + + ) : filamentData?.url ? ( + + {filamentData.url} + + ) : ( + n/a + )} + - - {isEditing ? ( - - - - ) : filamentData?.barcode ? ( - {filamentData.barcode} - ) : ( - n/a - )} - - - - -
- + + {isEditing ? ( + + + + ) : filamentData?.barcode ? ( + {filamentData.barcode} + ) : ( + n/a + )} + + + + +
- - updateCollapseState('notes', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - - - Notes - -
- } - key='notes' - > - - - - - + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + key='notes' + > + + + + - - updateCollapseState('auditLogs', keys.length > 0) - } - expandIcon={({ isActive }) => ( - - )} - className='no-h-padding-collapse' - > - - - - Audit Logs - - - } - key='auditLogs' - > - - - - -
- )} + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + key='auditLogs' + > + + +
+
) diff --git a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx index 3ef9fc3..5474888 100644 --- a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx +++ b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx @@ -72,7 +72,7 @@ const NewGCodeFile = ({ onOk, reset }) => { const { token, authenticated } = useContext(AuthContext) // eslint-disable-next-line - const fetchFilamentDetails = async () => { + const fetchFilamentInfo = async () => { if (!authenticated) { return } diff --git a/src/components/Dashboard/Production/Jobs.jsx b/src/components/Dashboard/Production/Jobs.jsx index be54fc8..e8ca155 100644 --- a/src/components/Dashboard/Production/Jobs.jsx +++ b/src/components/Dashboard/Production/Jobs.jsx @@ -17,7 +17,7 @@ import { } from 'antd' import { AuthContext } from '../context/AuthContext.js' -import { SocketContext } from '../context/SocketContext.js' +import { PrintServerContext } from '../context/PrintServerContext.js' import NewJob from './Jobs/NewJob.jsx' import JobState from '../common/JobState.jsx' import SubJobCounter from '../common/SubJobCounter.jsx' @@ -257,7 +257,7 @@ const Jobs = () => { ] const { authenticated } = useContext(AuthContext) - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [columnVisibility, updateColumnVisibility] = useColumnVisibility( 'Jobs', @@ -265,9 +265,9 @@ const Jobs = () => { ) const handleDeployJob = (jobId) => { - if (socket) { + if (printServer) { messageApi.info(`Print job ${jobId} deployment initiated`) - socket.emit('server.job_queue.deploy', { jobId }, (response) => { + printServer.emit('server.job_queue.deploy', { jobId }, (response) => { if (response == false) { notificationApi.error({ message: 'Print job deployment failed', diff --git a/src/components/Dashboard/Production/Jobs/JobInfo.jsx b/src/components/Dashboard/Production/Jobs/JobInfo.jsx index 36e2925..0aed559 100644 --- a/src/components/Dashboard/Production/Jobs/JobInfo.jsx +++ b/src/components/Dashboard/Production/Jobs/JobInfo.jsx @@ -21,7 +21,7 @@ import TimeDisplay from '../../common/TimeDisplay' import JobState from '../../common/JobState' import IdText from '../../common/IdText' import SubJobsTree from '../../common/SubJobsTree' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import GCodeFileIcon from '../../../Icons/GCodeFileIcon' import ReloadIcon from '../../../Icons/ReloadIcon' import useCollapseState from '../../hooks/useCollapseState' @@ -42,7 +42,7 @@ const JobInfo = () => { const location = useLocation() const [messageApi] = message.useMessage() const jobId = new URLSearchParams(location.search).get('jobId') - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [collapseState, updateCollapseState] = useCollapseState('JobInfo', { info: true, subJobs: true, @@ -57,8 +57,8 @@ const JobInfo = () => { }, [jobId]) useEffect(() => { - if (socket && jobId) { - socket.on('notify_job_update', (updateData) => { + if (printServer && jobId) { + printServer.on('notify_job_update', (updateData) => { if (updateData._id === jobId) { setJobData((prevData) => { if (!prevData) return prevData @@ -73,11 +73,11 @@ const JobInfo = () => { } return () => { - if (socket) { - socket.off('notify_job_update') + if (printServer) { + printServer.off('notify_job_update') } } - }, [socket, jobId]) + }, [printServer, jobId]) const fetchJobDetails = async () => { try { diff --git a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx index 3ba361f..8f8c689 100644 --- a/src/components/Dashboard/Production/Printers/ControlPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/ControlPrinter.jsx @@ -22,7 +22,7 @@ import { } from 'antd' import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' import PrinterPositionPanel from '../../common/PrinterPositionPanel' @@ -106,7 +106,7 @@ const ControlPrinter = () => { ) }, [componentVisibility]) - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const { authenticated } = useContext(AuthContext) // Fetch printer details when the component mounts @@ -143,9 +143,9 @@ const ControlPrinter = () => { // Add WebSocket event listener for real-time updates useEffect(() => { - if (socket && !initialized && printerId) { + if (printServer && !initialized && printerId) { setInitialized(true) - socket.on('notify_printer_update', (statusUpdate) => { + printServer.on('notify_printer_update', (statusUpdate) => { setPrinterData((prevData) => { if (statusUpdate?._id === printerId) { return { @@ -158,7 +158,7 @@ const ControlPrinter = () => { }) // Add WebSocket event listener for filament stock updates - socket.on('notify_filamentstock_update', (filamentStockUpdate) => { + printServer.on('notify_filamentstock_update', (filamentStockUpdate) => { setPrinterData((prevData) => { if (prevData?.currentFilamentStock) { if ( @@ -178,17 +178,17 @@ const ControlPrinter = () => { }) } return () => { - if (socket && initialized) { + if (printServer && initialized) { console.log('Deregistering') - socket.off('notify_printer_update') - socket.off('notify_filamentstock_update') + printServer.off('notify_printer_update') + printServer.off('notify_filamentstock_update') } } - }, [socket, initialized, printerId]) + }, [printServer, initialized, printerId]) function handleEmergencyStop() { console.log('Emergency stop button clicked') - socket.emit('printer.emergency_stop', { printerId }) + printServer.emit('printer.emergency_stop', { printerId }) } useEffect(() => { @@ -328,19 +328,19 @@ const ControlPrinter = () => { ], onClick: ({ key }) => { if (key === 'restartHost') { - socket.emit('printer.restart', { printerId }) + printServer.emit('printer.restart', { printerId }) } else if (key === 'restartFirmware') { - socket.emit('printer.firmware_restart', { printerId }) + printServer.emit('printer.firmware_restart', { printerId }) } else if (key === 'resumePrint') { - socket.emit('printer.print.resume', { printerId }) + printServer.emit('printer.print.resume', { printerId }) } else if (key === 'pausePrint') { - socket.emit('printer.print.pause', { printerId }) + printServer.emit('printer.print.pause', { printerId }) } else if (key === 'cancelPrint') { - socket.emit('printer.print.cancel', { printerId }) + printServer.emit('printer.print.cancel', { printerId }) } else if (key === 'startQueue') { - socket.emit('server.job_queue.start', { printerId }) + printServer.emit('server.job_queue.start', { printerId }) } else if (key === 'pauseQueue') { - socket.emit('server.job_queue.pause', { printerId }) + printServer.emit('server.job_queue.pause', { printerId }) } else if (key === 'loadFilamentStock') { setLoadFilamentStockModalOpen(true) } else if (key === 'unloadFilamentStock') { @@ -467,9 +467,9 @@ const ControlPrinter = () => { } onClick={() => { if (printerData?.state?.type === 'paused') { - socket.emit('printer.print.resume', { printerId }) + printServer.emit('printer.print.resume', { printerId }) } else { - socket.emit('printer.print.pause', { printerId }) + printServer.emit('printer.print.pause', { printerId }) } }} > @@ -482,7 +482,7 @@ const ControlPrinter = () => { printerData?.state?.type === 'error' } onClick={() => { - socket.emit('server.job_queue.start', { printerId }) + printServer.emit('server.job_queue.start', { printerId }) }} > @@ -933,7 +933,7 @@ const ControlPrinter = () => { key='firmwareRestart' icon={} onClick={() => { - socket.emit('printer.firmware_restart', { printerId }) + printServer.emit('printer.firmware_restart', { printerId }) setKlippyErrorModalOpen(false) }} > diff --git a/src/components/Dashboard/Production/Printers/NewPrinter.jsx b/src/components/Dashboard/Production/Printers/NewPrinter.jsx index e777570..a02cb19 100644 --- a/src/components/Dashboard/Production/Printers/NewPrinter.jsx +++ b/src/components/Dashboard/Production/Printers/NewPrinter.jsx @@ -21,7 +21,7 @@ import { } from 'antd' import { SearchOutlined, SettingOutlined } from '@ant-design/icons' import PropTypes from 'prop-types' -import { SocketContext } from '../../context/SocketContext' +import { PrintServerContext } from '../../context/PrintServerContext' import EditIcon from '../../../Icons/EditIcon.jsx' import config from '../../../../config.js' @@ -43,7 +43,7 @@ const NewPrinter = ({ onOk, reset }) => { reset: PropTypes.bool.isRequired } - const { socket } = useContext(SocketContext) + const { printServer } = useContext(PrintServerContext) const [messageApi, contextHolder] = message.useMessage() const [notificationApi, notificationContextHolder] = notification.useNotification() @@ -243,22 +243,22 @@ const NewPrinter = ({ onOk, reset }) => { setDiscovering(true) setDiscoveredPrinters([]) messageApi.info('Discovering printers...') - socket.off('notify_scan_network_found') - socket.off('notify_scan_network_progress') - socket.off('notify_scan_network_complete') + printServer.off('notify_scan_network_found') + printServer.off('notify_scan_network_progress') + printServer.off('notify_scan_network_complete') - socket.on('notify_scan_network_found', notifyScanNetworkFound) - socket.on('notify_scan_network_progress', notifyScanNetworkProgress) - socket.on('notify_scan_network_complete', notifyScanNetworkComplete) + printServer.on('notify_scan_network_found', notifyScanNetworkFound) + printServer.on('notify_scan_network_progress', notifyScanNetworkProgress) + printServer.on('notify_scan_network_complete', notifyScanNetworkComplete) - socket.emit('bridge.scan_network.start', { + printServer.emit('bridge.scan_network.start', { port: scanPort, protocol: scanProtocol }) } }, [ discovering, - socket, + printServer, scanPort, scanProtocol, messageApi, @@ -279,10 +279,10 @@ const NewPrinter = ({ onOk, reset }) => { setDiscovering(false) notificationApi.destroy('network-scan') messageApi.info('Stopping discovery...') - socket.off('notify_scan_network_found') - socket.off('notify_scan_network_progress') - socket.off('notify_scan_network_complete') - socket.emit('bridge.scan_network.stop', (response) => { + printServer.off('notify_scan_network_found') + printServer.off('notify_scan_network_progress') + printServer.off('notify_scan_network_complete') + printServer.emit('bridge.scan_network.stop', (response) => { if (response == false) { messageApi.error('Error stopping discovery!') } diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index 01db842..bee7906 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -46,7 +46,7 @@ const breadcrumbNameMap = { '/dashboard/inventory/stockaudits/info': 'Info', '/dashboard/developer/sessionstorage': 'Session Storage', '/dashboard/developer/authcontextdebug': 'Auth Context Debug', - '/dashboard/developer/socketcontextdebug': 'Socket Context Debug' + '/dashboard/developer/printservercontextdebug': 'Print Server Context Debug' } const DashboardBreadcrumb = () => { diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 19acece..3e3b8a5 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -8,9 +8,8 @@ import { Dropdown, Button, Tooltip, - Typography, - Divider, - Badge + Badge, + Divider } from 'antd' import { LogoutOutlined, @@ -19,12 +18,13 @@ import { LoadingOutlined } from '@ant-design/icons' import { AuthContext } from '../context/AuthContext' -import { SocketContext } from '../context/SocketContext' +import { PrintServerContext } from '../context/PrintServerContext' import { SpotlightContext } from '../context/SpotlightContext' -import { NotificationContext } from '../context/NotificationContext' +import { ApiServerContext } from '../context/ApiServerContext' import { useNavigate, useLocation } from 'react-router-dom' import { Header } from 'antd/es/layout/layout' import { useMediaQuery } from 'react-responsive' +import KeyboardShortcut from './KeyboardShortcut' import FarmControlLogo from '../../Logos/FarmControlLogo' import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall' @@ -36,16 +36,16 @@ import BellIcon from '../../Icons/BellIcon' import SearchIcon from '../../Icons/SearchIcon' import SettingsIcon from '../../Icons/SettingsIcon' import DeveloperIcon from '../../Icons/DeveloperIcon' - -const { Text } = Typography +import PrinterIcon from '../../Icons/PrinterIcon' const DashboardNavigation = () => { const { logout, userProfile } = useContext(AuthContext) const { showSpotlight } = useContext(SpotlightContext) - const { toggleNotificationCenter, unreadCount } = - useContext(NotificationContext) - const { socket } = useContext(SocketContext) - const [socketState, setSocketState] = useState('disconnected') + const { toggleNotificationCenter, unreadCount } = useContext(ApiServerContext) + const { printServer } = useContext(PrintServerContext) + const { apiServer } = useContext(ApiServerContext) + const [printServerState, setPrintServerState] = useState('disconnected') + const [apiServerState, setApiServerState] = useState('disconnected') const navigate = useNavigate() const location = useLocation() const [selectedKey, setSelectedKey] = useState('production') @@ -59,14 +59,24 @@ const DashboardNavigation = () => { }, [location.pathname]) useEffect(() => { - if (socket?.connecting) { - setSocketState('connecting') - } else if (socket?.connected) { - setSocketState('connected') + if (printServer?.connecting) { + setPrintServerState('connecting') + } else if (printServer?.connected) { + setPrintServerState('connected') } else { - setSocketState('disconnected') + setPrintServerState('disconnected') } - }, [socket?.connecting, socket?.connected]) + }, [printServer?.connecting, printServer?.connected]) + + useEffect(() => { + if (apiServer?.connecting) { + setApiServerState('connecting') + } else if (apiServer?.connected) { + setApiServerState('connected') + } else { + setApiServerState('disconnected') + } + }, [apiServer?.connecting, apiServer?.connected]) const mainMenuItems = [ { @@ -165,54 +175,90 @@ const DashboardNavigation = () => { /> - ⌘ ⇧ P} arrow={false}> + showSpotlight()} + > - + /> + - + toggleNotificationCenter()} + > + + ]} + > + {errorModalContent} + + + ) +} + +ApiServerProvider.propTypes = { + children: PropTypes.node.isRequired +} + +export { ApiServerContext, ApiServerProvider } diff --git a/src/components/Dashboard/context/NotificationContext.js b/src/components/Dashboard/context/NotificationContext.js deleted file mode 100644 index 6a8cf70..0000000 --- a/src/components/Dashboard/context/NotificationContext.js +++ /dev/null @@ -1,212 +0,0 @@ -// src/contexts/NotificationContext.js -import React, { - createContext, - useEffect, - useState, - useContext, - useRef -} from 'react' -import io from 'socket.io-client' -import { message, notification, Drawer } from 'antd' -import PropTypes from 'prop-types' -import { AuthContext } from './AuthContext' -import config from '../../../config' -import NotificationCenter from '../common/NotificationCenter' - -const NotificationContext = createContext() - -const NotificationProvider = ({ children }) => { - const { token } = useContext(AuthContext) - const socketRef = useRef(null) - const [connecting, setConnecting] = useState(false) - const [error, setError] = useState(null) - const [notificationVisible, setNotificationVisible] = useState(false) - const [notifications, setNotifications] = useState([]) - const [messageApi, contextHolder] = message.useMessage() - const [notificationApi] = notification.useNotification() - - useEffect(() => { - if (token) { - console.log( - 'Token is available, connecting to notification web socket server...' - ) - - const newSocket = io( - config.notificationWsUrl || `${config.wsUrl}/notifications`, - { - reconnectionAttempts: 3, - timeout: 3000, - auth: { token: token } - } - ) - - setConnecting(true) - - newSocket.on('connect', () => { - console.log('Notification socket connected') - setConnecting(false) - setError(null) - }) - - newSocket.on('disconnect', () => { - console.log('Notification socket disconnected') - setError('Notification socket disconnected') - }) - - newSocket.on('connect_error', (err) => { - console.error('Notification socket connection error:', err) - messageApi.error('Notification socket connection error: ' + err.message) - setError('Notification socket connection error') - }) - - newSocket.on('notification.new', (data) => { - console.log('New notification received:', data) - // Add new notification to state - setNotifications((prev) => [data, ...prev]) - - // Show toast notification - notificationApi.info({ - message: data.title || 'New Notification', - description: data.message, - placement: 'topRight', - duration: 4.5 - }) - }) - - newSocket.on('notification.update', (data) => { - console.log('Notification updated:', data) - setNotifications((prev) => - prev.map((notification) => { - if (notification._id === data._id) { - return { ...notification, ...data } - } - return notification - }) - ) - }) - - newSocket.on('notification.delete', (data) => { - console.log('Notification deleted:', data) - setNotifications((prev) => - prev.filter((notification) => notification._id !== data._id) - ) - }) - - newSocket.on('error', (err) => { - console.error('Notification socket error:', err) - setError('Notification socket error') - }) - - socketRef.current = newSocket - - // Clean up function - return () => { - if (socketRef.current) { - console.log('Cleaning up notification socket connection...') - socketRef.current.disconnect() - socketRef.current = null - } - } - } else if (!token && socketRef.current) { - console.log('Token not available, disconnecting notification socket...') - socketRef.current.disconnect() - socketRef.current = null - } - }, [token, messageApi, notificationApi]) - - const showNotificationCenter = () => { - setNotificationVisible(true) - } - - const hideNotificationCenter = () => { - setNotificationVisible(false) - } - - const toggleNotificationCenter = () => { - setNotificationVisible((prev) => !prev) - } - - const updateNotifications = (newNotifications) => { - setNotifications(newNotifications) - } - - const addNotification = (notification) => { - setNotifications((prev) => [notification, ...prev]) - } - - const removeNotification = (notificationId) => { - setNotifications((prev) => - prev.filter((notification) => notification._id !== notificationId) - ) - } - - const markNotificationAsRead = (notificationId) => { - setNotifications((prev) => - prev.map((notification) => { - if (notification._id === notificationId) { - return { ...notification, read: true } - } - return notification - }) - ) - } - - const markAllNotificationsAsRead = () => { - setNotifications((prev) => - prev.map((notification) => ({ ...notification, read: true })) - ) - } - - const clearAllNotifications = () => { - setNotifications([]) - } - - const getUnreadCount = () => { - return notifications.filter((notification) => !notification.read).length - } - - return ( - - {contextHolder} - {children} - - {/* Notification Drawer */} - - - - - ) -} - -NotificationProvider.propTypes = { - children: PropTypes.node.isRequired -} - -export { NotificationContext, NotificationProvider } diff --git a/src/components/Dashboard/context/SocketContext.js b/src/components/Dashboard/context/PrintServerContext.js similarity index 59% rename from src/components/Dashboard/context/SocketContext.js rename to src/components/Dashboard/context/PrintServerContext.js index e6c603d..160a1d1 100644 --- a/src/components/Dashboard/context/SocketContext.js +++ b/src/components/Dashboard/context/PrintServerContext.js @@ -1,4 +1,4 @@ -// src/contexts/SocketContext.js +// src/contexts/PrintServerContext.js import React, { createContext, useEffect, @@ -11,10 +11,13 @@ import { message, notification } from 'antd' import PropTypes from 'prop-types' import { AuthContext } from './AuthContext' import config from '../../../config' +import loglevel from 'loglevel' +const log = loglevel.getLogger('Print Server') +log.setLevel(config.logLevel) -const SocketContext = createContext() +const PrintServerContext = createContext() -const SocketProvider = ({ children }) => { +const PrintServerProvider = ({ children }) => { const { token } = useContext(AuthContext) const socketRef = useRef(null) const [connecting, setConnecting] = useState(false) @@ -24,9 +27,9 @@ const SocketProvider = ({ children }) => { useEffect(() => { if (token) { - console.log('Token is available, connecting to web socket server...') + log.debug('Token is available, connecting to print server...') - const newSocket = io(config.wsUrl, { + const newSocket = io(config.printServerUrl, { reconnectionAttempts: 3, timeout: 3000, auth: { token: token } @@ -35,20 +38,20 @@ const SocketProvider = ({ children }) => { setConnecting(true) newSocket.on('connect', () => { - console.log('Socket connected') + log.debug('Print server connected') setConnecting(false) setError(null) }) newSocket.on('disconnect', () => { - console.log('Socket disconnected') - setError('Socket disconnected') + log.debug('Print server disconnected') + setError('Print server disconnected') }) newSocket.on('connect_error', (err) => { - console.error('Socket connection error:', err) - messageApi.error('Socket connection error: ' + err.message) - setError('Socket connection error') + log.error('Print server connection error:', err) + messageApi.error('Print server connection error: ' + err.message) + setError('Print server connection error') }) newSocket.on('bridge.notification', (data) => { @@ -59,8 +62,8 @@ const SocketProvider = ({ children }) => { }) newSocket.on('error', (err) => { - console.error('Socket error:', err) - setError('Socket error') + log.error('Print server error:', err) + setError('Print server error') }) socketRef.current = newSocket @@ -68,30 +71,30 @@ const SocketProvider = ({ children }) => { // Clean up function return () => { if (socketRef.current) { - console.log('Cleaning up socket connection...') + log.debug('Cleaning up socket connection...') socketRef.current.disconnect() socketRef.current = null } } } else if (!token && socketRef.current) { - console.log('Token not available, disconnecting socket...') + log.debug('Token not available, disconnecting socket...') socketRef.current.disconnect() socketRef.current = null } }, [token, messageApi]) return ( - {contextHolder} {children} - + ) } -SocketProvider.propTypes = { +PrintServerProvider.propTypes = { children: PropTypes.node.isRequired } -export { SocketContext, SocketProvider } +export { PrintServerContext, PrintServerProvider } diff --git a/src/components/Dashboard/context/SpotlightContext.js b/src/components/Dashboard/context/SpotlightContext.js index 849e552..d53090b 100644 --- a/src/components/Dashboard/context/SpotlightContext.js +++ b/src/components/Dashboard/context/SpotlightContext.js @@ -345,24 +345,6 @@ const SpotlightProvider = ({ children }) => { } } - // Add keyboard shortcut listener - useEffect(() => { - const handleKeyPress = (e) => { - if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'p') { - e.preventDefault() // Prevent browser's default behavior - showSpotlight() - } - } - - // Add event listener - window.addEventListener('keydown', handleKeyPress) - - // Clean up - return () => { - window.removeEventListener('keydown', handleKeyPress) - } - }, []) - // Focus and select text in input when modal becomes visible useEffect(() => { if (showModal && inputRef.current) { diff --git a/src/components/Icons/LockIcon.jsx b/src/components/Icons/LockIcon.jsx new file mode 100644 index 0000000..be0660c --- /dev/null +++ b/src/components/Icons/LockIcon.jsx @@ -0,0 +1,7 @@ +import React from 'react' +import Icon from '@ant-design/icons' +import { ReactComponent as CustomIconSvg } from '../../assets/icons/lockicon.min.svg' + +const LockIcon = (props) => + +export default LockIcon diff --git a/src/config.js b/src/config.js index fec254e..fa55645 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,15 @@ const config = { development: { backendUrl: 'http://192.168.68.53:8080', - wsUrl: 'ws://192.168.68.53:8081' + printServerUrl: 'ws://192.168.68.53:8081', + apiServerUrl: 'ws://192.168.68.53:9090', + logLevel: 'trace' }, production: { - backendUrl: 'http://192.168.68.53:8080', // Replace with your production backend URL - wsUrl: 'http://192.168.68.53:8081' // Replace with your production WebSocket URL + backendUrl: 'http://192.168.68.53:8080', + printServerUrl: 'ws://192.168.68.53:8081', + apiServerUrl: 'ws://192.168.68.53:9090', + logLevel: 'error' } }