Enhance Document and Printer Components with Host Device Scanning
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced HostDeviceScanner component to facilitate network scanning for document printers and printers. - Updated NewDocumentPrinter and NewPrinter components to integrate HostDeviceScanner, allowing users to scan for compatible devices based on connection settings. - Enhanced ObjectInfo displays within both components to improve visibility of connection properties and device information. - Refactored default values to include connection settings for better initial configuration.
This commit is contained in:
parent
2fcab27cd4
commit
e32e48fa3a
@ -1,20 +1,42 @@
|
|||||||
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={{ 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 = [
|
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}
|
||||||
@ -22,8 +44,32 @@ const NewDocumentPrinter = ({ onOk, defaultValues }) => {
|
|||||||
isEditing={true}
|
isEditing={true}
|
||||||
required={true}
|
required={true}
|
||||||
labelWidth={100}
|
labelWidth={100}
|
||||||
|
visibleProperties={{ 'connection.host': false }}
|
||||||
objectData={objectData}
|
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 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 (
|
||||||
@ -16,12 +21,20 @@ 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}
|
||||||
@ -29,8 +42,31 @@ const NewPrinter = ({ onOk, defaultValues }) => {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
isEditing={true}
|
isEditing={true}
|
||||||
required={true}
|
required={true}
|
||||||
|
visibleProperties={{ 'moonraker.host': false }}
|
||||||
objectData={objectData}
|
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
|
||||||
Loading…
x
Reference in New Issue
Block a user