Compare commits
2 Commits
a8932f1e66
...
e32e48fa3a
| Author | SHA1 | Date | |
|---|---|---|---|
| e32e48fa3a | |||
| 2fcab27cd4 |
@ -3350,3 +3350,9 @@ body.objectKanbanColumnResizing * {
|
||||
stroke: currentColor;
|
||||
stroke-width: 12px;
|
||||
}
|
||||
|
||||
.printerQueue.ant-steps.ant-steps-vertical
|
||||
> .ant-steps-item:last-child
|
||||
.ant-steps-item-content {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@ -1,29 +1,75 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Flex } from 'antd'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import HostDeviceScanner, {
|
||||
getHostId,
|
||||
isNetworkScanCompatible
|
||||
} from '../../common/HostDeviceScanner'
|
||||
|
||||
const NewDocumentPrinter = ({ onOk, defaultValues }) => {
|
||||
return (
|
||||
<NewObjectForm
|
||||
type={'documentPrinter'}
|
||||
defaultValues={{ active: true, global: false, ...defaultValues }}
|
||||
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 = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='documentPrinter'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
labelWidth={100}
|
||||
objectData={objectData}
|
||||
/>
|
||||
<Flex vertical gap='middle' style={{ width: '100%' }}>
|
||||
<ObjectInfo
|
||||
type='documentPrinter'
|
||||
column={1}
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
labelWidth={100}
|
||||
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,7 +1,12 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Flex } from 'antd'
|
||||
import ObjectInfo from '../../common/ObjectInfo'
|
||||
import NewObjectForm from '../../common/NewObjectForm'
|
||||
import WizardView from '../../common/WizardView'
|
||||
import HostDeviceScanner, {
|
||||
getHostId,
|
||||
isNetworkScanCompatible
|
||||
} from '../../common/HostDeviceScanner'
|
||||
|
||||
const NewPrinter = ({ onOk, defaultValues }) => {
|
||||
return (
|
||||
@ -16,21 +21,52 @@ const NewPrinter = ({ onOk, defaultValues }) => {
|
||||
...defaultValues
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||
{({
|
||||
handleSubmit,
|
||||
submitLoading,
|
||||
objectData,
|
||||
formValid,
|
||||
form,
|
||||
setObjectData
|
||||
}) => {
|
||||
const steps = [
|
||||
{
|
||||
title: 'Required',
|
||||
key: 'required',
|
||||
content: (
|
||||
<ObjectInfo
|
||||
type='printer'
|
||||
column={1}
|
||||
labelWidth='100px'
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
objectData={objectData}
|
||||
/>
|
||||
<Flex vertical gap='middle' style={{ width: '100%' }}>
|
||||
<ObjectInfo
|
||||
type='printer'
|
||||
column={1}
|
||||
labelWidth='100px'
|
||||
bordered={false}
|
||||
isEditing={true}
|
||||
required={true}
|
||||
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>
|
||||
)
|
||||
},
|
||||
{
|
||||
|
||||
359
src/components/Dashboard/common/HostDeviceScanner.jsx
Normal file
359
src/components/Dashboard/common/HostDeviceScanner.jsx
Normal file
@ -0,0 +1,359 @@
|
||||
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,6 +49,7 @@ const PrinterQueue = ({ queue, currentSubJob }) => {
|
||||
<Steps
|
||||
current={currentIndex}
|
||||
direction='vertical'
|
||||
className='printerQueue'
|
||||
size='small'
|
||||
items={items.map((item) => ({
|
||||
title: <ObjectDisplay object={item} objectType='subJob' showHyperlink />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user