Refactored state display components to utilize StateTag for consistent state representation across AuditLogs, JobState, PrinterState, SubJobState, and others. Removed redundant badge logic and improved ObjectSelect for multi-select capabilities. Cleaned up unused props and streamlined vendor selection process.

This commit is contained in:
Tom Butcher 2025-06-28 02:55:35 +01:00
parent f220d81722
commit 859ae656d6
10 changed files with 208 additions and 390 deletions

View File

@ -24,6 +24,7 @@ import AuditLogIcon from '../../Icons/AuditLogIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import BoolDisplay from '../common/BoolDisplay'
import StateTag from '../common/StateTag'
const { Text } = Typography
@ -48,9 +49,7 @@ const formatValue = (value, propertyName) => {
}
if (propertyName === 'state' && typeof value === 'object' && value.type) {
return (
<Text>{value.type.charAt(0).toUpperCase() + value.type.slice(1)}</Text>
)
return <StateTag state={value.type} />
}
// Check if the value is a timestamp (ISO date string)

View File

@ -5,6 +5,7 @@ import IdText from './IdText'
import { AuditOutlined, LoadingOutlined } from '@ant-design/icons'
import TimeDisplay from '../common/TimeDisplay'
import BoolDisplay from './BoolDisplay'
import StateTag from './StateTag'
const { Text } = Typography
@ -33,9 +34,7 @@ const formatValue = (value, propertyName) => {
}
if (propertyName === 'state' && typeof value === 'object' && value.type) {
return (
<Text>{value.type.charAt(0).toUpperCase() + value.type.slice(1)}</Text>
)
return <StateTag state={value.type} />
}
// Check if the value is a timestamp (ISO date string)

View File

@ -28,9 +28,4 @@ FilamentSelect.propTypes = {
useFilter: PropTypes.bool
}
FilamentSelect.defaultProps = {
filter: {},
useFilter: false
}
export default FilamentSelect

View File

