// src/PrintJobs.js import React, { useEffect, useState, useCallback, useContext } from 'react' import { useNavigate } from 'react-router-dom' import axios from 'axios' import { Table, Button, Flex, Space, Modal, Dropdown, message, notification, Input, Typography, Checkbox, Popover, Spin } from 'antd' import { createStyles } from 'antd-style' import { LoadingOutlined, PlayCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, PauseCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { AuthContext } from '../../Auth/AuthContext' import { SocketContext } from '../context/SocketContext' import NewPrintJob from './PrintJobs/NewPrintJob' import JobState from '../common/JobState' import SubJobCounter from '../common/SubJobCounter' import TimeDisplay from '../common/TimeDisplay' import IdText from '../common/IdText' import useColumnVisibility from '../hooks/useColumnVisibility' import JobIcon from '../../Icons/JobIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import PlusIcon from '../../Icons/PlusIcon' import ReloadIcon from '../../Icons/ReloadIcon' import EditIcon from '../../Icons/EditIcon.jsx' import XMarkIcon from '../../Icons/XMarkIcon.jsx' import CheckIcon from '../../Icons/CheckIcon.jsx' import config from '../../../config.js' const { Text } = Typography const useStyle = createStyles(({ css, token }) => { const { antCls } = token return { customTable: css` ${antCls}-table { ${antCls}-table-container { ${antCls}-table-body, ${antCls}-table-content { scrollbar-width: thin; scrollbar-color: #eaeaea transparent; scrollbar-gutter: stable; } } } ` } }) const PrintJobs = () => { const { styles } = useStyle() const [messageApi, contextHolder] = message.useMessage() const [notificationApi, notificationContextHolder] = notification.useNotification() const navigate = useNavigate() const [printJobsData, setPrintJobsData] = useState([]) const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [lazyLoading, setLazyLoading] = useState(false) const [filters, setFilters] = useState({}) const [sorter, setSorter] = useState({}) const [newPrintJobOpen, setNewPrintJobOpen] = useState(false) const [loading, setLoading] = useState(true) const getFilterDropdown = ({ setSelectedKeys, selectedKeys, confirm, clearFilters, propertyName }) => { return (
setSelectedKeys(e.target.value ? [e.target.value] : []) } onPressEnter={() => confirm()} style={{ width: 200, display: 'block' }} />
) } // Column definitions const columns = [ { title: '', dataIndex: '', key: '', width: 40, fixed: 'left', render: () => }, { title: 'GCode File Name', dataIndex: 'gcodeFile', key: 'gcodeFileName', width: 200, fixed: 'left', render: (gcodeFile) => {gcodeFile.name}, filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => getFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, propertyName: 'GCode file name' }), onFilter: (value, record) => record.gcodeFile.name.toLowerCase().includes(value.toLowerCase()) }, { title: 'ID', dataIndex: 'id', key: 'id', width: 165, render: (text) => , filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => getFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, propertyName: 'ID' }), onFilter: (value, record) => record.id.toLowerCase().includes(value.toLowerCase()) }, { title: 'State', key: 'state', width: 240, render: (record) => { return }, filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => getFilterDropdown({ setSelectedKeys, selectedKeys, confirm, clearFilters, propertyName: 'state' }), onFilter: (value, record) => record.state.type.toLowerCase().includes(value.toLowerCase()) }, { title: , key: 'complete', width: 70, render: (record) => { return } }, { title: , key: 'queued', width: 70, render: (record) => { return } }, { title: , key: 'failed', width: 70, render: (record) => { return } }, { title: , key: 'draft', width: 70, render: (record) => { return } }, { title: 'Created At', dataIndex: 'createdAt', key: 'createdAt', width: 180, render: (createdAt) => { if (createdAt) { return } else { return 'n/a' } }, sorter: true }, { title: 'Started At', dataIndex: 'startedAt', key: 'startedAt', width: 180, render: (startedAt) => { if (startedAt) { return } else { return 'n/a' } }, sorter: true }, { title: 'Actions', key: 'operation', fixed: 'right', width: 150, render: (record) => { return ( {record.state.type === 'draft' ? ( ) } } ] const { authenticated } = useContext(AuthContext) const { socket } = useContext(SocketContext) const [columnVisibility, updateColumnVisibility] = useColumnVisibility( 'PrintJobs', columns ) const handleDeployPrintJob = (printJobId) => { if (socket) { messageApi.info(`Print job ${printJobId} deployment initiated`) socket.emit('server.job_queue.deploy', { printJobId }, (response) => { if (response == false) { notificationApi.error({ message: 'Print job deployment failed', description: 'Please try again later' }) } else { notificationApi.success({ message: 'Print job deployment initiated', description: 'Please wait for the print job to start' }) } }) navigate(`/dashboard/production/printjobs/info?printJobId=${printJobId}`) } else { messageApi.error('Socket connection not available') } } const fetchPrintJobsData = useCallback( async (pageNum = 1, append = false) => { if (!authenticated) { return } try { const params = { page: pageNum, limit: 25, ...filters, sort: sorter.field, order: sorter.order } const response = await axios.get(`${config.backendUrl}/printjobs`, { params, headers: { Accept: 'application/json' }, withCredentials: true }) const newData = response.data setHasMore(newData.length === 25) if (append) { setPrintJobsData((prev) => [...prev, ...newData]) } else { setPrintJobsData(newData) } setLoading(false) setLazyLoading(false) } catch (error) { setLoading(false) setLazyLoading(false) if (error.response) { messageApi.error( 'Error fetching print jobs data:', error.response.status ) } else { messageApi.error( 'An unexpected error occurred. Please try again later.' ) } } }, [authenticated, messageApi, filters, sorter] ) const handleScroll = useCallback( (e) => { const { target } = e const scrollHeight = target.scrollHeight const scrollTop = target.scrollTop const clientHeight = target.clientHeight // If we're near the bottom (within 100px) and not currently loading if ( scrollHeight - scrollTop - clientHeight < 100 && !lazyLoading && hasMore ) { setLazyLoading(true) const nextPage = page + 1 setPage(nextPage) fetchPrintJobsData(nextPage, true) } }, [page, lazyLoading, hasMore, fetchPrintJobsData] ) useEffect(() => { // Fetch initial data if (authenticated) { fetchPrintJobsData() } }, [authenticated, fetchPrintJobsData]) const handleTableChange = (pagination, filters, sorter) => { const newFilters = {} Object.entries(filters).forEach(([key, value]) => { if (value && value.length > 0) { newFilters[key] = value[0] } }) setFilters(newFilters) setSorter({ field: sorter.field, order: sorter.order }) setPage(1) fetchPrintJobsData(1) } const getPrintJobActionItems = (printJobId) => { return { items: [ { label: 'Info', key: 'info', icon: }, { label: 'Edit', key: 'edit', icon: } ], onClick: ({ key }) => { if (key === 'edit') { showNewPrintJobModal(printJobId) } else if (key === 'info') { navigate( `/dashboard/production/printjobs/info?printJobId=${printJobId}` ) } } } } const actionItems = { items: [ { label: 'New Print Job', key: 'newPrintJob', icon: }, { type: 'divider' }, { label: 'Reload List', key: 'reloadList', icon: } ], onClick: ({ key }) => { if (key === 'newPrintJob') { showNewPrintJobModal() } else if (key === 'reloadList') { fetchPrintJobsData() } } } const showNewPrintJobModal = () => { setNewPrintJobOpen(true) } const getViewDropdownItems = () => { const columnItems = columns .filter((col) => col.key && col.title !== '') .map((col) => ( { updateColumnVisibility(col.key, e.target.checked) }} > {col.title} )) return ( {columnItems} ) } const visibleColumns = columns.filter( (col) => !col.key || columnVisibility[col.key] ) return ( <> {notificationContextHolder} {contextHolder} {lazyLoading && } />} }} scroll={{ y: 'calc(100vh - 270px)' }} onChange={handleTableChange} onScroll={handleScroll} showSorterTooltip={false} /> { setNewPrintJobOpen(false) }} > { setNewPrintJobOpen(false) messageApi.success('New print job created successfully.') fetchPrintJobsData() }} reset={newPrintJobOpen} /> ) } export default PrintJobs