565 lines
16 KiB
JavaScript
565 lines
16 KiB
JavaScript
import { useState, useContext, useEffect, useCallback } from 'react'
|
|
import axios from 'axios'
|
|
import {
|
|
Form,
|
|
Button,
|
|
message,
|
|
Typography,
|
|
Flex,
|
|
Steps,
|
|
Divider,
|
|
Input,
|
|
Select,
|
|
Space,
|
|
Descriptions,
|
|
List,
|
|
InputNumber,
|
|
notification,
|
|
Progress,
|
|
Modal,
|
|
Radio
|
|
} from 'antd'
|
|
import { SearchOutlined, SettingOutlined } from '@ant-design/icons'
|
|
import PropTypes from 'prop-types'
|
|
import { PrintServerContext } from '../../context/PrintServerContext'
|
|
import EditIcon from '../../../Icons/EditIcon.jsx'
|
|
|
|
import config from '../../../../config.js'
|
|
|
|
const { Title } = Typography
|
|
|
|
const initialNewPrinterForm = {
|
|
moonraker: {
|
|
protocol: 'ws',
|
|
host: '',
|
|
port: '',
|
|
apiKey: ''
|
|
}
|
|
}
|
|
|
|
const NewPrinter = ({ onOk, reset }) => {
|
|
NewPrinter.propTypes = {
|
|
onOk: PropTypes.func.isRequired,
|
|
reset: PropTypes.bool.isRequired
|
|
}
|
|
|
|
const { printServer } = useContext(PrintServerContext)
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
const [notificationApi, notificationContextHolder] =
|
|
notification.useNotification()
|
|
const [newPrinterLoading, setNewPrinterLoading] = useState(false)
|
|
const [currentStep, setCurrentStep] = useState(0)
|
|
const [nextEnabled, setNextEnabled] = useState(false)
|
|
const [newPrinterForm] = Form.useForm()
|
|
const [newPrinterFormValues, setNewPrinterFormValues] = useState(
|
|
initialNewPrinterForm
|
|
)
|
|
const [discoveredPrinters, setDiscoveredPrinters] = useState([])
|
|
const [discovering, setDiscovering] = useState(false)
|
|
const [showManualSetup, setShowManualSetup] = useState(false)
|
|
const [scanPort, setScanPort] = useState(7125)
|
|
const [scanProtocol, setScanProtocol] = useState('ws')
|
|
const [editingHostname, setEditingHostname] = useState(null)
|
|
const [hostnameInput, setHostnameInput] = useState('')
|
|
const [initialized, setInitialized] = useState(false)
|
|
|
|
const newPrinterFormUpdateValues = Form.useWatch([], newPrinterForm)
|
|
|
|
useEffect(() => {
|
|
newPrinterForm
|
|
.validateFields({
|
|
validateOnly: true
|
|
})
|
|
.then(() => {
|
|
if (currentStep === 0) {
|
|
const moonraker = newPrinterForm.getFieldValue('moonraker')
|
|
setNextEnabled(
|
|
!!(moonraker?.protocol && moonraker?.host && moonraker?.port)
|
|
)
|
|
} else if (currentStep === 1) {
|
|
const name = newPrinterForm.getFieldValue('name')
|
|
setNextEnabled(!!name)
|
|
} else {
|
|
setNextEnabled(true)
|
|
}
|
|
})
|
|
.catch(() => setNextEnabled(false))
|
|
}, [newPrinterForm, newPrinterFormUpdateValues, currentStep])
|
|
|
|
const summaryItems = [
|
|
{
|
|
key: 'name',
|
|
label: 'Name',
|
|
children: newPrinterFormValues.name
|
|
},
|
|
{
|
|
key: 'protocol',
|
|
label: 'Protocol',
|
|
children: newPrinterFormValues.moonraker?.protocol
|
|
},
|
|
{
|
|
key: 'host',
|
|
label: 'Host',
|
|
children: newPrinterFormValues.moonraker?.host
|
|
},
|
|
{
|
|
key: 'port',
|
|
label: 'Port',
|
|
children: newPrinterFormValues.moonraker?.port
|
|
}
|
|
]
|
|
|
|
useEffect(() => {
|
|
if (reset) {
|
|
newPrinterForm.resetFields()
|
|
}
|
|
}, [reset, newPrinterForm])
|
|
|
|
const handlePrinterSelect = (printer) => {
|
|
newPrinterForm.setFieldsValue({
|
|
moonraker: {
|
|
protocol: printer.protocol,
|
|
host: printer.host,
|
|
port: printer.port
|
|
}
|
|
})
|
|
setNewPrinterFormValues({
|
|
...newPrinterFormValues,
|
|
moonraker: {
|
|
protocol: printer.protocol,
|
|
host: printer.host,
|
|
port: printer.port
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleHostnameEdit = (printer, newHostname) => {
|
|
if (newHostname && newHostname.trim() !== '') {
|
|
const updatedPrinter = {
|
|
...printer,
|
|
host: newHostname.trim()
|
|
}
|
|
setDiscoveredPrinters((prev) =>
|
|
prev.map((p) => (p.host === printer.host ? updatedPrinter : p))
|
|
)
|
|
setEditingHostname(null)
|
|
setHostnameInput('')
|
|
}
|
|
}
|
|
|
|
const showEditHostnameDialog = (printer) => {
|
|
setEditingHostname(printer.host)
|
|
setHostnameInput(printer.host)
|
|
}
|
|
|
|
const handleNewPrinter = async () => {
|
|
setNewPrinterLoading(true)
|
|
try {
|
|
await axios.post(
|
|
`${config.backendUrl}/printers`,
|
|
{
|
|
...newPrinterFormValues
|
|
},
|
|
{
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
withCredentials: true
|
|
}
|
|
)
|
|
|
|
onOk()
|
|
} catch (error) {
|
|
messageApi.error('Error adding new printer: ' + error.message)
|
|
} finally {
|
|
setNewPrinterLoading(false)
|
|
}
|
|
}
|
|
|
|
const notifyScanNetworkFound = useCallback(
|
|
(data) => {
|
|
const newPrinter = {
|
|
protocol: scanProtocol,
|
|
host: data.hostname || data.ip,
|
|
port: scanPort
|
|
}
|
|
notificationApi.info({
|
|
message: 'Printer Found',
|
|
description: `Printer found: ${data.hostname || data.ip}!`
|
|
})
|
|
setDiscoveredPrinters((prev) => [...prev, newPrinter])
|
|
},
|
|
[scanProtocol, scanPort, notificationApi]
|
|
)
|
|
|
|
const notifyScanNetworkComplete = useCallback(
|
|
(data) => {
|
|
setDiscovering(false)
|
|
notificationApi.destroy('network-scan')
|
|
if (data == false) {
|
|
messageApi.error('Error discovering printers!')
|
|
} else {
|
|
messageApi.success('Finished discovering printers!')
|
|
}
|
|
},
|
|
[messageApi, notificationApi]
|
|
)
|
|
|
|
const notifyScanNetworkProgress = useCallback(
|
|
(data) => {
|
|
notificationApi.info({
|
|
message: 'Scanning Network',
|
|
description: (
|
|
<div>
|
|
<div style={{ marginBottom: 12 }}>
|
|
Scanning IP: {data.currentIP}
|
|
</div>
|
|
<Progress
|
|
percent={data.progress}
|
|
size='small'
|
|
status='active'
|
|
showInfo={false}
|
|
/>
|
|
</div>
|
|
),
|
|
duration: 0,
|
|
key: 'network-scan',
|
|
icon: null,
|
|
placement: 'bottomRight',
|
|
style: {
|
|
width: 360
|
|
},
|
|
className: 'network-scan-notification',
|
|
closeIcon: null,
|
|
onClose: () => {},
|
|
btn: null
|
|
})
|
|
},
|
|
[notificationApi]
|
|
)
|
|
|
|
const discoverPrinters = useCallback(() => {
|
|
if (!discovering) {
|
|
setDiscovering(true)
|
|
setDiscoveredPrinters([])
|
|
messageApi.info('Discovering printers...')
|
|
printServer.off('notify_scan_network_found')
|
|
printServer.off('notify_scan_network_progress')
|
|
printServer.off('notify_scan_network_complete')
|
|
|
|
printServer.on('notify_scan_network_found', notifyScanNetworkFound)
|
|
printServer.on('notify_scan_network_progress', notifyScanNetworkProgress)
|
|
printServer.on('notify_scan_network_complete', notifyScanNetworkComplete)
|
|
|
|
printServer.emit('bridge.scan_network.start', {
|
|
port: scanPort,
|
|
protocol: scanProtocol
|
|
})
|
|
}
|
|
}, [
|
|
discovering,
|
|
printServer,
|
|
scanPort,
|
|
scanProtocol,
|
|
messageApi,
|
|
notifyScanNetworkFound,
|
|
notifyScanNetworkProgress,
|
|
notifyScanNetworkComplete
|
|
])
|
|
|
|
useEffect(() => {
|
|
setInitialized(true)
|
|
if (!initialized) {
|
|
discoverPrinters()
|
|
}
|
|
}, [initialized, discoverPrinters])
|
|
|
|
const stopDiscovery = () => {
|
|
if (discovering) {
|
|
setDiscovering(false)
|
|
notificationApi.destroy('network-scan')
|
|
messageApi.info('Stopping discovery...')
|
|
printServer.off('notify_scan_network_found')
|
|
printServer.off('notify_scan_network_progress')
|
|
printServer.off('notify_scan_network_complete')
|
|
printServer.emit('bridge.scan_network.stop', (response) => {
|
|
if (response == false) {
|
|
messageApi.error('Error stopping discovery!')
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const handlePortChange = (value) => {
|
|
stopDiscovery()
|
|
setScanPort(value)
|
|
}
|
|
|
|
const handleProtocolChange = (value) => {
|
|
stopDiscovery()
|
|
setScanProtocol(value)
|
|
}
|
|
|
|
const steps = [
|
|
{
|
|
title: 'Discovery',
|
|
key: 'discovery',
|
|
content: (
|
|
<>
|
|
<Flex vertical style={{ width: '100%' }} gap='large'>
|
|
{!showManualSetup ? (
|
|
<>
|
|
<Flex
|
|
style={{ width: '100%' }}
|
|
justify='space-between'
|
|
align='center'
|
|
gap='middle'
|
|
>
|
|
<Space.Compact>
|
|
<InputNumber
|
|
min={1}
|
|
max={65535}
|
|
value={scanPort}
|
|
onChange={handlePortChange}
|
|
style={{ width: '80px' }}
|
|
placeholder='Port'
|
|
/>
|
|
<Select
|
|
value={scanProtocol}
|
|
onChange={handleProtocolChange}
|
|
options={[
|
|
{ value: 'ws', label: 'ws' },
|
|
{ value: 'wss', label: 'wss' }
|
|
]}
|
|
/>
|
|
<Button
|
|
icon={<SearchOutlined />}
|
|
onClick={discoverPrinters}
|
|
loading={discovering}
|
|
>
|
|
{discovering ? 'Discovering...' : 'Discover'}
|
|
</Button>
|
|
</Space.Compact>
|
|
<Button
|
|
icon={<SettingOutlined />}
|
|
onClick={() => setShowManualSetup(true)}
|
|
>
|
|
Manual Setup
|
|
</Button>
|
|
</Flex>
|
|
<List
|
|
dataSource={discoveredPrinters}
|
|
renderItem={(printer) => (
|
|
<List.Item
|
|
key={`${printer.host}:${printer.port}`}
|
|
actions={[
|
|
<Radio
|
|
key='select'
|
|
defaultChecked={
|
|
newPrinterFormValues.moonraker?.host ===
|
|
printer.host
|
|
}
|
|
onChange={() => handlePrinterSelect(printer)}
|
|
/>
|
|
]}
|
|
>
|
|
<List.Item.Meta
|
|
title={
|
|
<Space>
|
|
{printer.host}
|
|
{!printer.hostname && (
|
|
<Button
|
|
type='text'
|
|
icon={<EditIcon />}
|
|
onClick={() => showEditHostnameDialog(printer)}
|
|
/>
|
|
)}
|
|
</Space>
|
|
}
|
|
description={`Protocol: ${printer.protocol}, Port: ${printer.port}`}
|
|
/>
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
<Modal
|
|
title='Edit Host'
|
|
open={editingHostname !== null}
|
|
onOk={() => {
|
|
const printer = discoveredPrinters.find(
|
|
(p) => p.host === editingHostname
|
|
)
|
|
if (printer) {
|
|
handleHostnameEdit(printer, hostnameInput)
|
|
}
|
|
}}
|
|
onCancel={() => {
|
|
setEditingHostname(null)
|
|
setHostnameInput('')
|
|
}}
|
|
>
|
|
<Form.Item label='Host' required>
|
|
<Input
|
|
value={hostnameInput}
|
|
onChange={(e) => setHostnameInput(e.target.value)}
|
|
placeholder='Enter host'
|
|
autoFocus
|
|
/>
|
|
</Form.Item>
|
|
</Modal>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Flex style={{ width: '100%' }} justify='end'>
|
|
<Button
|
|
icon={<SearchOutlined />}
|
|
onClick={() => setShowManualSetup(false)}
|
|
>
|
|
Back to Discovery
|
|
</Button>
|
|
</Flex>
|
|
<Flex vertical>
|
|
<Form.Item
|
|
label='Protocol'
|
|
name={['moonraker', 'protocol']}
|
|
rules={[
|
|
{ required: true, message: 'Protocol is required' }
|
|
]}
|
|
>
|
|
<Select
|
|
defaultValue='ws'
|
|
options={[
|
|
{ value: 'ws', label: 'Websocket' },
|
|
{ value: 'wss', label: 'Websocket Secure' }
|
|
]}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label='Host'
|
|
name={['moonraker', 'host']}
|
|
rules={[{ required: true, message: 'Host is required' }]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label='Port'
|
|
name={['moonraker', 'port']}
|
|
rules={[{ required: true, message: 'Port is required' }]}
|
|
>
|
|
<InputNumber
|
|
min={1}
|
|
max={65535}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label='API Key'
|
|
name={['moonraker', 'apiKey']}
|
|
rules={[{ required: false }]}
|
|
>
|
|
<Input.Password
|
|
placeholder='Optional API key'
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</Form.Item>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
</Flex>
|
|
</>
|
|
)
|
|
},
|
|
{
|
|
title: 'Required',
|
|
key: 'required',
|
|
content: (
|
|
<>
|
|
<Form.Item
|
|
label='Name'
|
|
name='name'
|
|
rules={[{ required: true, message: 'Name is required' }]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
</>
|
|
)
|
|
},
|
|
{
|
|
title: 'Summary',
|
|
key: 'summary',
|
|
content: (
|
|
<Form.Item>
|
|
<Descriptions column={1} items={summaryItems} size={'small'} />
|
|
</Form.Item>
|
|
)
|
|
}
|
|
]
|
|
|
|
return (
|
|
<Flex gap={'middle'}>
|
|
{contextHolder}
|
|
{notificationContextHolder}
|
|
|
|
<div style={{ minWidth: '160px' }}>
|
|
<Steps current={currentStep} items={steps} direction='vertical' />
|
|
</div>
|
|
|
|
<Divider type={'vertical'} style={{ height: 'unset' }} />
|
|
|
|
<Flex vertical={'true'} style={{ flexGrow: 1 }}>
|
|
<Title level={2} style={{ marginTop: 0 }}>
|
|
New Printer
|
|
</Title>
|
|
<Form
|
|
name='basic'
|
|
autoComplete='off'
|
|
form={newPrinterForm}
|
|
onFinish={handleNewPrinter}
|
|
onValuesChange={(changedValues) =>
|
|
setNewPrinterFormValues((prevValues) => ({
|
|
...prevValues,
|
|
...changedValues
|
|
}))
|
|
}
|
|
initialValues={initialNewPrinterForm}
|
|
>
|
|
<div style={{ minHeight: '260px' }}>{steps[currentStep].content}</div>
|
|
|
|
<Flex justify={'end'}>
|
|
<Button
|
|
style={{
|
|
margin: '0 8px'
|
|
}}
|
|
onClick={() => setCurrentStep(currentStep - 1)}
|
|
disabled={!(currentStep > 0)}
|
|
>
|
|
Previous
|
|
</Button>
|
|
{currentStep < steps.length - 1 && (
|
|
<Button
|
|
type='primary'
|
|
disabled={!nextEnabled}
|
|
onClick={() => {
|
|
setCurrentStep(currentStep + 1)
|
|
}}
|
|
>
|
|
Next
|
|
</Button>
|
|
)}
|
|
{currentStep === steps.length - 1 && (
|
|
<Button
|
|
type='primary'
|
|
htmlType='submit'
|
|
loading={newPrinterLoading}
|
|
>
|
|
Done
|
|
</Button>
|
|
)}
|
|
</Flex>
|
|
</Form>
|
|
</Flex>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
export default NewPrinter
|