@ -1,8 +1,9 @@
import PropTypes from 'prop-types'
import { Badge, Progress, Flex, Typography, Tag, Space } from 'antd'
import { Progress, Flex, Typography, Space } from 'antd'
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
import IdText from './IdText'
import StateTag from './StateTag'
const JobState = ({
job,
@ -12,13 +13,12 @@ const JobState = ({
showQuantity = true
}) => {
const { socket } = useContext(SocketContext)
const [badgeStatus, setBadgeStatus] = useState('default')
const [badgeText, setBadgeText] = useState('Unknown')
const [currentState, setCurrentState] = useState(
job?.state || { type: 'unknown', progress: 0 }
)
const [initialized, setInitialized] = useState(false)
const { Text } = Typography
useEffect(() => {
if (socket && !initialized && job?._id) {
setInitialized(true)
@ -35,38 +35,6 @@ const JobState = ({
}
}, [socket, initialized, job?._id])
useEffect(() => {
switch (currentState?.type) {
case 'draft':
setBadgeStatus('default')
setBadgeText('Draft')
break
case 'printing':
setBadgeStatus('processing')
setBadgeText('Printing')
break
case 'complete':
setBadgeStatus('success')
setBadgeText('Complete')
break
case 'failed':
setBadgeStatus('error')
setBadgeText('Failed')
break
case 'queued':
setBadgeStatus('warning')
setBadgeText('Queued')
break
case 'paused':
setBadgeStatus('warning')
setBadgeText('Paused')
break
default:
setBadgeStatus('default')
setBadgeText('Unknown')
}
}, [currentState])
return (
<Flex gap='small' align={'center'}>
{showId && (
@ -75,12 +43,7 @@ const JobState = ({
{showQuantity && <Text>({job.quantity})</Text>}
{showStatus && (
<Space>
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
<Flex gap={6}>
<Badge status={badgeStatus} />
{badgeText}
</Flex>
</Tag>
<StateTag state={currentState?.type} />
</Space>
)}
{showProgress &&

View File

@ -4,8 +4,9 @@ import { TreeSelect, Typography, Flex, Badge } from 'antd'
import axios from 'axios'
import { getTypeMeta } from '../utils/Utils'
import IdText from './IdText'
import CountryDisplay from './CountryDisplay'
const { Text } = Typography
const { SHOW_CHILD } = TreeSelect
/**
* ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection.
*
@ -14,9 +15,10 @@ const { Text } = Typography
* - propertyOrder: array of property names for category levels (required)
* - filter: object for filtering (optional)
* - useFilter: bool (optional)
* - value: selected value (optional) - can be an object with _id or a simple value
* - value: selected value (optional) - can be an object with _id, array of objects, or simple value/array
* - onChange: function (optional)
* - showSearch: bool (optional, default false)
* - treeCheckable: bool (optional, default false) - enables multi-select mode with checkboxes
* - treeSelectProps: any other TreeSelect props (optional)
*/
const ObjectSelect = ({
@ -27,13 +29,14 @@ const ObjectSelect = ({
value,
onChange,
showSearch = false,
treeCheckable = false,
treeSelectProps = {},
type = 'unknown',
...rest
}) => {
const [treeData, setTreeData] = useState([])
const [loading, setLoading] = useState(false)
const [defaultValue, setDefaultValue] = useState(value)
const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
const [searchValue, setSearchValue] = useState('')
const [error, setError] = useState(false)
@ -102,7 +105,12 @@ const ObjectSelect = ({
const renderTitle = useCallback(
(item, isLeaf) => {
if (!isLeaf) {
// For category nodes, just show the value
// For category nodes, check if it's a country property
const currentProperty = propertyOrder[item.propertyId]
if (currentProperty === 'country' && item.value) {
return <CountryDisplay countryCode={item.value} />
}
// For other category nodes, just show the value
return <Text>{item[propertyOrder[item.propertyId]] || item.value}</Text>
}
// For leaf nodes, show icon, name, and id
@ -129,8 +137,9 @@ const ObjectSelect = ({
let currentPId = 0
// Build category nodes for each property level and load all available options
for (let i = 0; i < propertyOrder.length - 1; i++) {
for (let i = 0; i < propertyOrder.length; i++) {
const propertyName = propertyOrder[i]
console.log('propname', propertyName)
let propertyValue
// Handle nested property access (e.g., 'filament.diameter')
@ -372,9 +381,25 @@ const ObjectSelect = ({
// OnChange handler
const handleOnChange = (val, selectedOptions) => {
if (onChange) {
// Find the raw object for the selected value
const node = treeData.find((n) => n.value === val)
onChange(node ? node.raw : val, selectedOptions)
if (treeCheckable) {
// Handle multiple selections with checkboxes
const selectedObjects = []
if (Array.isArray(val)) {
val.forEach((selectedValue) => {
const node = treeData.find((n) => n.value === selectedValue)
if (node) {
selectedObjects.push(node.raw)
} else {
selectedObjects.push(selectedValue)
}
})
}
onChange(selectedObjects, selectedOptions)
} else {
// Handle single selection
const node = treeData.find((n) => n.value === val)
onChange(node ? node.raw : val, selectedOptions)
}
}
console.log('val', val)
setDefaultValue(val)
@ -388,29 +413,56 @@ const ObjectSelect = ({
// Keep defaultValue in sync and handle object values
useEffect(() => {
if (value?._id) {
setDefaultValue(value._id)
}
if (treeCheckable) {
// Handle array of values for multi-select
if (Array.isArray(value)) {
const valueIds = value.map((v) => v._id || v.id || v)
setDefaultValue(valueIds)
// Check if value is an object with _id (default object case)
if (value && typeof value === 'object' && value._id) {
// If we already have this object loaded, don't fetch again
const existingNode = treeData.find((node) => node.value === value._id)
if (!existingNode) {
fetchObjectById(value._id).then((object) => {
if (object) {
buildTreePathForObject(object)
// Load tree paths for any objects that aren't already loaded
value.forEach((item) => {
if (item && typeof item === 'object' && item._id) {
const existingNode = treeData.find(
(node) => node.value === item._id
)
if (!existingNode) {
fetchObjectById(item._id).then((object) => {
if (object) {
buildTreePathForObject(object)
}
})
}
}
})
} else {
setDefaultValue([])
}
} else {
// Handle single value
if (value?._id) {
setDefaultValue(value._id)
}
// Check if value is an object with _id (default object case)
if (value && typeof value === 'object' && value._id) {
// If we already have this object loaded, don't fetch again
const existingNode = treeData.find((node) => node.value === value._id)
if (!existingNode) {
fetchObjectById(value._id).then((object) => {
if (object) {
buildTreePathForObject(object)
}
})
}
}
}
}, [value, treeData, fetchObjectById, buildTreePathForObject])
}, [value, treeData, fetchObjectById, buildTreePathForObject, treeCheckable])
// Initial load
useEffect(() => {
if (treeData.length === 0 && !error && !loading) {
// If we have a default object value, don't load the regular tree
if (value && typeof value === 'object' && value._id) {
if (!treeCheckable && value && typeof value === 'object' && value._id) {
return
}
@ -429,7 +481,8 @@ const ObjectSelect = ({
handleTreeLoad,
error,
loading,
value
value,
treeCheckable
])
return error ? (
@ -455,6 +508,8 @@ const ObjectSelect = ({
value={defaultValue}
showSearch={showSearch}
onSearch={showSearch ? handleSearch : undefined}
treeCheckable={treeCheckable}
showCheckedStrategy={treeCheckable ? SHOW_CHILD : undefined}
{...treeSelectProps}
{...rest}
/>
@ -469,6 +524,7 @@ ObjectSelect.propTypes = {
value: PropTypes.any,
onChange: PropTypes.func,
showSearch: PropTypes.bool,
treeCheckable: PropTypes.bool,
treeSelectProps: PropTypes.object,
type: PropTypes.string.isRequired
}

View File

@ -1,36 +1,18 @@
// PrinterSelect.js
import React from 'react'
import PropTypes from 'prop-types'
import { Tag } from 'antd'
import config from '../../../config'
import ObjectSelect from './ObjectSelect'
import PrinterState from './PrinterState'
const PrinterSelect = ({ onChange, disabled }) => {
// getTitle: if isLeaf, render PrinterState, else render Tag or 'Untagged'
const getTitle = (item, isLeaf) =>
isLeaf ? (
<PrinterState printer={item} showProgress={false} showControls={false} />
) : item === 'Untagged' ? (
'Untagged'
) : (
<Tag color='blue'>{item}</Tag>
)
// getValue/getKey: for leaf, use _id; for tag, use tag string
const getValue = (item, isLeaf) => (isLeaf ? item._id : item)
const getKey = (item, isLeaf) => (isLeaf ? item._id : item)
return (
<ObjectSelect
endpoint={`${config.backendUrl}/printers`}
propertyOrder={['tags']}
getTitle={getTitle}
getValue={getValue}
getKey={getKey}
onChange={onChange}
disabled={disabled}
placeholder='Select Printer'
type='printer'
/>
)
}

View File

@ -1,20 +1,12 @@
// PrinterSelect.js
import PropTypes from 'prop-types'
import {
Badge,
Progress,
Flex,
Space,
Tag,
Typography,
Button,
Tooltip
} from 'antd'
import { Progress, Flex, Space, Typography, Button, Tooltip } from 'antd'
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
import { CaretLeftOutlined } from '@ant-design/icons'
import XMarkIcon from '../../Icons/XMarkIcon'
import PauseIcon from '../../Icons/PauseIcon'
import StateTag from './StateTag'
const PrinterState = ({
printer,
@ -24,8 +16,6 @@ const PrinterState = ({
showControls = true
}) => {
const { socket } = useContext(SocketContext)
const [badgeStatus, setBadgeStatus] = useState('unknown')
const [badgeText, setBadgeText] = useState('Unknown')
const [currentState, setCurrentState] = useState(
printer?.state || {
type: 'unknown',
@ -51,81 +41,12 @@ const PrinterState = ({
}
}, [socket, initialized, printer?.id])
useEffect(() => {
switch (currentState.type) {
case 'online':
setBadgeStatus('success')
setBadgeText('Online')
break
case 'standby':
setBadgeStatus('success')
setBadgeText('Standby')
break
case 'complete':
setBadgeStatus('success')
setBadgeText('Complete')
break
case 'offline':
setBadgeStatus('default')
setBadgeText('Offline')
break
case 'shutdown':
setBadgeStatus('default')
setBadgeText('Shutdown')
break
case 'initializing':
setBadgeStatus('warning')
setBadgeText('Initializing')
break
case 'printing':
setBadgeStatus('processing')
setBadgeText('Printing')
break
case 'paused':
setBadgeStatus('warning')
setBadgeText('Paused')
break
case 'cancelled':
setBadgeStatus('warning')
setBadgeText('Cancelled')
break
case 'loading':
setBadgeStatus('processing')
setBadgeText('Uploading')
break
case 'processing':
setBadgeStatus('processing')
setBadgeText('Processing')
break
case 'ready':
setBadgeStatus('success')
setBadgeText('Ready')
break
case 'error':
setBadgeStatus('error')
setBadgeText('Error')
break
case 'startup':
setBadgeStatus('warning')
setBadgeText('Startup')
break
default:
setBadgeStatus('default')
setBadgeText(currentState.type)
}
}, [currentState])
return (
<Flex gap='small' align={'center'}>
{showPrinterName && <Text>{printer.name}</Text>}
{showStatus && (
<Space>
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
<Flex gap={6}>
<Badge status={badgeStatus} />
{badgeText}
</Flex>
</Tag>
<StateTag state={currentState.type} />
</Space>
)}
{showProgress &&

View File

@ -0,0 +1,103 @@
import PropTypes from 'prop-types'
import { Badge, Flex, Tag } from 'antd'
import React, { useMemo } from 'react'
const StateTag = ({ state, showBadge = true, style = {} }) => {
const { badgeStatus, badgeText } = useMemo(() => {
let status = 'default'
let text = 'Unknown'
switch (state) {
case 'online':
status = 'success'
text = 'Online'
break
case 'standby':
status = 'success'
text = 'Standby'
break
case 'complete':
status = 'success'
text = 'Complete'
break
case 'offline':
status = 'default'
text = 'Offline'
break
case 'shutdown':
status = 'default'
text = 'Shutdown'
break
case 'initializing':
status = 'warning'
text = 'Initializing'
break
case 'printing':
status = 'processing'
text = 'Printing'
break
case 'paused':
status = 'warning'
text = 'Paused'
break
case 'cancelled':
status = 'error'
text = 'Cancelled'
break
case 'loading':
status = 'processing'
text = 'Uploading'
break
case 'processing':
status = 'processing'
text = 'Processing'
break
case 'ready':
status = 'success'
text = 'Ready'
break
case 'error':
status = 'error'
text = 'Error'
break
case 'startup':
status = 'warning'
text = 'Startup'
break
case 'draft':
status = 'default'
text = 'Draft'
break
case 'failed':
status = 'error'
text = 'Failed'
break
case 'queued':
status = 'warning'
text = 'Queued'
break
default:
status = 'default'
text = state || 'Unknown'
}
return { badgeStatus: status, badgeText: text }
}, [state])
return (
<Tag color={badgeStatus} style={{ marginRight: 0, ...style }}>
<Flex gap={6}>
{showBadge && <Badge status={badgeStatus} />}
{badgeText}
</Flex>
</Tag>
)
}
StateTag.propTypes = {
state: PropTypes.string,
showBadge: PropTypes.bool,
style: PropTypes.object
}
export default StateTag

View File

@ -1,9 +1,10 @@
import PropTypes from 'prop-types'
import { Badge, Progress, Flex, Button, Space, Tag, Tooltip } from 'antd' // eslint-disable-line
import { Progress, Flex, Button, Space, Tooltip } from 'antd' // eslint-disable-line
import { CaretLeftOutlined } from '@ant-design/icons' // eslint-disable-line
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
import IdText from './IdText'
import StateTag from './StateTag'
import XMarkIcon from '../../Icons/XMarkIcon'
import PauseIcon from '../../Icons/PauseIcon'
import BinIcon from '../../Icons/BinIcon'
@ -16,8 +17,6 @@ const SubJobState = ({
showControls = true //eslint-disable-line
}) => {
const { socket } = useContext(SocketContext)
const [badgeStatus, setBadgeStatus] = useState('unknown')
const [badgeText, setBadgeText] = useState('Unknown')
const [currentState, setCurrentState] = useState(
subJob?.state || {
type: 'unknown',
@ -45,42 +44,6 @@ const SubJobState = ({
}
}, [socket, initialized, subJob?._id])
useEffect(() => {
switch (currentState.type) {
case 'draft':
setBadgeStatus('default')
setBadgeText('Draft')
break
case 'printing':
setBadgeStatus('processing')
setBadgeText('Printing')
break
case 'complete':
setBadgeStatus('success')
setBadgeText('Complete')
break
case 'failed':
setBadgeStatus('error')
setBadgeText('Failed')
break
case 'queued':
setBadgeStatus('warning')
setBadgeText('Queued')
break
case 'paused':
setBadgeStatus('warning')
setBadgeText('Paused')
break
case 'cancelled':
setBadgeStatus('error')
setBadgeText('Cancelled')
break
default:
setBadgeStatus('default')
setBadgeText('Unknown')
}
}, [currentState])
return (
<Flex gap='small' align={'center'}>
{showId && (
@ -88,12 +51,7 @@ const SubJobState = ({
)}
{showStatus && (
<Space>
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
<Flex gap={6}>
<Badge status={badgeStatus} />
{badgeText}
</Flex>
</Tag>
<StateTag state={currentState?.type} />
</Space>
)}
{showProgress &&

View File

@ -1,188 +1,30 @@
import { TreeSelect, Space } from 'antd'
import React, { useEffect, useState } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import CountryDisplay from './CountryDisplay'
import VendorIcon from '../../Icons/VendorIcon'
import config from '../../../config'
import ObjectSelect from './ObjectSelect'
const propertyOrder = ['country']
const VendorSelect = ({ onChange, filter = {}, useFilter = false, value }) => {
const [vendorsTreeData, setVendorsTreeData] = useState([])
const [loading, setLoading] = useState(true)
const [defaultValue, setDefaultValue] = useState(null)
const fetchVendorsData = async (property, filter) => {
setLoading(true)
try {
const response = await axios.get(`${config.backendUrl}/vendors`, {
params: {
...filter,
property
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
setLoading(false)
return response.data
} catch (err) {
console.error(err)
}
}
const getFilter = (node) => {
var filter = {}
var currentId = node.id
while (currentId != 0) {
const currentNode = vendorsTreeData.filter(
(treeData) => treeData['id'] === currentId
)[0]
filter[propertyOrder[currentNode.propertyId]] =
currentNode.value.split('-')[0]
currentId = currentNode.pId
}
return filter
}
const generateVendorTreeNodes = async (node = null, filter = null) => {
if (!node) {
return
}
if (filter === null) {
filter = getFilter(node)
}
const vendorData = await fetchVendorsData(null, filter)
let newNodeList = []
for (const vendor of vendorData) {
const random = Math.random().toString(36).substring(2, 6)
const newNode = {
id: random,
pId: node.id,
value: vendor._id,
vendor: vendor,
key: vendor._id,
title: (
<Space>
<VendorIcon />
{vendor.name}
</Space>
),
isLeaf: true
}
newNodeList.push(newNode)
}
setVendorsTreeData(vendorsTreeData.concat(newNodeList))
}
const generateVendorCategoryTreeNodes = async (node = null) => {
var filter = {}
var propertyId = 0
if (!node) {
node = {}
node.id = 0
} else {
filter = getFilter(node)
propertyId = node.propertyId + 1
}
const propertyName = propertyOrder[propertyId]
const propertyData = await fetchVendorsData(propertyName, filter)
const newNodeList = []
for (const item of propertyData) {
const property = item[propertyName]
const random = Math.random().toString(36).substring(2, 6)
const newNode = {
id: random,
pId: node.id,
value: property + '-' + random,
key: property + '-' + random,
propertyId: propertyId,
title: <CountryDisplay countryCode={property} />,
isLeaf: false,
selectable: false
}
newNodeList.push(newNode)
}
setVendorsTreeData(vendorsTreeData.concat(newNodeList))
}
const handleVendorsTreeLoad = async (node) => {
if (node) {
if (node.propertyId !== propertyOrder.length - 1) {
await generateVendorCategoryTreeNodes(node)
} else {
await generateVendorTreeNodes(node) // End of properties
}
} else {
await generateVendorCategoryTreeNodes(null) // First property
}
}
const handleOnChange = (value, selectedOptions) => {
const vendorObject = vendorsTreeData.find(
(node) => node.value === value
)?.vendor
onChange(vendorObject, selectedOptions)
}
useEffect(() => {
setVendorsTreeData([])
}, [])
useEffect(() => {
if (vendorsTreeData.length === 0) {
if (useFilter === true) {
generateVendorTreeNodes({ id: 0 }, filter)
} else {
handleVendorsTreeLoad(null)
}
}
}, [vendorsTreeData])
useEffect(() => {
console.log('value', value)
if (value?.name) {
setDefaultValue(value.name)
}
}, [value])
return (
<TreeSelect
treeDataSimpleMode
loadData={handleVendorsTreeLoad}
treeData={vendorsTreeData}
onChange={handleOnChange}
loading={loading}
<ObjectSelect
endpoint={`${config.backendUrl}/vendors`}
propertyOrder={propertyOrder}
filter={filter}
useFilter={useFilter}
value={value}
onChange={onChange}
placeholder='Select a vendor'
style={{ width: '100%' }}
value={defaultValue}
type={'vendor'}
/>
)
}
VendorSelect.propTypes = {
onChange: PropTypes.func,
value: PropTypes.object,
filter: PropTypes.object,
useFilter: PropTypes.bool,
value: PropTypes.object
useFilter: PropTypes.bool
}
export default VendorSelect