Compare commits

..

No commits in common. "cd83679232e5b398773a015aedfb7fe667a2192a" and "2a18f3d697a7b5ca32ee0e5d6982e214b727351a" have entirely different histories.

18 changed files with 330 additions and 916 deletions

View File

@ -1,68 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Modal, Button, Space, Typography } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import BinIcon from '../../Icons/BinIcon'
const { Text } = Typography
const DeleteObjectModal = ({
open,
onOk,
onCancel,
loading,
objectType,
objectName
}) => {
const model = getModelByName(objectType)
return (
<Modal
open={open}
title={
<Space size={'middle'}>
<BinIcon />
{`Confirm Delete ${model.label}`}
</Space>
}
onOk={onOk}
onCancel={onCancel}
okText='Delete'
cancelText='Cancel'
okType='danger'
closable={false}
centered
maskClosable={false}
footer={[
<Button key='cancel' onClick={onCancel} disabled={loading}>
Cancel
</Button>,
<Button
key='delete'
type='primary'
danger
onClick={onOk}
loading={loading}
disabled={loading}
>
Delete
</Button>
]}
>
<Text>
Are you sure you want to delete this {model.label.toLowerCase()}
{objectName ? ` "${objectName}"` : ''}?
</Text>
</Modal>
)
}
DeleteObjectModal.propTypes = {
open: PropTypes.bool.isRequired,
onOk: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
loading: PropTypes.bool,
objectType: PropTypes.string.isRequired,
objectName: PropTypes.string
}
export default DeleteObjectModal

View File

