403 lines
13 KiB
JavaScript
403 lines
13 KiB
JavaScript
import React, { useState, useEffect, useContext } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
import axios from 'axios'
|
|
import {
|
|
Descriptions,
|
|
Spin,
|
|
Space,
|
|
Button,
|
|
message,
|
|
Progress,
|
|
Typography,
|
|
Collapse,
|
|
Flex,
|
|
Dropdown,
|
|
Popover,
|
|
Checkbox,
|
|
Card
|
|
} from 'antd'
|
|
import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons'
|
|
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 GCodeFileIcon from '../../../Icons/GCodeFileIcon'
|
|
import ReloadIcon from '../../../Icons/ReloadIcon'
|
|
import useCollapseState from '../../hooks/useCollapseState'
|
|
import config from '../../../../config'
|
|
import AuditLogTable from '../../common/AuditLogTable'
|
|
import DashboardNotes from '../../common/DashboardNotes'
|
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
|
import JobIcon from '../../../Icons/JobIcon'
|
|
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
|
import NoteIcon from '../../../Icons/NoteIcon'
|
|
|
|
const { Title, Text } = Typography
|
|
|
|
const JobInfo = () => {
|
|
const [jobData, setJobData] = useState(null)
|
|
const [fetchLoading, setFetchLoading] = useState(true)
|
|
const [error, setError] = useState(null)
|
|
const location = useLocation()
|
|
const [messageApi] = message.useMessage()
|
|
const jobId = new URLSearchParams(location.search).get('jobId')
|
|
const { socket } = useContext(SocketContext)
|
|
const [collapseState, updateCollapseState] = useCollapseState('JobInfo', {
|
|
info: true,
|
|
subJobs: true,
|
|
notes: true,
|
|
auditLogs: true
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (jobId) {
|
|
fetchJobDetails()
|
|
}
|
|
}, [jobId])
|
|
|
|
useEffect(() => {
|
|
if (socket && jobId) {
|
|
socket.on('notify_job_update', (updateData) => {
|
|
if (updateData._id === jobId) {
|
|
setJobData((prevData) => {
|
|
if (!prevData) return prevData
|
|
return {
|
|
...prevData,
|
|
state: updateData.state,
|
|
...updateData
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
return () => {
|
|
if (socket) {
|
|
socket.off('notify_job_update')
|
|
}
|
|
}
|
|
}, [socket, jobId])
|
|
|
|
const fetchJobDetails = async () => {
|
|
try {
|
|
setFetchLoading(true)
|
|
const response = await axios.get(`${config.backendUrl}/jobs/${jobId}`, {
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
withCredentials: true // Important for including cookies
|
|
})
|
|
setJobData(response.data)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError('Failed to fetch print job details')
|
|
messageApi.error('Failed to fetch print job details')
|
|
} finally {
|
|
setFetchLoading(false)
|
|
}
|
|
}
|
|
|
|
const getViewDropdownItems = () => {
|
|
const sections = [
|
|
{ key: 'info', label: 'Job Information' },
|
|
{ key: 'subJobs', label: 'Sub Jobs' },
|
|
{ key: 'notes', label: 'Notes' },
|
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
|
]
|
|
|
|
return (
|
|
<Flex vertical>
|
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
|
{sections.map((section) => (
|
|
<Checkbox
|
|
checked={collapseState[section.key]}
|
|
key={section.key}
|
|
onChange={(e) => {
|
|
updateCollapseState(section.key, e.target.checked)
|
|
}}
|
|
>
|
|
{section.label}
|
|
</Checkbox>
|
|
))}
|
|
</Flex>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
const actionItems = {
|
|
items: [
|
|
{
|
|
label: 'Reload Job',
|
|
key: 'reload',
|
|
icon: <ReloadIcon />
|
|
}
|
|
],
|
|
onClick: ({ key }) => {
|
|
if (key === 'edit') {
|
|
// TODO: Implement edit functionality
|
|
messageApi.info('Edit functionality coming soon')
|
|
} else if (key === 'reload') {
|
|
fetchJobDetails()
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Flex
|
|
gap='large'
|
|
vertical='true'
|
|
style={{ height: '100%', minHeight: 0 }}
|
|
>
|
|
<Flex justify={'space-between'}>
|
|
<Space size='small'>
|
|
<Dropdown menu={actionItems}>
|
|
<Button>Actions</Button>
|
|
</Dropdown>
|
|
<Popover
|
|
content={getViewDropdownItems()}
|
|
placement='bottomLeft'
|
|
arrow={false}
|
|
>
|
|
<Button>View</Button>
|
|
</Popover>
|
|
</Space>
|
|
</Flex>
|
|
|
|
{error ? (
|
|
<Space
|
|
direction='vertical'
|
|
style={{ width: '100%', textAlign: 'center' }}
|
|
>
|
|
<p>{error || 'Print job not found'}</p>
|
|
<Button icon={<ReloadIcon />} onClick={fetchJobDetails}>
|
|
Retry
|
|
</Button>
|
|
</Space>
|
|
) : (
|
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
<Flex vertical style={{ flexGrow: 1 }} gap={'large'}>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.info ? ['info'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('info', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
className='no-h-padding-collapse no-t-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex align='center' gap={'middle'}>
|
|
<InfoCircleIcon />
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Job Information
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='info'
|
|
>
|
|
<Spin spinning={fetchLoading} indicator={<LoadingOutlined />}>
|
|
<Descriptions
|
|
bordered
|
|
column={{
|
|
xs: 1,
|
|
sm: 1,
|
|
md: 1,
|
|
lg: 2,
|
|
xl: 2,
|
|
xxl: 2
|
|
}}
|
|
>
|
|
<Descriptions.Item label='ID'>
|
|
{jobData?._id ? (
|
|
<IdText id={jobData._id} type={'job'} />
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Status'>
|
|
{jobData?.state ? (
|
|
<JobState
|
|
job={jobData}
|
|
showProgress={false}
|
|
showQuantity={false}
|
|
showId={false}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='GCode File Name'>
|
|
{jobData?.gcodeFile ? (
|
|
<Space>
|
|
<GCodeFileIcon />
|
|
<Text>
|
|
{jobData.gcodeFile.name || 'Not specified'}
|
|
</Text>
|
|
</Space>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='GCode File ID'>
|
|
{jobData?.gcodeFile?._id ? (
|
|
<IdText
|
|
id={jobData.gcodeFile._id}
|
|
type={'gcodefile'}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Quantity'>
|
|
{jobData?.quantity ? (
|
|
<Text>{jobData.quantity}</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Created At'>
|
|
{jobData?.createdAt ? (
|
|
<TimeDisplay
|
|
dateTime={jobData.createdAt}
|
|
showSince={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Started At'>
|
|
{jobData?.startedAt ? (
|
|
<TimeDisplay
|
|
dateTime={jobData.startedAt}
|
|
showSince={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
{jobData?.state?.type === 'printing' && (
|
|
<Descriptions.Item label='Progress'>
|
|
<Progress
|
|
percent={Math.round(
|
|
(jobData.state.progress || 0) * 100
|
|
)}
|
|
/>
|
|
</Descriptions.Item>
|
|
)}
|
|
<Descriptions.Item label='Assigned Printers'>
|
|
{jobData?.printers ? (
|
|
<Text>
|
|
{jobData.printers.length} printers assigned
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Spin>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.subJobs ? ['2'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('subJobs', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex align='center' gap={'middle'}>
|
|
<JobIcon />
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Sub Job Information
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='2'
|
|
>
|
|
<SubJobsTree jobData={jobData} loading={fetchLoading} />
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.notes ? ['notes'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('notes', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex align='center' gap={'middle'}>
|
|
<NoteIcon />
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Notes
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='notes'
|
|
>
|
|
<Card>
|
|
<DashboardNotes _id={jobId} />
|
|
</Card>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.auditLogs ? ['auditLogs'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('auditLogs', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex align='center' gap={'middle'}>
|
|
<AuditLogIcon />
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Audit Logs
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='auditLogs'
|
|
>
|
|
<AuditLogTable
|
|
items={jobData?.auditLogs || []}
|
|
loading={fetchLoading}
|
|
showTargetColumn={false}
|
|
/>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</Flex>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default JobInfo
|