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