598 lines
21 KiB
JavaScript
598 lines
21 KiB
JavaScript
import React, { useState, useEffect } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
import axios from 'axios'
|
|
import {
|
|
Descriptions,
|
|
Spin,
|
|
Space,
|
|
Button,
|
|
message,
|
|
Badge,
|
|
Form,
|
|
Typography,
|
|
Flex,
|
|
Input,
|
|
Card,
|
|
Collapse,
|
|
Dropdown,
|
|
Popover,
|
|
Checkbox
|
|
} from 'antd'
|
|
import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons'
|
|
import IdText from '../../common/IdText.jsx'
|
|
import { capitalizeFirstLetter } from '../../utils/Utils.js'
|
|
import FilamentSelect from '../../common/FilamentSelect'
|
|
import useCollapseState from '../../hooks/useCollapseState'
|
|
import FilamentIcon from '../../../Icons/FilamentIcon'
|
|
import TimeDisplay from '../../common/TimeDisplay.jsx'
|
|
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'
|
|
import AuditLogTable from '../../common/AuditLogTable.jsx'
|
|
import DashboardNotes from '../../common/DashboardNotes.jsx'
|
|
import BinIcon from '../../../Icons/BinIcon.jsx'
|
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
|
import GCodeFileIcon from '../../../Icons/GCodeFileIcon.jsx'
|
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
|
|
|
const { Title, Text } = Typography
|
|
|
|
const GCodeFileInfo = () => {
|
|
const [gcodeFileData, setGCodeFileData] = useState(null)
|
|
const [editLoading, setLoading] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
const location = useLocation()
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
const [form] = Form.useForm()
|
|
const [fetchLoading, setFetchLoading] = useState(true)
|
|
const [collapseState, updateCollapseState] = useCollapseState(
|
|
'GCodeFileInfo',
|
|
{
|
|
info: true,
|
|
preview: true
|
|
}
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (gcodeFileId) {
|
|
fetchGCodeFileDetails()
|
|
}
|
|
}, [gcodeFileId])
|
|
|
|
useEffect(() => {
|
|
if (gcodeFileData) {
|
|
form.setFieldsValue({
|
|
name: gcodeFileData.name || '',
|
|
filament: gcodeFileData.filament || { id: null, name: '' }
|
|
})
|
|
}
|
|
}, [gcodeFileData, form])
|
|
|
|
const fetchGCodeFileDetails = async () => {
|
|
try {
|
|
setFetchLoading(true)
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/gcodefiles/${gcodeFileId}`,
|
|
{
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
withCredentials: true
|
|
}
|
|
)
|
|
setGCodeFileData(response.data)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError('Failed to fetch GCodeFile details')
|
|
messageApi.error('Failed to fetch GCodeFile details')
|
|
} finally {
|
|
setFetchLoading(false)
|
|
}
|
|
}
|
|
|
|
const startEditing = () => {
|
|
setIsEditing(true)
|
|
updateCollapseState('info', true)
|
|
}
|
|
|
|
const cancelEditing = () => {
|
|
form.setFieldsValue({
|
|
name: gcodeFileData?.name || '',
|
|
filament: gcodeFileData?.filament || { id: null, name: '' }
|
|
})
|
|
setIsEditing(false)
|
|
}
|
|
|
|
const updateGCodeFileInfo = async () => {
|
|
try {
|
|
const values = await form.validateFields()
|
|
setLoading(true)
|
|
|
|
await axios.put(
|
|
`${config.backendUrl}/gcodefiles/${gcodeFileId}`,
|
|
values,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
withCredentials: true
|
|
}
|
|
)
|
|
|
|
setGCodeFileData({ ...gcodeFileData, ...values })
|
|
setIsEditing(false)
|
|
messageApi.success('GCode File information updated successfully')
|
|
} catch (err) {
|
|
if (err.errorFields) {
|
|
return
|
|
}
|
|
console.error('Failed to update gcode file information:', err)
|
|
messageApi.error('Failed to update gcode file information')
|
|
} finally {
|
|
fetchGCodeFileDetails()
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const actionItems = {
|
|
items: [
|
|
{
|
|
label: 'Edit GCode File',
|
|
key: 'edit',
|
|
icon: <EditIcon />
|
|
},
|
|
{
|
|
label: 'Delete GCode File',
|
|
key: 'delete',
|
|
icon: <BinIcon />,
|
|
danger: true
|
|
},
|
|
{ type: 'divider' },
|
|
{
|
|
label: 'Reload GCode File',
|
|
key: 'reload',
|
|
icon: <ReloadIcon />
|
|
}
|
|
],
|
|
onClick: ({ key }) => {
|
|
if (key === 'reload') {
|
|
fetchGCodeFileDetails()
|
|
}
|
|
}
|
|
}
|
|
|
|
const getViewDropdownItems = () => {
|
|
const sections = [
|
|
{ key: 'info', label: 'GCode File Information' },
|
|
{ key: 'preview', label: 'GCode File Preview' },
|
|
{ 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>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{contextHolder}
|
|
<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>
|
|
<Space>
|
|
{isEditing ? (
|
|
<>
|
|
<Button
|
|
icon={<CheckIcon />}
|
|
type='primary'
|
|
onClick={updateGCodeFileInfo}
|
|
loading={editLoading}
|
|
disabled={editLoading}
|
|
/>
|
|
<Button
|
|
icon={<XMarkIcon />}
|
|
onClick={cancelEditing}
|
|
disabled={editLoading}
|
|
/>
|
|
</>
|
|
) : (
|
|
<Button icon={<EditIcon />} onClick={startEditing} />
|
|
)}
|
|
</Space>
|
|
</Flex>
|
|
|
|
{error ? (
|
|
<Space
|
|
direction='vertical'
|
|
style={{ width: '100%', textAlign: 'center' }}
|
|
>
|
|
<p>{error || 'Print job not found'}</p>
|
|
<Button icon={<ReloadIcon />} onClick={fetchGCodeFileDetails}>
|
|
Retry
|
|
</Button>
|
|
</Space>
|
|
) : (
|
|
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
<Flex vertical 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 }}>
|
|
GCode File Information
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='info'
|
|
>
|
|
<Form form={form} layout='vertical'>
|
|
<Spin
|
|
spinning={fetchLoading}
|
|
indicator={<LoadingOutlined />}
|
|
>
|
|
<Descriptions
|
|
bordered
|
|
column={{
|
|
xs: 1,
|
|
sm: 1,
|
|
md: 1,
|
|
lg: 2,
|
|
xl: 2,
|
|
xxl: 2
|
|
}}
|
|
>
|
|
<Descriptions.Item label='ID' span={1}>
|
|
{gcodeFileData?._id ? (
|
|
<IdText
|
|
id={gcodeFileData._id}
|
|
type='gcodefile'
|
|
></IdText>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Created At'>
|
|
{gcodeFileData?.createdAt ? (
|
|
<TimeDisplay
|
|
dateTime={gcodeFileData.createdAt}
|
|
showSince={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Name'>
|
|
{isEditing ? (
|
|
<Form.Item
|
|
name='name'
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: 'Please enter a vendor name'
|
|
},
|
|
{
|
|
max: 100,
|
|
message: 'Name cannot exceed 100 characters'
|
|
}
|
|
]}
|
|
style={{ margin: 0 }}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
) : gcodeFileData?.name ? (
|
|
<Text>{gcodeFileData.name}</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
|
|
<Descriptions.Item label='Updated At'>
|
|
{gcodeFileData?.updatedAt ? (
|
|
<TimeDisplay
|
|
dateTime={gcodeFileData.updatedAt}
|
|
showSince={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Name'>
|
|
{isEditing ? (
|
|
<Form.Item
|
|
name='filament'
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: 'Please enter a filament'
|
|
}
|
|
]}
|
|
style={{ margin: 0 }}
|
|
>
|
|
<FilamentSelect />
|
|
</Form.Item>
|
|
) : gcodeFileData?.filament ? (
|
|
<Space>
|
|
<FilamentIcon />
|
|
<Badge
|
|
color={gcodeFileData.filament.color}
|
|
text={gcodeFileData.filament.name}
|
|
/>
|
|
</Space>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament ID'>
|
|
{gcodeFileData?.filament ? (
|
|
<IdText
|
|
id={gcodeFileData.filament.id}
|
|
type={'filament'}
|
|
showHyperlink={true}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Est Print Time'>
|
|
{gcodeFileData?.gcodeFileInfo
|
|
?.estimatedPrintingTimeNormalMode ? (
|
|
<Text>
|
|
{
|
|
gcodeFileData.gcodeFileInfo
|
|
.estimatedPrintingTimeNormalMode
|
|
}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Cost'>
|
|
{gcodeFileData?.cost ? (
|
|
<Text>{'£' + gcodeFileData.cost.toFixed(2)}</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Infill Density'>
|
|
{gcodeFileData?.gcodeFileInfo?.sparseInfillDensity ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.sparseInfillDensity}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Infill Pattern'>
|
|
{gcodeFileData?.gcodeFileInfo?.sparseInfillPattern ? (
|
|
<Text>
|
|
{capitalizeFirstLetter(
|
|
gcodeFileData.gcodeFileInfo.sparseInfillPattern
|
|
)}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Used (mm)'>
|
|
{gcodeFileData?.gcodeFileInfo?.filamentUsedMm ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.filamentUsedMm}mm
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Used (g)'>
|
|
{gcodeFileData?.gcodeFileInfo?.filamentUsedG ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.filamentUsedG}g
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Hotend Temperature'>
|
|
{gcodeFileData?.gcodeFileInfo?.nozzleTemperature ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.nozzleTemperature}°
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Bed Temperature'>
|
|
{gcodeFileData?.gcodeFileInfo?.hotPlateTemp ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.hotPlateTemp}°
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Filament Profile'>
|
|
{gcodeFileData?.gcodeFileInfo?.filamentSettingsId ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.filamentSettingsId.replaceAll(
|
|
'"',
|
|
''
|
|
)}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Print Profile'>
|
|
{gcodeFileData?.gcodeFileInfo?.printSettingsId ? (
|
|
<Text>
|
|
{gcodeFileData.gcodeFileInfo.printSettingsId.replaceAll(
|
|
'"',
|
|
''
|
|
)}
|
|
</Text>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Spin>
|
|
</Form>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.preview ? ['preview'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('preview', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex align='center' gap={'middle'}>
|
|
<GCodeFileIcon />
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
GCode File Preview
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='preview'
|
|
>
|
|
<Spin spinning={fetchLoading} indicator={<LoadingOutlined />}>
|
|
<Card styles={{ body: { padding: '10px' } }}>
|
|
{gcodeFileData?.gcodeFileInfo?.thumbnail ? (
|
|
<img
|
|
src={`data:image/png;base64,${gcodeFileData.gcodeFileInfo.thumbnail.data}`}
|
|
alt='GCodeFile'
|
|
style={{ maxWidth: '100%' }}
|
|
/>
|
|
) : (
|
|
<Text>n/a</Text>
|
|
)}
|
|
</Card>
|
|
</Spin>
|
|
</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={gcodeFileId} />
|
|
</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 Log
|
|
</Title>
|
|
</Flex>
|
|
}
|
|
key='auditLogs'
|
|
>
|
|
<AuditLogTable
|
|
items={gcodeFileData?.auditLogs || []}
|
|
loading={fetchLoading}
|
|
showTargetColumn={false}
|
|
/>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</Flex>
|
|
</div>
|
|
)}
|
|
</Flex>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default GCodeFileInfo
|