import { useState, useContext, useCallback, useEffect } from 'react' import axios from 'axios' import { useLocation } from 'react-router-dom' import { useMediaQuery } from 'react-responsive' import { Button, message, Spin, Flex, Card, Dropdown, Space, Descriptions, Progress, Modal, Typography, Badge, Alert, Popover, Checkbox, Collapse } from 'antd' import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons' import { PrintServerContext } from '../../context/PrintServerContext' import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel' import PrinterPositionPanel from '../../common/PrinterPositionPanel' import PrinterMovementPanel from '../../common/PrinterMovementPanel' import PrinterMiscPanel from '../../common/PrinterMiscPanel' import PrinterState from '../../common/StateDisplay' import { AuthContext } from '../../context/AuthContext' import PrinterSubJobsTree from '../../common/PrinterJobsTree' import IdDisplay from '../../common/IdDisplay' import FilamentIcon from '../../../Icons/FilamentIcon' import FilamentStockIcon from '../../../Icons/FilamentStockIcon' import ReloadIcon from '../../../Icons/ReloadIcon' import GCodeFileIcon from '../../../Icons/GCodeFileIcon' import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock' import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock' import FilamentStockState from '../../common/FilamentStockState' import useCollapseState from '../../hooks/useCollapseState' import config from '../../../../config' import PlayCircleIcon from '../../../Icons/PlayCircleIcon' import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' import PauseCircleIcon from '../../../Icons/PauseCircleIcon' import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon' import TimeDisplay from '../../common/TimeDisplay' const { Text, Title } = Typography // Helper function to parse query parameters const useQuery = () => { return new URLSearchParams(useLocation().search) } const ControlPrinter = () => { const [messageApi] = message.useMessage() const query = useQuery() const printerId = query.get('printerId') const isMobile = useMediaQuery({ maxWidth: 768 }) const [printerData, setPrinterData] = useState(null) const [fetchLoading, setFetchLoading] = useState(true) const [initialized, setInitialized] = useState(false) const [loadFilamentStockModalOpen, setLoadFilamentStockModalOpen] = useState(false) const [unloadFilamentStockModalOpen, setUnloadFilamentStockModalOpen] = useState(false) const [klippyErrorModalOpen, setKlippyErrorModalOpen] = useState(false) const [klippyErrorMessage, setKlippyErrorMessage] = useState('') const [klippyStartupMessage, setKlippyStartupMessage] = useState('') const [collapseState, updateCollapseState] = useCollapseState( 'ControlPrinter', { job: true, filament: true, gcodefile: true, jobs: true } ) // Load visibility preferences from sessionStorage on component mount const [componentVisibility, setComponentVisibility] = useState(() => { const savedVisibility = sessionStorage.getItem('printerControlVisibility') if (savedVisibility) { return JSON.parse(savedVisibility) } return { temperature: true, movement: true, subjobs: true } }) // Save visibility preferences to sessionStorage whenever they change useEffect(() => { sessionStorage.setItem( 'printerControlVisibility', JSON.stringify(componentVisibility) ) }, [componentVisibility]) const { printServer } = useContext(PrintServerContext) const { authenticated } = useContext(AuthContext) // Fetch printer details when the component mounts const fetchPrinterDetails = useCallback(async () => { if (printerId) { setFetchLoading(true) try { const response = await axios.get( `${config.backendUrl}/printers/${printerId}`, { headers: { Accept: 'application/json' }, withCredentials: true // Important for including cookies } ) setFetchLoading(false) setPrinterData(response.data) } catch (error) { setFetchLoading(false) if (error.response) { messageApi.error( 'Error fetching printer data:', error.response.status ) } else { messageApi.error( 'An unexpected error occurred. Please try again later.' ) } } } }, [printerId, messageApi]) // Add WebSocket event listener for real-time updates useEffect(() => { if (printServer && !initialized && printerId) { setInitialized(true) printServer.on('notify_printer_update', (statusUpdate) => { setPrinterData((prevData) => { if (statusUpdate?._id === printerId) { return { ...prevData, ...statusUpdate } } return prevData }) }) // Add WebSocket event listener for filament stock updates printServer.on('notify_filamentstock_update', (filamentStockUpdate) => { setPrinterData((prevData) => { if (prevData?.currentFilamentStock) { if ( prevData?.currentFilamentStock?._id === filamentStockUpdate?._id ) { return { ...prevData, currentFilamentStock: { ...prevData.currentFilamentStock, ...filamentStockUpdate } } } } return prevData }) }) } return () => { if (printServer && initialized) { printServer.off('notify_printer_update') printServer.off('notify_filamentstock_update') } } }, [printServer, initialized, printerId]) function handleEmergencyStop() { printServer.emit('printer.emergency_stop', { printerId }) } useEffect(() => { if (authenticated) { fetchPrinterDetails() } }, [authenticated, fetchPrinterDetails]) useEffect(() => { const loadFilamentStock = printerData?.alerts?.find( (alert) => alert.type === 'loadFilamentStock' ) const klippyError = printerData?.alerts?.find( (alert) => alert.type === 'klippyError' ) const klippyStartup = printerData?.alerts?.find( (alert) => alert.type === 'klippyStartup' ) if (loadFilamentStock) { setLoadFilamentStockModalOpen(true) } else { setLoadFilamentStockModalOpen(false) } if (klippyError) { setKlippyErrorModalOpen(true) setKlippyErrorMessage(klippyError.message) } else { setKlippyErrorModalOpen(false) setKlippyErrorMessage('') } if (klippyStartup) { setKlippyStartupMessage(klippyStartup.message) } else { setKlippyStartupMessage('') } }, [printerData?.alerts]) const actionItems = { items: [ { label: 'Resume Print', key: 'resumePrint', icon: , disabled: printerData?.state?.type !== 'paused' }, { label: 'Pause Print', key: 'pausePrint', icon: , disabled: printerData?.state?.type !== 'printing' }, { label: 'Cancel Print', key: 'cancelPrint', icon: , disabled: !( printerData?.state?.type === 'printing' || printerData?.state?.type === 'paused' ) }, { type: 'divider' }, { label: 'Queue', key: 'queue', children: [ { label: 'Start Queue', key: 'startQueue', disabled: printerData?.state?.type === 'printing' || printerData?.state?.type === 'deploying' || printerData?.state?.type === 'paused' || printerData?.state?.type === 'error', icon: }, { label: 'Pause Queue', key: 'pauseQueue', icon: } ] }, { label: 'Filament', key: 'filament', children: [ { label: 'Load Filament Stock', key: 'loadFilamentStock', icon: , disabled: printerData?.state?.type === 'printing' || printerData?.state?.type === 'error' || printerData?.state?.type === 'offline' || printerData?.currentFilamentStock !== null }, { label: 'Unload Filament Stock', key: 'unloadFilamentStock', icon: , disabled: printerData?.state?.type === 'printing' || printerData?.state?.type === 'error' || printerData?.state?.type === 'offline' || printerData?.currentFilamentStock === null }, { type: 'divider' }, { label: 'Filament Info', key: 'filamentInfo', icon: } ] }, { type: 'divider' }, { label: 'Restart Host', key: 'restartHost', icon: }, { label: 'Restart Firmware', key: 'restartFirmware', icon: } ], onClick: ({ key }) => { if (key === 'restartHost') { printServer.emit('printer.restart', { printerId }) } else if (key === 'restartFirmware') { printServer.emit('printer.firmware_restart', { printerId }) } else if (key === 'resumePrint') { printServer.emit('printer.print.resume', { printerId }) } else if (key === 'pausePrint') { printServer.emit('printer.print.pause', { printerId }) } else if (key === 'cancelPrint') { printServer.emit('printer.print.cancel', { printerId }) } else if (key === 'startQueue') { printServer.emit('server.job_queue.start', { printerId }) } else if (key === 'pauseQueue') { printServer.emit('server.job_queue.pause', { printerId }) } else if (key === 'loadFilamentStock') { setLoadFilamentStockModalOpen(true) } else if (key === 'unloadFilamentStock') { setUnloadFilamentStockModalOpen(true) } } } const getViewDropdownItems = () => { return ( { setComponentVisibility((prev) => ({ ...prev, temperature: e.target.checked })) }} > Temperature Panel { setComponentVisibility((prev) => ({ ...prev, position: e.target.checked })) }} > Position Panel { setComponentVisibility((prev) => ({ ...prev, movement: e.target.checked })) }} > Movement Panel { setComponentVisibility((prev) => ({ ...prev, subjobs: e.target.checked })) }} > Sub Jobs { setComponentVisibility((prev) => ({ ...prev, misc: e.target.checked })) }} > Misc Panel ) } return ( <> {printerData ? ( ) : ( } size='small' /> )}
{printerData?.alerts?.some( (alert) => alert.type === 'klippyError' ) && } {printerData?.alerts?.some( (alert) => alert.type === 'klippyStartup' ) && } updateCollapseState('job', keys.length > 0)} expandIcon={({ isActive }) => ( )} style={{ padding: 0 }} className='no-h-padding-collapse no-t-padding-collapse' > Current Job } key='1' > } spinning={fetchLoading} > {printerData?.name ? ( {printerData.name} ) : ( n/a )} {printerData?._id ? ( ) : ( n/a )} {printerData?.currentJob?.gcodeFile?.name ? ( {printerData.currentJob?.gcodeFile?.name} ) : ( n/a )} {printerData?.currentJob?.gcodeFile ? ( ) : ( n/a )} {printerData?.currentJob?.id ? ( ) : ( n/a )} {printerData?.currentSubJob?.id ? ( ) : ( n/a )} {printerData?.state?.type === 'printing' && ( <> {printerData?.currentSubJob?.startedAt ? ( ) : ( n/a )} )} {printerData?.currentJob?.gcodeFile?.gcodeFileInfo ?.printSettingsId ? ( {printerData.currentJob.gcodeFile.gcodeFileInfo.printSettingsId.replaceAll( '"', '' )} ) : ( n/a )} {printerData?.currentJob?.gcodeFile?.gcodeFileInfo ?.estimatedPrintingTimeNormalMode ? ( { printerData.currentJob.gcodeFile.gcodeFileInfo .estimatedPrintingTimeNormalMode } ) : ( n/a )} updateCollapseState('filament', keys.length > 0) } expandIcon={({ isActive }) => ( )} style={{ padding: 0 }} className='no-h-padding-collapse' > Loaded Filament Stock } key='1' > } spinning={fetchLoading} > {printerData?.currentFilamentStock ? ( ) : ( n/a )} {printerData?.currentFilamentStock?._id ? ( ) : ( n/a )} {printerData?.currentFilamentStock?.filament?.name ? ( ) : ( n/a )} {printerData?.currentFilamentStock?.filament ? ( ) : ( n/a )} {printerData?.currentFilamentStock?.currentNetWeight ? (
{printerData.currentFilamentStock.currentNetWeight.toFixed( 2 ) + 'g'} {printerData.currentFilamentStock.currentGrossWeight.toFixed( 2 ) + 'g'}
) : ( n/a )}
updateCollapseState('jobs', keys.length > 0) } expandIcon={({ isActive }) => ( )} style={{ padding: 0 }} className='no-h-padding-collapse' > Printer Jobs } key='1' > {componentVisibility.temperature && ( } spinning={fetchLoading} > )} {componentVisibility.position && ( } spinning={fetchLoading} > )} {componentVisibility.movement && ( } spinning={fetchLoading} > )} {componentVisibility.misc && ( } spinning={fetchLoading} > )}
{ setLoadFilamentStockModalOpen(false) }} > { setLoadFilamentStockModalOpen(false) messageApi.success('New print job created successfully.') }} isFilamentLoaded={false} printer={printerData} reset={loadFilamentStockModalOpen} /> { setUnloadFilamentStockModalOpen(false) }} > { setUnloadFilamentStockModalOpen(false) messageApi.success('Filament unloaded successfully.') }} printer={printerData} reset={unloadFilamentStockModalOpen} /> Klipper Error } onCancel={() => setKlippyErrorModalOpen(false)} footer={[ , ]} > {klippyErrorMessage} ) } export default ControlPrinter