Compare commits
No commits in common. "e32e48fa3a5409a1acaa81b0176071dd1712e414" and "a8932f1e6692963bf93b00cf71aff4573d4c47d0" have entirely different histories.
e32e48fa3a
...
a8932f1e66
@ -3350,9 +3350,3 @@ body.objectKanbanColumnResizing * {
|
|||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
stroke-width: 12px;
|
stroke-width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.printerQueue.ant-steps.ant-steps-vertical
|
|
||||||
> .ant-steps-item:last-child
|
|
||||||
.ant-steps-item-content {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,75 +1,29 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Flex } from 'antd'
|
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import NewObjectForm from '../../common/NewObjectForm'
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
import WizardView from '../../common/WizardView'
|
import WizardView from '../../common/WizardView'
|
||||||
import HostDeviceScanner, {
|
|
||||||
getHostId,
|
|
||||||
isNetworkScanCompatible
|
|
||||||
} from '../../common/HostDeviceScanner'
|
|
||||||
|
|
||||||
const NewDocumentPrinter = ({ onOk, defaultValues }) => {
|
const NewDocumentPrinter = ({ onOk, defaultValues }) => {
|
||||||
return (
|
return (
|
||||||
<NewObjectForm
|
<NewObjectForm
|
||||||
type={'documentPrinter'}
|
type={'documentPrinter'}
|
||||||
defaultValues={{
|
defaultValues={{ active: true, global: false, ...defaultValues }}
|
||||||
active: true,
|
|
||||||
global: false,
|
|
||||||
connection: {
|
|
||||||
interface: 'cups',
|
|
||||||
protocol: 'ipp',
|
|
||||||
port: 631
|
|
||||||
},
|
|
||||||
...defaultValues
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
handleSubmit,
|
|
||||||
submitLoading,
|
|
||||||
objectData,
|
|
||||||
formValid,
|
|
||||||
form,
|
|
||||||
setObjectData
|
|
||||||
}) => {
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
title: 'Required',
|
title: 'Required',
|
||||||
key: 'required',
|
key: 'required',
|
||||||
content: (
|
content: (
|
||||||
<Flex vertical gap='middle' style={{ width: '100%' }}>
|
<ObjectInfo
|
||||||
<ObjectInfo
|
type='documentPrinter'
|
||||||
type='documentPrinter'
|
column={1}
|
||||||
column={1}
|
bordered={false}
|
||||||
bordered={false}
|
isEditing={true}
|
||||||
isEditing={true}
|
required={true}
|
||||||
required={true}
|
labelWidth={100}
|
||||||
labelWidth={100}
|
objectData={objectData}
|
||||||
visibleProperties={{ 'connection.host': false }}
|
/>
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
{getHostId(objectData.host) &&
|
|
||||||
isNetworkScanCompatible(objectData.connection?.protocol) ? (
|
|
||||||
<HostDeviceScanner
|
|
||||||
objectType='documentPrinter'
|
|
||||||
host={objectData.host}
|
|
||||||
port={objectData.connection?.port}
|
|
||||||
protocol={objectData.connection?.protocol}
|
|
||||||
interface={objectData.connection?.interface}
|
|
||||||
form={form}
|
|
||||||
setObjectData={setObjectData}
|
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<ObjectInfo
|
|
||||||
type='documentPrinter'
|
|
||||||
column={1}
|
|
||||||
bordered={false}
|
|
||||||
isEditing={true}
|
|
||||||
labelWidth={100}
|
|
||||||
properties={['connection.host']}
|
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Flex } from 'antd'
|
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import NewObjectForm from '../../common/NewObjectForm'
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
import WizardView from '../../common/WizardView'
|
import WizardView from '../../common/WizardView'
|
||||||
import HostDeviceScanner, {
|
|
||||||
getHostId,
|
|
||||||
isNetworkScanCompatible
|
|
||||||
} from '../../common/HostDeviceScanner'
|
|
||||||
|
|
||||||
const NewPrinter = ({ onOk, defaultValues }) => {
|
const NewPrinter = ({ onOk, defaultValues }) => {
|
||||||
return (
|
return (
|
||||||
@ -21,52 +16,21 @@ const NewPrinter = ({ onOk, defaultValues }) => {
|
|||||||
...defaultValues
|
...defaultValues
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
handleSubmit,
|
|
||||||
submitLoading,
|
|
||||||
objectData,
|
|
||||||
formValid,
|
|
||||||
form,
|
|
||||||
setObjectData
|
|
||||||
}) => {
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
title: 'Required',
|
title: 'Required',
|
||||||
key: 'required',
|
key: 'required',
|
||||||
content: (
|
content: (
|
||||||
<Flex vertical gap='middle' style={{ width: '100%' }}>
|
<ObjectInfo
|
||||||
<ObjectInfo
|
type='printer'
|
||||||
type='printer'
|
column={1}
|
||||||
column={1}
|
labelWidth='100px'
|
||||||
labelWidth='100px'
|
bordered={false}
|
||||||
bordered={false}
|
isEditing={true}
|
||||||
isEditing={true}
|
required={true}
|
||||||
required={true}
|
objectData={objectData}
|
||||||
visibleProperties={{ 'moonraker.host': false }}
|
/>
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
{getHostId(objectData.host) &&
|
|
||||||
isNetworkScanCompatible(objectData.moonraker?.protocol) ? (
|
|
||||||
<HostDeviceScanner
|
|
||||||
objectType='printer'
|
|
||||||
host={objectData.host}
|
|
||||||
port={objectData.moonraker?.port}
|
|
||||||
protocol={objectData.moonraker?.protocol}
|
|
||||||
form={form}
|
|
||||||
setObjectData={setObjectData}
|
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<ObjectInfo
|
|
||||||
type='printer'
|
|
||||||
column={1}
|
|
||||||
labelWidth='100px'
|
|
||||||
bordered={false}
|
|
||||||
isEditing={true}
|
|
||||||
properties={['moonraker.host']}
|
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,359 +0,0 @@
|
|||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Flex, Typography } from 'antd'
|
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
|
||||||
import { mergeFormData } from '../utils/Utils'
|
|
||||||
import ProgressDisplay from './ProgressDisplay'
|
|
||||||
import MissingPlaceholder from './MissingPlaceholder'
|
|
||||||
import PrinterIcon from '../../Icons/PrinterIcon'
|
|
||||||
import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon'
|
|
||||||
import ElipsisText from './ElipsisText'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
export const getHostId = (host) => {
|
|
||||||
if (!host) return null
|
|
||||||
if (typeof host === 'string') return host
|
|
||||||
if (typeof host._id === 'string') return host._id
|
|
||||||
if (typeof host._id?._id === 'string') return host._id._id
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isNetworkScanCompatible = (protocol) =>
|
|
||||||
protocol !== 'serial' && protocol !== 'system'
|
|
||||||
|
|
||||||
const deviceKey = (device) => device?.ip || device?.hostname
|
|
||||||
|
|
||||||
const HostDeviceScanner = ({
|
|
||||||
objectType,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
interface: connectionInterface,
|
|
||||||
form,
|
|
||||||
setObjectData,
|
|
||||||
objectData
|
|
||||||
}) => {
|
|
||||||
const { connected, sendObjectAction, subscribeToObjectEvent } =
|
|
||||||
useContext(ApiServerContext)
|
|
||||||
const sendObjectActionRef = useRef(sendObjectAction)
|
|
||||||
sendObjectActionRef.current = sendObjectAction
|
|
||||||
const subscribeToObjectEventRef = useRef(subscribeToObjectEvent)
|
|
||||||
subscribeToObjectEventRef.current = subscribeToObjectEvent
|
|
||||||
const sessionIdRef = useRef(
|
|
||||||
typeof crypto !== 'undefined' && crypto.randomUUID
|
|
||||||
? crypto.randomUUID()
|
|
||||||
: `scan-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const [scanning, setScanning] = useState(false)
|
|
||||||
const [progress, setProgress] = useState(0)
|
|
||||||
const [currentIP, setCurrentIP] = useState(null)
|
|
||||||
const [devices, setDevices] = useState([])
|
|
||||||
const [selectedKey, setSelectedKey] = useState(null)
|
|
||||||
const [scanError, setScanError] = useState(null)
|
|
||||||
|
|
||||||
const hostId = getHostId(host)
|
|
||||||
const compatible = isNetworkScanCompatible(protocol)
|
|
||||||
|
|
||||||
const DeviceIcon =
|
|
||||||
objectType === 'documentPrinter' ? DocumentPrinterIcon : PrinterIcon
|
|
||||||
|
|
||||||
const scanDataRef = useRef({
|
|
||||||
objectType,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
interface: connectionInterface
|
|
||||||
})
|
|
||||||
scanDataRef.current = {
|
|
||||||
objectType,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
interface: connectionInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hostId || connected !== true || !compatible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false
|
|
||||||
const sessionId = sessionIdRef.current
|
|
||||||
|
|
||||||
const isSessionEvent = (event) => event?.data?.sessionId === sessionId
|
|
||||||
|
|
||||||
const startScan = () => {
|
|
||||||
if (cancelled) return
|
|
||||||
|
|
||||||
setProgress(0)
|
|
||||||
setCurrentIP(null)
|
|
||||||
setScanError(null)
|
|
||||||
setScanning(true)
|
|
||||||
|
|
||||||
sendObjectActionRef.current(
|
|
||||||
hostId,
|
|
||||||
'host',
|
|
||||||
{
|
|
||||||
type: 'scanNetwork',
|
|
||||||
data: {
|
|
||||||
...scanDataRef.current,
|
|
||||||
sessionId,
|
|
||||||
loop: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
if (cancelled) return
|
|
||||||
if (result?.success === false) {
|
|
||||||
setScanning(false)
|
|
||||||
setScanError(result.error || 'Failed to start network scan')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFound = (event) => {
|
|
||||||
if (!isSessionEvent(event)) return
|
|
||||||
const device = event?.data
|
|
||||||
if (!device?.ip && !device?.hostname) return
|
|
||||||
setDevices((prev) => {
|
|
||||||
if (prev.some((item) => deviceKey(item) === deviceKey(device))) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
return [...prev, device]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onProgress = (event) => {
|
|
||||||
if (!isSessionEvent(event)) return
|
|
||||||
const data = event?.data || {}
|
|
||||||
if (typeof data.progress === 'number') {
|
|
||||||
setProgress(Math.round(data.progress))
|
|
||||||
}
|
|
||||||
if (data.currentIP) {
|
|
||||||
setCurrentIP(data.currentIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onComplete = (event) => {
|
|
||||||
if (!isSessionEvent(event) || cancelled) return
|
|
||||||
|
|
||||||
const data = event?.data || {}
|
|
||||||
if (data.stopped) {
|
|
||||||
setScanning(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (data.success === false) {
|
|
||||||
setScanning(false)
|
|
||||||
setScanError(data.error || 'Network scan failed')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data.devices)) {
|
|
||||||
setDevices((prev) => {
|
|
||||||
const next = [...prev]
|
|
||||||
data.devices.forEach((device) => {
|
|
||||||
if (!next.some((item) => deviceKey(item) === deviceKey(device))) {
|
|
||||||
next.push(device)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgress(data.looping ? 0 : 100)
|
|
||||||
if (!data.looping) {
|
|
||||||
setScanning(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubFound = subscribeToObjectEventRef.current(
|
|
||||||
hostId,
|
|
||||||
'host',
|
|
||||||
'scanNetworkFound',
|
|
||||||
onFound
|
|
||||||
)
|
|
||||||
const unsubProgress = subscribeToObjectEventRef.current(
|
|
||||||
hostId,
|
|
||||||
'host',
|
|
||||||
'scanNetworkProgress',
|
|
||||||
onProgress
|
|
||||||
)
|
|
||||||
const unsubComplete = subscribeToObjectEventRef.current(
|
|
||||||
hostId,
|
|
||||||
'host',
|
|
||||||
'scanNetworkComplete',
|
|
||||||
onComplete
|
|
||||||
)
|
|
||||||
|
|
||||||
setDevices([])
|
|
||||||
setSelectedKey(null)
|
|
||||||
startScan()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
if (unsubFound) unsubFound()
|
|
||||||
if (unsubProgress) unsubProgress()
|
|
||||||
if (unsubComplete) unsubComplete()
|
|
||||||
sendObjectActionRef.current(hostId, 'host', {
|
|
||||||
type: 'scanNetworkStop',
|
|
||||||
data: { sessionId }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
hostId,
|
|
||||||
connected,
|
|
||||||
compatible,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
connectionInterface,
|
|
||||||
objectType
|
|
||||||
])
|
|
||||||
|
|
||||||
const applyDevice = useCallback(
|
|
||||||
(device) => {
|
|
||||||
const deviceHost = device?.hostname || device?.ip
|
|
||||||
if (!deviceHost) return
|
|
||||||
|
|
||||||
const nextValues =
|
|
||||||
objectType === 'documentPrinter'
|
|
||||||
? {
|
|
||||||
connection: {
|
|
||||||
...(objectData?.connection || {}),
|
|
||||||
host: deviceHost,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
interface: connectionInterface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
moonraker: {
|
|
||||||
...(objectData?.moonraker || {}),
|
|
||||||
host: deviceHost,
|
|
||||||
port,
|
|
||||||
protocol
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!objectData?.name) {
|
|
||||||
nextValues.name = deviceHost
|
|
||||||
}
|
|
||||||
|
|
||||||
form?.setFieldsValue(nextValues)
|
|
||||||
setObjectData?.((prev) => mergeFormData(prev || {}, nextValues))
|
|
||||||
setSelectedKey(deviceKey(device))
|
|
||||||
},
|
|
||||||
[
|
|
||||||
objectType,
|
|
||||||
objectData,
|
|
||||||
port,
|
|
||||||
protocol,
|
|
||||||
connectionInterface,
|
|
||||||
form,
|
|
||||||
setObjectData
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const statusLabel = useMemo(() => {
|
|
||||||
if (scanError) {
|
|
||||||
return scanError
|
|
||||||
}
|
|
||||||
if (currentIP) {
|
|
||||||
return `Scanning ${currentIP}...`
|
|
||||||
}
|
|
||||||
if (scanning) {
|
|
||||||
return 'Scanning...'
|
|
||||||
}
|
|
||||||
return 'Waiting...'
|
|
||||||
}, [scanning, currentIP, scanError])
|
|
||||||
|
|
||||||
if (!hostId || !compatible) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
vertical
|
|
||||||
gap={10}
|
|
||||||
style={{ width: '100%', marginBottom: 6, marginTop: 4 }}
|
|
||||||
>
|
|
||||||
{statusLabel ? (
|
|
||||||
<ElipsisText style={{ width: '100%', minWidth: 0 }}>
|
|
||||||
{statusLabel}
|
|
||||||
</ElipsisText>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ProgressDisplay
|
|
||||||
percent={Math.round(progress == 100 ? 0 : progress)}
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
status={scanning ? 'active' : scanError ? 'exception' : 'normal'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{devices.length === 0 ? (
|
|
||||||
<MissingPlaceholder
|
|
||||||
message={scanning ? 'Looking for devices...' : 'No devices found.'}
|
|
||||||
hasBackground={false}
|
|
||||||
mutedBorder
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Flex vertical gap={8} style={{ width: '100%' }}>
|
|
||||||
{devices.map((device) => {
|
|
||||||
const key = deviceKey(device)
|
|
||||||
const selected = selectedKey === key
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
onClick={() => applyDevice(device)}
|
|
||||||
style={{
|
|
||||||
textAlign: 'left',
|
|
||||||
cursor: 'pointer',
|
|
||||||
background: selected
|
|
||||||
? 'color-mix(in srgb, var(--color-primary) 12%, transparent)'
|
|
||||||
: 'transparent',
|
|
||||||
border: selected
|
|
||||||
? '1px solid var(--color-primary)'
|
|
||||||
: '1px solid var(--color-descriptions-border)',
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: '14px 18px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex align='center' gap={10}>
|
|
||||||
<DeviceIcon style={{ fontSize: 20 }} />
|
|
||||||
<Flex vertical style={{ minWidth: 0 }}>
|
|
||||||
<Text strong ellipsis>
|
|
||||||
{device.hostname || device.ip}
|
|
||||||
</Text>
|
|
||||||
{device.hostname && device.ip ? (
|
|
||||||
<Text type='secondary' ellipsis>
|
|
||||||
{device.ip}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HostDeviceScanner.propTypes = {
|
|
||||||
objectType: PropTypes.oneOf(['printer', 'documentPrinter']).isRequired,
|
|
||||||
host: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
|
||||||
port: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
||||||
protocol: PropTypes.string,
|
|
||||||
interface: PropTypes.string,
|
|
||||||
form: PropTypes.object,
|
|
||||||
setObjectData: PropTypes.func,
|
|
||||||
objectData: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HostDeviceScanner
|
|
||||||
@ -49,7 +49,6 @@ const PrinterQueue = ({ queue, currentSubJob }) => {
|
|||||||
<Steps
|
<Steps
|
||||||
current={currentIndex}
|
current={currentIndex}
|
||||||
direction='vertical'
|
direction='vertical'
|
||||||
className='printerQueue'
|
|
||||||
size='small'
|
size='small'
|
||||||
items={items.map((item) => ({
|
items={items.map((item) => ({
|
||||||
title: <ObjectDisplay object={item} objectType='subJob' showHyperlink />
|
title: <ObjectDisplay object={item} objectType='subJob' showHyperlink />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user