949 lines
31 KiB
JavaScript
949 lines
31 KiB
JavaScript
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: <PlayCircleIcon />,
|
|
disabled: printerData?.state?.type !== 'paused'
|
|
},
|
|
{
|
|
label: 'Pause Print',
|
|
key: 'pausePrint',
|
|
icon: <PauseCircleIcon />,
|
|
disabled: printerData?.state?.type !== 'printing'
|
|
},
|
|
{
|
|
label: 'Cancel Print',
|
|
key: 'cancelPrint',
|
|
icon: <XMarkCircleIcon />,
|
|
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: <PlayCircleIcon />
|
|
},
|
|
{
|
|
label: 'Pause Queue',
|
|
key: 'pauseQueue',
|
|
icon: <PauseCircleIcon />
|
|
}
|
|
]
|
|
},
|
|
{
|
|
label: 'Filament',
|
|
key: 'filament',
|
|
children: [
|
|
{
|
|
label: 'Load Filament Stock',
|
|
key: 'loadFilamentStock',
|
|
icon: <FilamentStockIcon />,
|
|
disabled:
|
|
printerData?.state?.type === 'printing' ||
|
|
printerData?.state?.type === 'error' ||
|
|
printerData?.state?.type === 'offline' ||
|
|
printerData?.currentFilamentStock !== null
|
|
},
|
|
{
|
|
label: 'Unload Filament Stock',
|
|
key: 'unloadFilamentStock',
|
|
icon: <FilamentStockIcon />,
|
|
disabled:
|
|
printerData?.state?.type === 'printing' ||
|
|
printerData?.state?.type === 'error' ||
|
|
printerData?.state?.type === 'offline' ||
|
|
printerData?.currentFilamentStock === null
|
|
},
|
|
{
|
|
type: 'divider'
|
|
},
|
|
{
|
|
label: 'Filament Info',
|
|
key: 'filamentInfo',
|
|
icon: <FilamentIcon />
|
|
}
|
|
]
|
|
},
|
|
|
|
{
|
|
type: 'divider'
|
|
},
|
|
{
|
|
label: 'Restart Host',
|
|
key: 'restartHost',
|
|
icon: <ReloadIcon />
|
|
},
|
|
{
|
|
label: 'Restart Firmware',
|
|
key: 'restartFirmware',
|
|
icon: <ReloadIcon />
|
|
}
|
|
],
|
|
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 (
|
|
<Flex vertical>
|
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
<Checkbox
|
|
checked={componentVisibility.temperature}
|
|
onChange={(e) => {
|
|
setComponentVisibility((prev) => ({
|
|
...prev,
|
|
temperature: e.target.checked
|
|
}))
|
|
}}
|
|
>
|
|
Temperature Panel
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={componentVisibility.position}
|
|
onChange={(e) => {
|
|
setComponentVisibility((prev) => ({
|
|
...prev,
|
|
position: e.target.checked
|
|
}))
|
|
}}
|
|
>
|
|
Position Panel
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={componentVisibility.movement}
|
|
onChange={(e) => {
|
|
setComponentVisibility((prev) => ({
|
|
...prev,
|
|
movement: e.target.checked
|
|
}))
|
|
}}
|
|
>
|
|
Movement Panel
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={componentVisibility.subjobs}
|
|
onChange={(e) => {
|
|
setComponentVisibility((prev) => ({
|
|
...prev,
|
|
subjobs: e.target.checked
|
|
}))
|
|
}}
|
|
>
|
|
Sub Jobs
|
|
</Checkbox>
|
|
<Checkbox
|
|
checked={componentVisibility.misc}
|
|
onChange={(e) => {
|
|
setComponentVisibility((prev) => ({
|
|
...prev,
|
|
misc: e.target.checked
|
|
}))
|
|
}}
|
|
>
|
|
Misc Panel
|
|
</Checkbox>
|
|
</Flex>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Flex
|
|
gap='large'
|
|
vertical='true'
|
|
style={{ height: '100%', minHeight: 0 }}
|
|
>
|
|
<Flex justify={'space-between'}>
|
|
<Space size='middle'>
|
|
<Space size='small'>
|
|
<Dropdown menu={actionItems}>
|
|
<Button>Actions</Button>
|
|
</Dropdown>
|
|
<Popover
|
|
content={getViewDropdownItems()}
|
|
placement='bottomLeft'
|
|
arrow={false}
|
|
>
|
|
<Button>View</Button>
|
|
</Popover>
|
|
</Space>
|
|
{printerData ? (
|
|
<PrinterState
|
|
printer={printerData}
|
|
showProgress={false}
|
|
showName={false}
|
|
showControls={false}
|
|
/>
|
|
) : (
|
|
<Spin indicator={<LoadingOutlined spin />} size='small' />
|
|
)}
|
|
</Space>
|
|
<Space size='small'>
|
|
<Button
|
|
icon={<ExclamationOctagonIcon />}
|
|
danger
|
|
onClick={handleEmergencyStop}
|
|
></Button>
|
|
<Button
|
|
icon={
|
|
printerData?.state?.type === 'paused' ? (
|
|
<PlayCircleIcon />
|
|
) : (
|
|
<PauseCircleIcon />
|
|
)
|
|
}
|
|
disabled={
|
|
!(
|
|
printerData?.state?.type == 'printing' ||
|
|
printerData?.state?.type == 'paused'
|
|
)
|
|
}
|
|
onClick={() => {
|
|
if (printerData?.state?.type === 'paused') {
|
|
printServer.emit('printer.print.resume', { printerId })
|
|
} else {
|
|
printServer.emit('printer.print.pause', { printerId })
|
|
}
|
|
}}
|
|
></Button>
|
|
<Button
|
|
icon={<PlayCircleIcon />}
|
|
disabled={
|
|
printerData?.state?.type === 'printing' ||
|
|
printerData?.state?.type === 'deploying' ||
|
|
printerData?.state?.type === 'paused' ||
|
|
printerData?.state?.type === 'error'
|
|
}
|
|
onClick={() => {
|
|
printServer.emit('server.job_queue.start', { printerId })
|
|
}}
|
|
></Button>
|
|
</Space>
|
|
</Flex>
|
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
<Flex gap={'large'} wrap>
|
|
<Flex vertical style={{ flexGrow: 1 }} gap={'large'}>
|
|
<Flex gap={16} vertical style={{ flexGrow: 1 }}>
|
|
{printerData?.alerts?.some(
|
|
(alert) => alert.type === 'klippyError'
|
|
) && <Alert message={klippyErrorMessage} type='error' />}
|
|
{printerData?.alerts?.some(
|
|
(alert) => alert.type === 'klippyStartup'
|
|
) && <Alert message={klippyStartupMessage} type='warning' />}
|
|
</Flex>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.job ? ['1'] : []}
|
|
onChange={(keys) => updateCollapseState('job', keys.length > 0)}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
style={{ padding: 0 }}
|
|
className='no-h-padding-collapse no-t-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex
|
|
align='center'
|
|
justify='space-between'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Current Job
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='1'
|
|
>
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Descriptions
|
|
bordered
|
|
column={{
|
|
xs: 1,
|
|
sm: 1,
|
|
md: 1,
|
|
lg: 1,
|
|
xl: 2,
|
|
xxl: 2
|
|
}}
|
|
>
|
|
<Descriptions.Item label='Printer Name'>
|
|
{printerData?.name ? (
|
|
<Text>{printerData.name}</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Printer ID'>
|
|
{printerData?._id ? (
|
|
<IdDisplay
|
|
id={printerData._id}
|
|
type='printer'
|
|
longId={false}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='GCode File Name'>
|
|
{printerData?.currentJob?.gcodeFile?.name ? (
|
|
<Space>
|
|
<GCodeFileIcon />
|
|
<Text ellipsis style={{ maxWidth: 200 }}>
|
|
{printerData.currentJob?.gcodeFile?.name}
|
|
</Text>
|
|
</Space>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='GCode File ID'>
|
|
{printerData?.currentJob?.gcodeFile ? (
|
|
<IdDisplay
|
|
id={printerData.currentJob.gcodeFile.id}
|
|
type='gcodeFile'
|
|
longId={false}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Print Job ID'>
|
|
{printerData?.currentJob?.id ? (
|
|
<IdDisplay
|
|
id={printerData.currentJob.id}
|
|
type='job'
|
|
longId={false}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Sub Job ID'>
|
|
{printerData?.currentSubJob?.id ? (
|
|
<IdDisplay
|
|
id={printerData.currentSubJob.number
|
|
.toString()
|
|
.padStart(6, '0')}
|
|
type='subjob'
|
|
longId={false}
|
|
showHyperlink={false}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
{printerData?.state?.type === 'printing' && (
|
|
<>
|
|
<Descriptions.Item label='Progress' span={1}>
|
|
<Progress
|
|
percent={Math.round(
|
|
(printerData.state.progress || 0) * 100
|
|
)}
|
|
status='active'
|
|
/>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Started At' span={1}>
|
|
{printerData?.currentSubJob?.startedAt ? (
|
|
<TimeDisplay
|
|
dateTime={printerData.currentSubJob.startedAt}
|
|
showSince={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
</>
|
|
)}
|
|
|
|
<Descriptions.Item label='Print Profile'>
|
|
{printerData?.currentJob?.gcodeFile?.gcodeFileInfo
|
|
?.printSettingsId ? (
|
|
<Text ellipsis style={{ maxWidth: 200 }}>
|
|
{printerData.currentJob.gcodeFile.gcodeFileInfo.printSettingsId.replaceAll(
|
|
'"',
|
|
''
|
|
)}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Est. Print Time'>
|
|
{printerData?.currentJob?.gcodeFile?.gcodeFileInfo
|
|
?.estimatedPrintingTimeNormalMode ? (
|
|
<Text ellipsis>
|
|
{
|
|
printerData.currentJob.gcodeFile.gcodeFileInfo
|
|
.estimatedPrintingTimeNormalMode
|
|
}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Spin>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.filament ? ['1'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('filament', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
style={{ padding: 0 }}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex
|
|
align='center'
|
|
justify='space-between'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Loaded Filament Stock
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='1'
|
|
>
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Descriptions
|
|
bordered
|
|
column={{
|
|
xs: 1,
|
|
sm: 1,
|
|
md: 1,
|
|
lg: 1,
|
|
xl: 2,
|
|
xxl: 2
|
|
}}
|
|
>
|
|
<Descriptions.Item label='Filament Stock' span={1}>
|
|
{printerData?.currentFilamentStock ? (
|
|
<FilamentStockState
|
|
filamentStock={printerData?.currentFilamentStock}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Stock ID'>
|
|
{printerData?.currentFilamentStock?._id ? (
|
|
<IdDisplay
|
|
id={printerData.currentFilamentStock._id}
|
|
type='filamentstock'
|
|
longId={false}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Name'>
|
|
{printerData?.currentFilamentStock?.filament?.name ? (
|
|
<Space>
|
|
<FilamentIcon />
|
|
<Badge
|
|
text={
|
|
printerData.currentFilamentStock.filament.name
|
|
}
|
|
color={
|
|
printerData.currentFilamentStock.filament.color
|
|
}
|
|
></Badge>
|
|
</Space>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament ID'>
|
|
{printerData?.currentFilamentStock?.filament ? (
|
|
<IdDisplay
|
|
id={printerData.currentFilamentStock.filament._id}
|
|
type='filament'
|
|
longId={false}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Weight'>
|
|
{printerData?.currentFilamentStock?.currentNetWeight ? (
|
|
<div>
|
|
<Descriptions
|
|
style={{ width: isMobile ? '100%' : '250px' }}
|
|
column={2}
|
|
size={'small'}
|
|
>
|
|
<Descriptions.Item label='Net'>
|
|
{printerData.currentFilamentStock.currentNetWeight.toFixed(
|
|
2
|
|
) + 'g'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Gross'>
|
|
{printerData.currentFilamentStock.currentGrossWeight.toFixed(
|
|
2
|
|
) + 'g'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</div>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Spin>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.jobs ? ['1'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('jobs', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
style={{ padding: 0 }}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex
|
|
align='center'
|
|
justify='space-between'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Printer Jobs
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='1'
|
|
>
|
|
<PrinterSubJobsTree
|
|
subJobs={printerData?.subJobs}
|
|
loading={fetchLoading}
|
|
/>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</Flex>
|
|
<Flex gap={'large'} wrap vertical>
|
|
{componentVisibility.temperature && (
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Card>
|
|
<PrinterTemperaturePanel
|
|
printerId={printerId}
|
|
></PrinterTemperaturePanel>
|
|
</Card>
|
|
</Spin>
|
|
)}
|
|
|
|
{componentVisibility.position && (
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Card>
|
|
<PrinterPositionPanel
|
|
printerId={printerId}
|
|
showControls={true}
|
|
showMoreInfo={true}
|
|
/>
|
|
</Card>
|
|
</Spin>
|
|
)}
|
|
|
|
{componentVisibility.movement && (
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Card>
|
|
<PrinterMovementPanel
|
|
printerId={printerId}
|
|
></PrinterMovementPanel>
|
|
</Card>
|
|
</Spin>
|
|
)}
|
|
|
|
{componentVisibility.misc && (
|
|
<Spin
|
|
indicator={<LoadingOutlined spin />}
|
|
spinning={fetchLoading}
|
|
>
|
|
<Card>
|
|
<PrinterMiscPanel printerId={printerId} />
|
|
</Card>
|
|
</Spin>
|
|
)}
|
|
</Flex>
|
|
</Flex>
|
|
</div>
|
|
</Flex>
|
|
<Modal
|
|
open={loadFilamentStockModalOpen}
|
|
footer={null}
|
|
width={700}
|
|
onCancel={() => {
|
|
setLoadFilamentStockModalOpen(false)
|
|
}}
|
|
>
|
|
<LoadFilamentStock
|
|
onOk={() => {
|
|
setLoadFilamentStockModalOpen(false)
|
|
messageApi.success('New print job created successfully.')
|
|
}}
|
|
isFilamentLoaded={false}
|
|
printer={printerData}
|
|
reset={loadFilamentStockModalOpen}
|
|
/>
|
|
</Modal>
|
|
<Modal
|
|
open={unloadFilamentStockModalOpen}
|
|
footer={null}
|
|
width={700}
|
|
onCancel={() => {
|
|
setUnloadFilamentStockModalOpen(false)
|
|
}}
|
|
>
|
|
<UnloadFilamentStock
|
|
onOk={() => {
|
|
setUnloadFilamentStockModalOpen(false)
|
|
messageApi.success('Filament unloaded successfully.')
|
|
}}
|
|
printer={printerData}
|
|
reset={unloadFilamentStockModalOpen}
|
|
/>
|
|
</Modal>
|
|
<Modal
|
|
open={klippyErrorModalOpen}
|
|
title={
|
|
<Space size={'middle'}>
|
|
<ExclamationOctagonIcon />
|
|
Klipper Error
|
|
</Space>
|
|
}
|
|
onCancel={() => setKlippyErrorModalOpen(false)}
|
|
footer={[
|
|
<Button
|
|
key='close'
|
|
onClick={() => {
|
|
setKlippyErrorModalOpen(false)
|
|
}}
|
|
>
|
|
Close
|
|
</Button>,
|
|
<Button
|
|
key='firmwareRestart'
|
|
icon={<ReloadIcon />}
|
|
onClick={() => {
|
|
printServer.emit('printer.firmware_restart', { printerId })
|
|
setKlippyErrorModalOpen(false)
|
|
}}
|
|
>
|
|
Restart Firmware
|
|
</Button>
|
|
]}
|
|
>
|
|
<Typography.Paragraph>{klippyErrorMessage}</Typography.Paragraph>
|
|
</Modal>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default ControlPrinter
|