@ -49,7 +49,7 @@ const IdDisplay = ({
}
return (
<Flex align={'end'} className='iddisplay'>
<Flex align={'center'} className='iddisplay'>
{(() => {
const content = (
<Flex gap={4}>

View File

@ -12,33 +12,33 @@ const InfoCollapse = ({
active,
onToggle,
className = '',
collapseKey = 'default'
key = 'default'
}) => {
return (
<Collapse
ghost
expandIconPosition='end'
activeKey={active ? [collapseKey] : []}
activeKey={active ? [key] : []}
onChange={(keys) => onToggle(keys.length > 0)}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className={`no-h-padding-collapse ${className}`}
items={[
{
key: collapseKey,
children: children,
label: (
<Flex align='center' gap={'middle'}>
{icon}
<Title level={5} style={{ margin: 0 }}>
{title}
</Title>
</Flex>
)
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
{icon}
<Title level={5} style={{ margin: 0 }}>
{title}
</Title>
</Flex>
}
]}
/>
key={key}
>
{children}
</Collapse.Panel>
</Collapse>
)
}
@ -49,7 +49,7 @@ InfoCollapse.propTypes = {
active: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
className: PropTypes.string,
collapseKey: PropTypes.string
key: PropTypes.string
}
export default InfoCollapse

View File

@ -26,7 +26,6 @@ import MarkdownDisplay from './MarkdownDisplay'
import axios from 'axios'
import config from '../../../config'
import { AuthContext } from '../context/AuthContext'
import { ApiServerContext } from '../context/ApiServerContext'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import NoteTypeSelect from './NoteTypeSelect'
import IdDisplay from './IdDisplay'
@ -260,7 +259,7 @@ NoteItem.propTypes = {
onChildNoteAdded: PropTypes.func
}
const NotesPanel = ({ _id, onNewNote, type }) => {
const NotesPanel = ({ _id, onNewNote }) => {
const [newNoteOpen, setNewNoteOpen] = useState(false)
const [showMarkdown, setShowMarkdown] = useState(false)
const [loading, setLoading] = useState(true)
@ -274,7 +273,6 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
const [expandedNotes, setExpandedNotes] = useState({})
const [newNoteForm] = Form.useForm()
const [selectedParentId, setSelectedParentId] = useState(null)
const [selectedParentType, setSelectedParentType] = useState(null)
const [childNoteCallbacks, setChildNoteCallbacks] = useState({})
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [noteToDelete, setNoteToDelete] = useState(null)
@ -291,33 +289,30 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
}, [newNoteForm, newNoteFormUpdateValues])
const { authenticated, userProfile } = useContext(AuthContext)
const { fetchNotes } = useContext(ApiServerContext)
const fetchData = useCallback(
async (id) => {
try {
const newData = await fetchNotes(id)
setLoading(false)
return newData
} catch (error) {
setNotes([])
setError(error)
setLoading(false)
}
},
[fetchNotes]
)
const fetchData = useCallback(async (id) => {
try {
const response = await axios.get(`${config.backendUrl}/notes`, {
params: {
parent: id,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const handleNewChildNote = useCallback(
(parentId) => {
setSelectedParentId(parentId)
setSelectedParentType('note')
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
},
[newNoteForm]
)
const newData = response.data
setLoading(false)
return newData
} catch (error) {
setNotes([])
setError(error)
setLoading(false)
}
}, [])
const generateNotes = useCallback(
async (id) => {
@ -350,7 +345,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
expandedNotes={expandedNotes}
setExpandedNotes={setExpandedNotes}
fetchData={fetchData}
onNewNote={handleNewChildNote}
onNewNote={handleNewNoteFromDropdown}
onDeleteNote={handleDeleteNote}
userProfile={userProfile}
onChildNoteAdded={(noteId, callback) => {
@ -362,7 +357,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
/>
))
},
[loading, fetchData, expandedNotes, userProfile, handleNewChildNote]
[loading, fetchData, expandedNotes, userProfile]
)
const handleNewNote = async () => {
@ -370,11 +365,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
try {
await axios.post(
`${config.backendUrl}/notes`,
{
...newNoteFormValues,
parent: selectedParentId,
parentType: selectedParentType
},
{ ...newNoteFormValues, parent: selectedParentId || _id },
{
withCredentials: true
}
@ -411,6 +402,13 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
}
}
const handleNewNoteFromDropdown = (parentId) => {
setSelectedParentId(parentId)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
}
const handleDeleteNote = async (noteId) => {
setNoteToDelete(noteId)
setDeleteConfirmOpen(true)
@ -501,8 +499,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
setLoading(true)
handleReloadData()
} else if (key === 'newNote') {
setSelectedParentId(_id)
setSelectedParentType(type)
setSelectedParentId(null)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
@ -525,8 +522,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
icon={<PlusIcon />}
disabled={loading}
onClick={() => {
setSelectedParentId(_id)
setSelectedParentType(type)
setSelectedParentId(null)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
@ -692,7 +688,6 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
NotesPanel.propTypes = {
_id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
onNewNote: PropTypes.func
}

View File

@ -14,7 +14,6 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id) {
const item = {
key: action.key || action.name,
label: action.label,
danger: action?.danger || false,
icon: action.icon ? React.createElement(action.icon) : undefined,
disabled: actionUrl && actionUrl === currentUrlWithActions
}
@ -54,10 +53,11 @@ const ObjectActions = ({
location.search
)
console.log('curr url', currentUrlWithoutActions)
// Filter out actions whose url matches currentUrl
const filteredActions = actions.filter(
(action) =>
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
(action) => !(action.url(id) && action.url(id) === currentUrlWithoutActions)
)
const currentUrlWithActions = location.pathname + location.search

View File

@ -13,7 +13,6 @@ const ObjectInfo = ({
properties = [],
required = undefined,
visibleProperties = {},
objectPropertyProps = {},
...rest
}) => {
const allItems = getModelProperties(type)
@ -56,7 +55,6 @@ const ObjectInfo = ({
children: (
<ObjectProperty
{...item}
{...objectPropertyProps}
isEditing={isEditing}
objectData={objectData}
/>
@ -93,8 +91,7 @@ ObjectInfo.propTypes = {
type: PropTypes.string.isRequired,
objectData: PropTypes.object,
required: PropTypes.bool,
visibleProperties: PropTypes.object,
objectPropertyProps: PropTypes.object
visibleProperties: PropTypes.object
}
export default ObjectInfo

View File

@ -1,49 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { List, Typography, Flex } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import IdDisplay from './IdDisplay'
const { Text } = Typography
const ObjectList = ({ value, objectType, bordered = true }) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return <Text type='secondary'>n/a</Text>
}
return (
<List
size='small'
bordered={bordered}
dataSource={value}
renderItem={(item) => {
const model = getModelByName(objectType)
const Icon = model.icon
return (
<List.Item>
<Flex gap={'small'} align='center'>
<Icon />
{item?.name ? <Text ellipsis>{item.name}</Text> : null}
{item?._id ? (
<IdDisplay
id={item?._id}
longId={false}
type={objectType}
showHyperlink={true}
/>
) : null}
</Flex>
</List.Item>
)
}}
style={{ width: '100%' }}
/>
)
}
ObjectList.propTypes = {
value: PropTypes.array,
bordered: PropTypes.bool,
objectType: PropTypes.string
}
export default ObjectList

View File

@ -37,10 +37,6 @@ import { getPropertyValue } from '../../../database/ObjectModels'
import PropertyChanges from './PropertyChanges'
import NetGrossDisplay from './NetGrossDisplay'
import NetGrossInput from './NetGrossInput'
import ObjectList from './ObjectList'
import VarianceDisplay from './VarianceDisplay'
import OperationDisplay from './OperationDisplay'
import MarkdownDisplay from './MarkdownDisplay'
const { Text } = Typography
@ -75,7 +71,7 @@ const ObjectProperty = ({
initial = false,
...rest
}) => {
if (value && typeof value == 'function' && objectData) {
if (typeof value == 'function' && objectData) {
value = value(objectData)
}
@ -91,14 +87,6 @@ const ObjectProperty = ({
difference = difference(objectData)
}
if (prefix && typeof prefix == 'function' && objectData) {
prefix = prefix(objectData)
}
if (suffix && typeof suffix == 'function' && objectData) {
suffix = suffix(objectData)
}
if (!value) {
value = getPropertyValue(objectData, name)
}
@ -230,19 +218,6 @@ const ObjectProperty = ({
)
}
}
case 'variance': {
if (value != null) {
return (
<VarianceDisplay value={value} prefix={prefix} suffix={suffix} />
)
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'text':
if (value != null && value != '') {
return (
@ -259,16 +234,6 @@ const ObjectProperty = ({
</Text>
)
}
case 'markdown':
if (value != null && value != '') {
return <MarkdownDisplay content={value} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'email':
if (value != null && value != '') {
return <EmailDisplay email={value} />
@ -300,9 +265,6 @@ const ObjectProperty = ({
)
}
}
case 'objectList': {
return <ObjectList value={value} objectType={objectType} />
}
case 'state': {
if (value && value?.type) {
switch (objectType) {
@ -384,17 +346,6 @@ const ObjectProperty = ({
)
}
}
case 'operation': {
if (value != null) {
return <OperationDisplay operation={value} />
} else {
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
}
case 'propertyChanges': {
return <PropertyChanges type={objectType} value={value} />
}
@ -604,7 +555,7 @@ const ObjectProperty = ({
<PrinterSelect placeholder={label} />
</Form.Item>
)
case 'gcodeFile':
case 'gcodefile':
return (
<Form.Item name={formItemName} {...mergedFormItemProps}>
<GCodeFileSelect placeholder={label} />
@ -652,22 +603,20 @@ const ObjectProperty = ({
ObjectProperty.propTypes = {
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
value: PropTypes.any,
isEditing: PropTypes.bool,
formItemProps: PropTypes.object,
required: PropTypes.bool,
name: PropTypes.string,
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
prefix: PropTypes.string,
suffix: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
showLabel: PropTypes.bool,
objectType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
objectType: PropTypes.string,
readOnly: PropTypes.bool,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
difference: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
objectData: PropTypes.object
disabled: PropTypes.bool
}
export default ObjectProperty

View File

@ -1,57 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Space, Tag } from 'antd'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import PlusIcon from '../../Icons/PlusIcon'
import BinIcon from '../../Icons/BinIcon'
import EditIcon from '../../Icons/EditIcon'
const OperationDisplay = ({
operation,
showIcon = true,
showText = true,
showColor = true
}) => {
var tagText = 'False'
var tagIcon = <QuestionCircleIcon />
var tagColor = 'default'
switch (operation) {
case 'new':
tagText = 'New'
tagIcon = <PlusIcon />
tagColor = 'success'
break
case 'delete':
tagText = 'Delete'
tagIcon = <BinIcon />
tagColor = 'error'
break
case 'edit':
tagText = 'Edit'
tagIcon = <EditIcon />
tagColor = 'blue'
break
default:
break
}
return (
<Space>
<Tag
style={{ margin: 0 }}
color={showColor ? tagColor : 'default'}
icon={showIcon ? tagIcon : undefined}
>
{showText ? tagText : null}
</Tag>
</Space>
)
}
OperationDisplay.propTypes = {
operation: PropTypes.bool.isRequired,
showIcon: PropTypes.bool,
showText: PropTypes.bool,
showColor: PropTypes.bool
}
export default OperationDisplay

View File

@ -8,7 +8,7 @@ import ArrowRightIcon from '../../Icons/ArrowRightIcon'
const { Text } = Typography
const PropertyChanges = ({ type, value }) => {
if (!value || (!value.new && !value.old)) {
if (!value || !value.new) {
return <Text type='secondary'>n/a</Text>
}
@ -18,15 +18,7 @@ const PropertyChanges = ({ type, value }) => {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = obj[key]
const newKey = prefix ? `${prefix}.${key}` : key
// Don't flatten keys that are "state" or "netGross"
if (
val &&
typeof val === 'object' &&
!Array.isArray(val) &&
key !== 'state' &&
key !== 'netGross'
) {
if (val && typeof val === 'object' && !Array.isArray(val)) {
flattenObject(val, newKey, res)
} else {
res[newKey] = val
@ -42,6 +34,7 @@ const PropertyChanges = ({ type, value }) => {
...flatOld,
...flatNew
}
console.log('combined', combinedChanges)
return (
<Descriptions size='small' column={1}>
{Object.keys(combinedChanges).map((key) => {
@ -53,25 +46,19 @@ const PropertyChanges = ({ type, value }) => {
return (
<Descriptions.Item key={key} label={changeProperty.label}>
<Space>
{value?.old ? (
<ObjectProperty
{...changeProperty}
longId={false}
objectData={value?.old}
/>
) : null}
{value?.old && value?.new ? (
<Text type='secondary'>
<ArrowRightIcon />
</Text>
) : null}
{value?.new ? (
<ObjectProperty
{...changeProperty}
longId={false}
objectData={value?.new}
/>
) : null}
<ObjectProperty
{...changeProperty}
longId={false}
objectData={value?.old}
/>
<Text type='secondary'>
<ArrowRightIcon />
</Text>
<ObjectProperty
{...changeProperty}
longId={false}
objectData={value?.new}
/>
</Space>
</Descriptions.Item>
)

View File

@ -1,32 +0,0 @@
import React from 'react'
import { Typography } from 'antd'
import PropTypes from 'prop-types'
const { Text } = Typography
const VarianceDisplay = ({ value, prefix, suffix }) => {
if (value === null || value === undefined) {
return <Text type='secondary'>n/a</Text>
}
const isPositive = value > 0
const isNegative = value < 0
const displayValue = Math.abs(value).toFixed(2)
return (
<Text type={isPositive ? 'success' : isNegative ? 'danger' : undefined}>
{prefix || ''}
{isPositive ? '+' : isNegative ? '-' : ''}
{displayValue}
{suffix || ''}
</Text>
)
}
VarianceDisplay.propTypes = {
value: PropTypes.number,
prefix: PropTypes.string,
suffix: PropTypes.string
}
export default VarianceDisplay

View File

@ -4,8 +4,7 @@ import React, {
useEffect,
useState,
useContext,
useRef,
useCallback
useRef
} from 'react'
import io from 'socket.io-client'
import { message, notification, Modal, Space, Button } from 'antd'
@ -23,9 +22,8 @@ logger.setLevel(config.logLevel)
const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => {
const { token, userProfile, authenticated } = useContext(AuthContext)
const { token, userProfile } = useContext(AuthContext)
const socketRef = useRef(null)
const [connected, setConnected] = useState(false)
const [connecting, setConnecting] = useState(false)
const [error, setError] = useState(null)
const [messageApi, contextHolder] = message.useMessage()
@ -34,43 +32,9 @@ const ApiServerProvider = ({ children }) => {
const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null)
const subscribedCallbacksRef = useRef(new Map())
const subscribedLockCallbacksRef = useRef(new Map())
const notifyLockUpdate = useCallback(
async (lockData) => {
logger.debug('Notifying lock update:', lockData)
const objectId = lockData._id || lockData.id
if (
objectId &&
subscribedLockCallbacksRef.current.has(objectId) &&
lockData.user != userProfile?._id
) {
const callbacks = subscribedLockCallbacksRef.current.get(objectId)
logger.debug(
`Calling ${callbacks.length} lock callbacks for object:`,
objectId
)
callbacks.forEach((callback) => {
try {
callback(lockData)
} catch (error) {
logger.error('Error in lock update callback:', error)
}
})
} else {
logger.debug(
`No lock callbacks found for object: ${objectId}, subscribed lock callbacks:`,
Array.from(subscribedLockCallbacksRef.current.keys())
)
}
},
[userProfile?._id]
)
const connectToServer = useCallback(() => {
if (token && authenticated == true) {
useEffect(() => {
if (token) {
logger.debug('Token is available, connecting to api server...')
const newSocket = io(config.apiServerUrl, {
@ -84,25 +48,18 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => {
logger.debug('Api Server connected')
setConnecting(false)
setConnected(true)
setError(null)
})
newSocket.on('notify_object_update', notifyObjectUpdate)
newSocket.on('notify_object_new', notifyObjectNew)
newSocket.on('notify_lock_update', notifyLockUpdate)
newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected')
setError('Api Server disconnected')
setConnected(false)
})
newSocket.on('connect_error', (err) => {
logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error')
setConnected(false)
})
newSocket.on('bridge.notification', (data) => {
@ -118,27 +75,21 @@ const ApiServerProvider = ({ children }) => {
})
socketRef.current = newSocket
}
}, [token, authenticated, messageApi, notificationApi, notifyLockUpdate])
useEffect(() => {
if (token && authenticated == true) {
connectToServer()
// Clean up function
return () => {
if (socketRef.current) {
logger.debug('Cleaning up api server connection...')
socketRef.current.disconnect()
socketRef.current = null
}
}
} else if (!token && socketRef.current) {
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
// Clean up function
return () => {
if (socketRef.current) {
logger.debug('Cleaning up api server connection...')
socketRef.current.disconnect()
socketRef.current = null
}
}
}, [token, authenticated, connectToServer])
}, [token, messageApi])
const lockObject = (id, type) => {
logger.debug('Locking ' + id)
@ -167,12 +118,8 @@ const ApiServerProvider = ({ children }) => {
type: type
},
(lockEvent) => {
logger.debug('Received lock status for object:', id, lockEvent)
if (lockEvent.user != userProfile?._id) {
resolve(lockEvent)
} else {
resolve(null)
}
logger.debug('Received lock event for object:', id, lockEvent)
resolve(lockEvent)
}
)
logger.debug('Sent fetch lock command for object:', id)
@ -180,186 +127,65 @@ const ApiServerProvider = ({ children }) => {
}
}
const notifyObjectUpdate = async (object) => {
logger.debug('Notifying object update:', object)
const objectId = object._id || object.id
if (objectId && subscribedCallbacksRef.current.has(objectId)) {
const callbacks = subscribedCallbacksRef.current.get(objectId)
logger.debug(
`Calling ${callbacks.length} callbacks for object:`,
objectId
)
callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object update callback:', error)
const onLockEvent = (id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const eventHandler = (data) => {
if (data._id === id && data?.user !== userProfile._id) {
logger.debug(
'Lock update received for object:',
id,
'locked:',
data.locked
)
callback(data)
}
})
} else {
logger.debug(
`No callbacks found for object: ${objectId}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
socketRef.current.on('notify_lock_update', eventHandler)
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, eventHandler)
}
}
const notifyObjectNew = async (object) => {
logger.debug('Notifying object new:', object)
const objectType = object.type || 'unknown'
if (objectType && subscribedCallbacksRef.current.has(objectType)) {
const callbacks = subscribedCallbacksRef.current.get(objectType)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
objectType
)
callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${objectType}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const offUpdateEvent = useCallback((id, type, callback) => {
const offLockEvent = (id, eventHandler) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(id)) {
const callbacks = subscribedCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(id)
socketRef.current.emit('unsubscribe', { id: id, type: type })
} else {
subscribedCallbacksRef.current.set(id, callbacks)
}
}
logger.debug('Removed update event listener for object:', id)
}
}, [])
const offTypeEvent = useCallback((type, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(type)) {
const callbacks = subscribedCallbacksRef.current
.get(type)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(type)
socketRef.current.emit('unsubscribe', { type: type })
} else {
subscribedCallbacksRef.current.set(type, callbacks)
}
}
logger.debug('Removed new event listener for type:', type)
}
}, [])
const subscribeToObject = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(id)) {
subscribedCallbacksRef.current.set(id, [])
}
subscribedCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added callback for object ${id}, total callbacks: ${subscribedCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe', { id: id, type: type })
logger.debug('Registered update event listener for object:', id)
// Return cleanup function
return () => offUpdateEvent(id, type, callback)
}
},
[offUpdateEvent]
)
const subscribeToType = useCallback(
(type, callback) => {
logger.debug('Subscribing to type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(type)) {
subscribedCallbacksRef.current.set(type, [])
}
subscribedCallbacksRef.current.get(type).push(callback)
logger.debug(
`Added callback for type ${type}, total callbacks: ${subscribedCallbacksRef.current.get(type).length}`
)
socketRef.current.emit('subscribe', { type: type })
logger.debug('Registered update event listener for object:', type)
// Return cleanup function
return () => offTypeEvent(type, callback)
}
},
[offTypeEvent]
)
const offLockEvent = useCallback((id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed lock callbacks map
if (subscribedLockCallbacksRef.current.has(id)) {
const callbacks = subscribedLockCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedLockCallbacksRef.current.delete(id)
} else {
subscribedLockCallbacksRef.current.set(id, callbacks)
}
}
socketRef.current.off('notify_lock_update', eventHandler)
logger.debug('Removed lock event listener for object:', id)
}
}, [])
}
const subscribeToLock = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to lock for object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed lock callbacks map immediately
if (!subscribedLockCallbacksRef.current.has(id)) {
subscribedLockCallbacksRef.current.set(id, [])
const onUpdateEvent = (id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const eventHandler = (data) => {
if (data._id === id && data?.user !== userProfile._id) {
logger.debug(
'Update event received for object:',
id,
'updatedAt:',
data.updatedAt
)
callback(data)
}
subscribedLockCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe_lock', { id: id, type: type })
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, callback)
}
},
[offLockEvent]
)
const showError = (error, callback = null) => {
var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})`
socketRef.current.on('notify_object_update', eventHandler)
logger.debug('Registered update event listener for object:', id)
// Return cleanup function
return () => offUpdateEvent(id, eventHandler)
}
}
const offUpdateEvent = (id, eventHandler) => {
if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.off('notify_update', eventHandler)
logger.debug('Removed update event listener for object:', id)
}
}
const showError = (content, callback = null) => {
setErrorModalContent(content)
setRetryCallback(() => callback)
setShowErrorModal(true)
@ -374,8 +200,8 @@ const ApiServerProvider = ({ children }) => {
setRetryCallback(null)
}
// Generalized fetchObject function
const fetchObject = async (id, type) => {
// Generalized fetchObjectInfo function
const fetchObjectInfo = async (id, type) => {
const fetchUrl = `${config.backendUrl}/${type}s/${id}`
setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl)
@ -388,17 +214,62 @@ const ApiServerProvider = ({ children }) => {
})
return response.data
} catch (err) {
showError(err, () => {
fetchObject(id, type)
})
return {}
logger.error('Failed to fetch object information:', err)
// Don't automatically show error - let the component handle it
throw err
} finally {
setFetchLoading(false)
}
}
// Update filament information
const updateObjectInfo = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Filament updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjects = async (type, params = {}) => {
const fetchTableData = async (type, params = {}) => {
const {
page = 1,
limit = 25,
@ -448,122 +319,9 @@ const ApiServerProvider = ({ children }) => {
hasMore,
page
}
} catch (err) {
showError(err, () => {
fetchObjects(type, params)
})
return []
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => {
const { filter = {}, properties = [] } = params
logger.debug('Fetching property object data from:', type, {
properties,
filter
})
try {
const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
{
params: {
...filter,
properties: properties.join(',') // Convert array to comma-separated string
},
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const newData = response.data
return newData
} catch (err) {
showError(err, () => {
fetchObjectsByProperty(type, params)
})
return []
}
}
// Update filament information
const updateObject = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
setError(err, () => {
updateObject(id, type, value)
})
return {}
}
}
// Update filament information
const deleteObject = async (id, type) => {
const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Deleting object ID: ' + id)
try {
const response = await axios.delete(deleteUrl, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object deleted successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
showError(err, () => {
deleteObject(id, type)
})
return {}
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
showError(err, () => {
createObject(type, value)
})
return {}
} catch (error) {
logger.error('Failed to fetch table data:', error)
throw error
}
}
@ -590,35 +348,27 @@ const ApiServerProvider = ({ children }) => {
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
} catch (err) {
showError(err, () => {
fetchObjectContent(id, type, fileName)
})
}
}
// Fetch notes for a specific parent
const fetchNotes = async (parentId) => {
logger.debug('Fetching notes for parent:', parentId)
try {
const response = await axios.get(`${config.backendUrl}/notes`, {
params: {
parent: parentId,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const notesData = response.data
logger.debug('Fetched notes:', notesData.length)
return notesData
} catch (error) {
logger.error('Failed to fetch notes:', error)
throw error
logger.error('Failed to download GCode file content:', error)
if (error.response) {
if (error.response.status === 404) {
showError(
`The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`,
() => fetchObjectContent(id, type, fileName)
)
} else {
showError(
`Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`,
() => fetchObjectContent(id, type, fileName)
)
}
} else {
showError(
'An unexpected error occurred while downloading. Please check your connection and try again.',
() => fetchObjectContent(id, type, fileName)
)
}
}
}
@ -628,24 +378,19 @@ const ApiServerProvider = ({ children }) => {
apiServer: socketRef.current,
error,
connecting,
connected,
lockObject,
unlockObject,
fetchObjectLock,
updateObject,
updateObjectInfo,
createObject,
deleteObject,
subscribeToObject,
subscribeToType,
subscribeToLock,
onLockEvent,
onUpdateEvent,
offUpdateEvent,
fetchObject,
fetchObjects,
fetchObjectsByProperty,
fetchObjectInfo,
fetchTableData,
fetchLoading,
showError,
fetchObjectContent,
fetchNotes
fetchObjectContent
}}
>
{contextHolder}

View File

@ -53,7 +53,8 @@ const AuthProvider = ({ children }) => {
})
if (response.status === 200 && response.data) {
logger.debug('Got auth token!')
logger.debug('User is authenticated!')
setAuthenticated(true)
setToken(response.data.access_token)
setExpiresAt(response.data.expires_at)
setUserProfile(response.data)
@ -88,6 +89,75 @@ const AuthProvider = ({ children }) => {
}
}, [])
const showTokenExpirationMessage = useCallback(
(expiresAt) => {
const now = new Date()
const expirationDate = new Date(expiresAt)
const timeRemaining = expirationDate - now
if (timeRemaining <= 0) {
if (authenticated) {
setShowSessionExpiredModal(true)
setAuthenticated(false)
notificationApi.destroy('token-expiration')
}
} else {
const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000)
// Only show notification in the final minute
if (minutes === 0) {
const totalSeconds = 60
const remainingSeconds = totalSeconds - seconds
const progress = (remainingSeconds / totalSeconds) * 100
notificationApi.info({
message: 'Session Expiring Soon',
description: (
<div>
<div style={{ marginBottom: 8 }}>
Your session will expire in {seconds} seconds
</div>
<Progress
percent={progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'token-expiration',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'token-expiration-notification',
closeIcon: null,
onClose: () => {},
btn: (
<Button
type='primary'
size='small'
onClick={() => {
notificationApi.destroy('token-expiration')
refreshToken()
}}
>
Reload Session
</Button>
)
})
} else if (minutes === 1) {
// Clear any existing notification when we enter the final minute
notificationApi.destroy('token-expiration')
}
}
},
[authenticated, notificationApi]
)
const handleSessionExpiredModalOk = () => {
setShowSessionExpiredModal(false)
loginWithSSO()
@ -97,89 +167,22 @@ const AuthProvider = ({ children }) => {
useEffect(() => {
let intervalId
const tokenRefresh = () => {
const tokenRefreshInterval = () => {
if (expiresAt) {
const now = new Date()
const expirationDate = new Date(expiresAt)
const timeRemaining = expirationDate - now
if (timeRemaining <= 0) {
if (authenticated == true) {
setAuthenticated(false)
}
setShowSessionExpiredModal(true)
notificationApi.destroy('token-expiration')
} else {
if (authenticated == false) {
setAuthenticated(true)
}
const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000)
// Only show notification in the final minute
if (minutes === 0) {
const totalSeconds = 60
const remainingSeconds = totalSeconds - seconds
const progress = (remainingSeconds / totalSeconds) * 100
notificationApi.info({
message: 'Session Expiring Soon',
description: (
<div>
<div style={{ marginBottom: 8 }}>
Your session will expire in {seconds} seconds
</div>
<Progress
percent={progress}
size='small'
status='active'
showInfo={false}
/>
</div>
),
duration: 0,
key: 'token-expiration',
icon: null,
placement: 'bottomRight',
style: {
width: 360
},
className: 'token-expiration-notification',
closeIcon: null,
onClose: () => {},
btn: (
<Button
type='primary'
size='small'
onClick={() => {
notificationApi.destroy('token-expiration')
refreshToken()
}}
>
Reload Session
</Button>
)
})
} else if (minutes === 1) {
// Clear any existing notification when we enter the final minute
notificationApi.destroy('token-expiration')
}
}
showTokenExpirationMessage(expiresAt)
}
}
intervalId = setInterval(tokenRefresh, 1000)
console.log('fresh', authenticated)
tokenRefresh()
if (authenticated) {
intervalId = setInterval(tokenRefreshInterval, 1000)
}
return () => {
if (intervalId) {
clearInterval(intervalId)
}
}
}, [expiresAt, authenticated, notificationApi, refreshToken])
}, [expiresAt, authenticated, showTokenExpirationMessage])
useEffect(() => {
checkAuthStatus()

View File

@ -461,9 +461,16 @@ const SpotlightProvider = ({ children }) => {
align='center'
justify='space-between'
>
<Flex gap={'small'} align='center'>
{Icon ? <Icon style={{ fontSize: '20px' }} /> : null}
<Flex
gap={'small'}
style={{ marginBottom: '2px' }}
align='center'
>
<Text>
{Icon ? (
<Icon style={{ fontSize: '20px' }} />
) : null}
</Text>
{item.name ? (
<Text ellipsis style={{ maxWidth: 170 }}>
{item.name}

View File

@ -6,16 +6,8 @@ export const AuditLog = {
prefix: 'ADL',
icon: AuditLogIcon,
actions: [],
columns: [
'_id',
'owner',
'owner._id',
'parent._id',
'operation',
'changes',
'createdAt'
],
filters: ['_id', 'owner._id', 'parent._id', 'operation'],
columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'],
filters: ['_id', 'owner._id', 'parent._id'],
sorters: ['createdAt'],
properties: [
{
@ -83,12 +75,6 @@ export const AuditLog = {
showHyperlink: true,
showCopy: true
},
{
name: 'operation',
label: 'Operation',
columnWidth: 120,
type: 'operation'
},
{
name: 'changes',
label: 'Changes',

View File

@ -1,6 +1,7 @@
import JobIcon from '../../components/Icons/JobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
export const Job = {
name: 'job',
@ -21,6 +22,13 @@ export const Job = {
label: 'Reload',
icon: ReloadIcon,
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=edit`
}
],
columns: [
@ -60,7 +68,7 @@ export const Job = {
type: 'object',
columnFixed: 'left',
objectType: 'gcodeFile',
required: true
readOnly: true
},
{
name: 'gcodeFile._id',
@ -90,10 +98,9 @@ export const Job = {
},
{
name: 'printers',
label: 'Printers',
type: 'objectList',
objectType: 'printer',
required: true
label: 'Assigned Printers',
type: 'number',
readOnly: true
}
]
}

View File

@ -16,58 +16,5 @@ export const Note = {
url: (_id) => `/dashboard/management/notes/info?noteId=${_id}`
}
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'note',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'noteType',
label: 'Note Type',
type: 'object',
objectType: 'noteType',
showHyperlink: true
},
{
name: 'parent._id',
label: 'Parent ID',
type: 'id',
objectType: (objectData) => {
return objectData.parentType
},
showHyperlink: true
},
{
name: 'noteType._id',
label: 'Note Type ID',
type: 'id',
objectType: 'noteType'
},
{
name: 'user._id',
label: 'User ID',
type: 'id',
objectType: 'user'
},
{
name: 'content',
label: 'Content',
type: 'markdown'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
}
]
url: () => `#`
}

View File

@ -60,7 +60,7 @@ export const StockEvent = {
label: 'Parent',
type: 'object',
objectType: (objectData) => {
return objectData?.parentType
return objectData.parentType
},
value: null,
showCopy: true
@ -78,12 +78,9 @@ export const StockEvent = {
{
name: 'value',
label: 'Value',
columnWidth: 120,
type: 'variance',
showCopy: true,
suffix: (objectData) => {
return objectData.unit
}
columnWidth: 100,
type: 'number',
showCopy: true
}
]
}