420 lines
13 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
} from 'antd'
import { LoadingOutlined, CaretRightOutlined } 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'
const { Title } = Typography
const GCodeFileInfo = () => {
const [gcodeFileData, setGCodeFileData] = useState(null)
const [loading, 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 updateInfo = 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)
}
}
if (error || !gcodeFileData) {
return (
<Space
direction='vertical'
style={{ width: '100%', textAlign: 'center' }}
>
<p>{error || 'GCodeFile not found'}</p>
<Button icon={<ReloadIcon />} onClick={fetchGCodeFileDetails}>
Retry
</Button>
</Space>
)
}
if (fetchLoading) {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
</div>
)
}
return (
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
{contextHolder}
<Flex vertical gap={'large'}>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.info ? ['1'] : []}
onChange={(keys) => updateCollapseState('info', keys.length > 0)}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '9px' }}
/>
)}
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 }}>
GCode File Information
</Title>
<Space>
{isEditing ? (
<>
<Button
icon={<CheckIcon />}
type='primary'
onClick={updateInfo}
loading={loading}
/>
<Button
icon={<XMarkIcon />}
onClick={cancelEditing}
disabled={loading}
/>
</>
) : (
<Button icon={<EditIcon />} onClick={startEditing} />
)}
</Space>
</Flex>
}
key='1'
>
<Form form={form} layout='vertical'>
<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>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Created At'>
<TimeDisplay
dateTime={gcodeFileData.createdAt}
showSince={true}
/>
</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
)}
</Descriptions.Item>
<Descriptions.Item label='Updated At'>
<TimeDisplay
dateTime={gcodeFileData.updatedAt}
showSince={true}
/>
</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>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament ID'>
{gcodeFileData.filament ? (
<IdText
id={gcodeFileData.filament.id}
type={'filament'}
showHyperlink={true}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Est Print Time'>
{gcodeFileData.gcodeFileInfo
.estimatedPrintingTimeNormalMode || 'n/a'}
</Descriptions.Item>
<Descriptions.Item label='Cost'>
{'£' + gcodeFileData.cost.toFixed(2) || 'n/a'}
</Descriptions.Item>
<Descriptions.Item label='Infill Density'>
{(() => {
if (gcodeFileData.gcodeFileInfo.sparseInfillDensity) {
return gcodeFileData.gcodeFileInfo.sparseInfillDensity
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Infill Pattern'>
{(() => {
if (gcodeFileData.gcodeFileInfo.sparseInfillPattern) {
return capitalizeFirstLetter(
gcodeFileData.gcodeFileInfo.sparseInfillPattern
)
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Filament Used (mm)'>
{(() => {
if (gcodeFileData.gcodeFileInfo.filamentUsedMm) {
return `${gcodeFileData.gcodeFileInfo.filamentUsedMm}mm`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Filament Used (g)'>
{(() => {
if (gcodeFileData.gcodeFileInfo.filamentUsedG) {
return `${gcodeFileData.gcodeFileInfo.filamentUsedG}g`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Hotend Temperature'>
{(() => {
if (gcodeFileData.gcodeFileInfo.nozzleTemperature) {
return `${gcodeFileData.gcodeFileInfo.nozzleTemperature}°`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Bed Temperature'>
{(() => {
if (gcodeFileData.gcodeFileInfo.hotPlateTemp) {
return `${gcodeFileData.gcodeFileInfo.hotPlateTemp}°`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Filament Profile'>
{(() => {
if (gcodeFileData.gcodeFileInfo.filamentSettingsId) {
return `${gcodeFileData.gcodeFileInfo.filamentSettingsId.replaceAll('"', '')}`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
<Descriptions.Item label='Print Profile'>
{(() => {
if (gcodeFileData.gcodeFileInfo.printSettingsId) {
return `${gcodeFileData.gcodeFileInfo.printSettingsId.replaceAll('"', '')}`
} else {
return 'n/a'
}
})()}
</Descriptions.Item>
</Descriptions>
</Form>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.preview ? ['2'] : []}
onChange={(keys) => updateCollapseState('preview', keys.length > 0)}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '2px' }}
/>
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Title level={5} style={{ margin: 0 }}>
GCode File Preview
</Title>
}
key='2'
>
<Card styles={{ body: { padding: '10px' } }}>
{gcodeFileData.gcodeFileInfo.thumbnail ? (
<img
src={`data:image/png;base64,${gcodeFileData.gcodeFileInfo.thumbnail.data}`}
alt='GCodeFile'
style={{ maxWidth: '100%' }}
/>
) : (
'n/a'
)}
</Card>
</Collapse.Panel>
</Collapse>
</Flex>
</div>
)
}
export default GCodeFileInfo