Compare commits
13 Commits
2a18f3d697
...
cd83679232
| Author | SHA1 | Date | |
|---|---|---|---|
| cd83679232 | |||
| 38f03f8fe9 | |||
| 9a1f58aafe | |||
| a0ab5be6f2 | |||
| 5aa7355b0f | |||
| a505e1aaba | |||
| a5458c6b67 | |||
| 3bd2628960 | |||
| d46402983f | |||
| b6c2cb22f4 | |||
| 0a897e663c | |||
| 3ad0002bbb | |||
| b71537dc64 |
68
src/components/Dashboard/common/DeleteObjectModal.jsx
Normal file
68
src/components/Dashboard/common/DeleteObjectModal.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
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
|
||||
@ -49,7 +49,7 @@ const IdDisplay = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex align={'center'} className='iddisplay'>
|
||||
<Flex align={'end'} className='iddisplay'>
|
||||
{(() => {
|
||||
const content = (
|
||||
<Flex gap={4}>
|
||||
|
||||
@ -12,33 +12,33 @@ const InfoCollapse = ({
|
||||
active,
|
||||
onToggle,
|
||||
className = '',
|
||||
key = 'default'
|
||||
collapseKey = 'default'
|
||||
}) => {
|
||||
return (
|
||||
<Collapse
|
||||
ghost
|
||||
expandIconPosition='end'
|
||||
activeKey={active ? [key] : []}
|
||||
activeKey={active ? [collapseKey] : []}
|
||||
onChange={(keys) => onToggle(keys.length > 0)}
|
||||
expandIcon={({ isActive }) => (
|
||||
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
|
||||
)}
|
||||
className={`no-h-padding-collapse ${className}`}
|
||||
>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
items={[
|
||||
{
|
||||
key: collapseKey,
|
||||
children: children,
|
||||
label: (
|
||||
<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,
|
||||
key: PropTypes.string
|
||||
collapseKey: PropTypes.string
|
||||
}
|
||||
|
||||
export default InfoCollapse
|
||||
|
||||
@ -26,6 +26,7 @@ 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'
|
||||
@ -259,7 +260,7 @@ NoteItem.propTypes = {
|
||||
onChildNoteAdded: PropTypes.func
|
||||
}
|
||||
|
||||
const NotesPanel = ({ _id, onNewNote }) => {
|
||||
const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
const [newNoteOpen, setNewNoteOpen] = useState(false)
|
||||
const [showMarkdown, setShowMarkdown] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -273,6 +274,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
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)
|
||||
@ -289,22 +291,12 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
}, [newNoteForm, newNoteFormUpdateValues])
|
||||
|
||||
const { authenticated, userProfile } = useContext(AuthContext)
|
||||
const { fetchNotes } = useContext(ApiServerContext)
|
||||
|
||||
const fetchData = useCallback(async (id) => {
|
||||
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 newData = response.data
|
||||
const newData = await fetchNotes(id)
|
||||
setLoading(false)
|
||||
return newData
|
||||
} catch (error) {
|
||||
@ -312,7 +304,20 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
},
|
||||
[fetchNotes]
|
||||
)
|
||||
|
||||
const handleNewChildNote = useCallback(
|
||||
(parentId) => {
|
||||
setSelectedParentId(parentId)
|
||||
setSelectedParentType('note')
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
},
|
||||
[newNoteForm]
|
||||
)
|
||||
|
||||
const generateNotes = useCallback(
|
||||
async (id) => {
|
||||
@ -345,7 +350,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
expandedNotes={expandedNotes}
|
||||
setExpandedNotes={setExpandedNotes}
|
||||
fetchData={fetchData}
|
||||
onNewNote={handleNewNoteFromDropdown}
|
||||
onNewNote={handleNewChildNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
userProfile={userProfile}
|
||||
onChildNoteAdded={(noteId, callback) => {
|
||||
@ -357,7 +362,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
/>
|
||||
))
|
||||
},
|
||||
[loading, fetchData, expandedNotes, userProfile]
|
||||
[loading, fetchData, expandedNotes, userProfile, handleNewChildNote]
|
||||
)
|
||||
|
||||
const handleNewNote = async () => {
|
||||
@ -365,7 +370,11 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
try {
|
||||
await axios.post(
|
||||
`${config.backendUrl}/notes`,
|
||||
{ ...newNoteFormValues, parent: selectedParentId || _id },
|
||||
{
|
||||
...newNoteFormValues,
|
||||
parent: selectedParentId,
|
||||
parentType: selectedParentType
|
||||
},
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
@ -402,13 +411,6 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewNoteFromDropdown = (parentId) => {
|
||||
setSelectedParentId(parentId)
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
}
|
||||
|
||||
const handleDeleteNote = async (noteId) => {
|
||||
setNoteToDelete(noteId)
|
||||
setDeleteConfirmOpen(true)
|
||||
@ -499,7 +501,8 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
setLoading(true)
|
||||
handleReloadData()
|
||||
} else if (key === 'newNote') {
|
||||
setSelectedParentId(null)
|
||||
setSelectedParentId(_id)
|
||||
setSelectedParentType(type)
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
@ -522,7 +525,8 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
icon={<PlusIcon />}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setSelectedParentId(null)
|
||||
setSelectedParentId(_id)
|
||||
setSelectedParentType(type)
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
@ -688,6 +692,7 @@ const NotesPanel = ({ _id, onNewNote }) => {
|
||||
|
||||
NotesPanel.propTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
onNewNote: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ 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
|
||||
}
|
||||
@ -53,11 +54,10 @@ const ObjectActions = ({
|
||||
location.search
|
||||
)
|
||||
|
||||
console.log('curr url', currentUrlWithoutActions)
|
||||
|
||||
// Filter out actions whose url matches currentUrl
|
||||
const filteredActions = actions.filter(
|
||||
(action) => !(action.url(id) && action.url(id) === currentUrlWithoutActions)
|
||||
(action) =>
|
||||
typeof action.url !== 'function' ||
|
||||
action.url(id) !== currentUrlWithoutActions
|
||||
)
|
||||
|
||||
const currentUrlWithActions = location.pathname + location.search
|
||||
|
||||
@ -13,6 +13,7 @@ const ObjectInfo = ({
|
||||
properties = [],
|
||||
required = undefined,
|
||||
visibleProperties = {},
|
||||
objectPropertyProps = {},
|
||||
...rest
|
||||
}) => {
|
||||
const allItems = getModelProperties(type)
|
||||
@ -55,6 +56,7 @@ const ObjectInfo = ({
|
||||
children: (
|
||||
<ObjectProperty
|
||||
{...item}
|
||||
{...objectPropertyProps}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
/>
|
||||
@ -91,7 +93,8 @@ ObjectInfo.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
objectData: PropTypes.object,
|
||||
required: PropTypes.bool,
|
||||
visibleProperties: PropTypes.object
|
||||
visibleProperties: PropTypes.object,
|
||||
objectPropertyProps: PropTypes.object
|
||||
}
|
||||
|
||||
export default ObjectInfo
|
||||
|
||||
49
src/components/Dashboard/common/ObjectList.jsx
Normal file
49
src/components/Dashboard/common/ObjectList.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
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
|
||||
@ -37,6 +37,10 @@ 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
|
||||
|
||||
@ -71,7 +75,7 @@ const ObjectProperty = ({
|
||||
initial = false,
|
||||
...rest
|
||||
}) => {
|
||||
if (typeof value == 'function' && objectData) {
|
||||
if (value && typeof value == 'function' && objectData) {
|
||||
value = value(objectData)
|
||||
}
|
||||
|
||||
@ -87,6 +91,14 @@ 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)
|
||||
}
|
||||
@ -218,6 +230,19 @@ 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 (
|
||||
@ -234,6 +259,16 @@ 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} />
|
||||
@ -265,6 +300,9 @@ const ObjectProperty = ({
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'objectList': {
|
||||
return <ObjectList value={value} objectType={objectType} />
|
||||
}
|
||||
case 'state': {
|
||||
if (value && value?.type) {
|
||||
switch (objectType) {
|
||||
@ -346,6 +384,17 @@ 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} />
|
||||
}
|
||||
@ -555,7 +604,7 @@ const ObjectProperty = ({
|
||||
<PrinterSelect placeholder={label} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'gcodefile':
|
||||
case 'gcodeFile':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<GCodeFileSelect placeholder={label} />
|
||||
@ -603,20 +652,22 @@ const ObjectProperty = ({
|
||||
|
||||
ObjectProperty.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
|
||||
isEditing: PropTypes.bool,
|
||||
formItemProps: PropTypes.object,
|
||||
required: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
prefix: PropTypes.string,
|
||||
suffix: PropTypes.string,
|
||||
prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
step: PropTypes.number,
|
||||
showLabel: PropTypes.bool,
|
||||
objectType: PropTypes.string,
|
||||
objectType: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
readOnly: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
||||
difference: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
|
||||
objectData: PropTypes.object
|
||||
}
|
||||
|
||||
export default ObjectProperty
|
||||
|
||||
57
src/components/Dashboard/common/OperationDisplay.jsx
Normal file
57
src/components/Dashboard/common/OperationDisplay.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
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
|
||||
@ -8,7 +8,7 @@ import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||
const { Text } = Typography
|
||||
|
||||
const PropertyChanges = ({ type, value }) => {
|
||||
if (!value || !value.new) {
|
||||
if (!value || (!value.new && !value.old)) {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
|
||||
@ -18,7 +18,15 @@ const PropertyChanges = ({ type, value }) => {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const val = obj[key]
|
||||
const newKey = prefix ? `${prefix}.${key}` : key
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
|
||||
// Don't flatten keys that are "state" or "netGross"
|
||||
if (
|
||||
val &&
|
||||
typeof val === 'object' &&
|
||||
!Array.isArray(val) &&
|
||||
key !== 'state' &&
|
||||
key !== 'netGross'
|
||||
) {
|
||||
flattenObject(val, newKey, res)
|
||||
} else {
|
||||
res[newKey] = val
|
||||
@ -34,7 +42,6 @@ const PropertyChanges = ({ type, value }) => {
|
||||
...flatOld,
|
||||
...flatNew
|
||||
}
|
||||
console.log('combined', combinedChanges)
|
||||
return (
|
||||
<Descriptions size='small' column={1}>
|
||||
{Object.keys(combinedChanges).map((key) => {
|
||||
@ -46,19 +53,25 @@ 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}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
)
|
||||
|
||||
32
src/components/Dashboard/common/VarianceDisplay.jsx
Normal file
32
src/components/Dashboard/common/VarianceDisplay.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
@ -4,7 +4,8 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useContext,
|
||||
useRef
|
||||
useRef,
|
||||
useCallback
|
||||
} from 'react'
|
||||
import io from 'socket.io-client'
|
||||
import { message, notification, Modal, Space, Button } from 'antd'
|
||||
@ -22,8 +23,9 @@ logger.setLevel(config.logLevel)
|
||||
const ApiServerContext = createContext()
|
||||
|
||||
const ApiServerProvider = ({ children }) => {
|
||||
const { token, userProfile } = useContext(AuthContext)
|
||||
const { token, userProfile, authenticated } = 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()
|
||||
@ -32,9 +34,43 @@ 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())
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
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) {
|
||||
logger.debug('Token is available, connecting to api server...')
|
||||
|
||||
const newSocket = io(config.apiServerUrl, {
|
||||
@ -48,18 +84,25 @@ 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) => {
|
||||
@ -75,6 +118,17 @@ const ApiServerProvider = ({ children }) => {
|
||||
})
|
||||
|
||||
socketRef.current = newSocket
|
||||
}
|
||||
}, [token, authenticated, messageApi, notificationApi, notifyLockUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (token && authenticated == true) {
|
||||
connectToServer()
|
||||
} else if (!token && socketRef.current) {
|
||||
logger.debug('Token not available, disconnecting api server...')
|
||||
socketRef.current.disconnect()
|
||||
socketRef.current = null
|
||||
}
|
||||
|
||||
// Clean up function
|
||||
return () => {
|
||||
@ -84,12 +138,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
socketRef.current = null
|
||||
}
|
||||
}
|
||||
} else if (!token && socketRef.current) {
|
||||
logger.debug('Token not available, disconnecting api server...')
|
||||
socketRef.current.disconnect()
|
||||
socketRef.current = null
|
||||
}
|
||||
}, [token, messageApi])
|
||||
}, [token, authenticated, connectToServer])
|
||||
|
||||
const lockObject = (id, type) => {
|
||||
logger.debug('Locking ' + id)
|
||||
@ -118,8 +167,12 @@ const ApiServerProvider = ({ children }) => {
|
||||
type: type
|
||||
},
|
||||
(lockEvent) => {
|
||||
logger.debug('Received lock event for object:', id, lockEvent)
|
||||
logger.debug('Received lock status for object:', id, lockEvent)
|
||||
if (lockEvent.user != userProfile?._id) {
|
||||
resolve(lockEvent)
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
)
|
||||
logger.debug('Sent fetch lock command for object:', id)
|
||||
@ -127,65 +180,186 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onLockEvent = (id, callback) => {
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
const eventHandler = (data) => {
|
||||
if (data._id === id && data?.user !== userProfile._id) {
|
||||
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(
|
||||
'Lock update received for object:',
|
||||
id,
|
||||
'locked:',
|
||||
data.locked
|
||||
`Calling ${callbacks.length} callbacks for object:`,
|
||||
objectId
|
||||
)
|
||||
callback(data)
|
||||
callbacks.forEach((callback) => {
|
||||
try {
|
||||
callback(object)
|
||||
} catch (error) {
|
||||
logger.error('Error in object update callback:', error)
|
||||
}
|
||||
}
|
||||
|
||||
socketRef.current.on('notify_lock_update', eventHandler)
|
||||
logger.debug('Registered lock event listener for object:', id)
|
||||
|
||||
// Return cleanup function
|
||||
return () => offLockEvent(id, eventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
const offLockEvent = (id, eventHandler) => {
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
socketRef.current.off('notify_lock_update', eventHandler)
|
||||
logger.debug('Removed lock event listener for object:', id)
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateEvent = (id, callback) => {
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
const eventHandler = (data) => {
|
||||
if (data._id === id && data?.user !== userProfile._id) {
|
||||
})
|
||||
} else {
|
||||
logger.debug(
|
||||
'Update event received for object:',
|
||||
id,
|
||||
'updatedAt:',
|
||||
data.updatedAt
|
||||
`No callbacks found for object: ${objectId}, subscribed callbacks:`,
|
||||
Array.from(subscribedCallbacksRef.current.keys())
|
||||
)
|
||||
callback(data)
|
||||
}
|
||||
}
|
||||
|
||||
socketRef.current.on('notify_object_update', 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) => {
|
||||
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, eventHandler)
|
||||
}
|
||||
return () => offUpdateEvent(id, type, callback)
|
||||
}
|
||||
},
|
||||
[offUpdateEvent]
|
||||
)
|
||||
|
||||
const offUpdateEvent = (id, eventHandler) => {
|
||||
const subscribeToType = useCallback(
|
||||
(type, callback) => {
|
||||
logger.debug('Subscribing to type:', type)
|
||||
if (socketRef.current && socketRef.current.connected == true) {
|
||||
socketRef.current.off('notify_update', eventHandler)
|
||||
logger.debug('Removed update event listener for object:', id)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
const showError = (content, callback = null) => {
|
||||
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, [])
|
||||
}
|
||||
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})`
|
||||
}
|
||||
setErrorModalContent(content)
|
||||
setRetryCallback(() => callback)
|
||||
setShowErrorModal(true)
|
||||
@ -200,8 +374,8 @@ const ApiServerProvider = ({ children }) => {
|
||||
setRetryCallback(null)
|
||||
}
|
||||
|
||||
// Generalized fetchObjectInfo function
|
||||
const fetchObjectInfo = async (id, type) => {
|
||||
// Generalized fetchObject function
|
||||
const fetchObject = async (id, type) => {
|
||||
const fetchUrl = `${config.backendUrl}/${type}s/${id}`
|
||||
setFetchLoading(true)
|
||||
logger.debug('Fetching from ' + fetchUrl)
|
||||
@ -214,62 +388,17 @@ const ApiServerProvider = ({ children }) => {
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
logger.error('Failed to fetch object information:', err)
|
||||
// Don't automatically show error - let the component handle it
|
||||
throw err
|
||||
showError(err, () => {
|
||||
fetchObject(id, type)
|
||||
})
|
||||
return {}
|
||||
} 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 fetchTableData = async (type, params = {}) => {
|
||||
const fetchObjects = async (type, params = {}) => {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 25,
|
||||
@ -319,9 +448,122 @@ const ApiServerProvider = ({ children }) => {
|
||||
hasMore,
|
||||
page
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch table data:', error)
|
||||
throw error
|
||||
} 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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -348,27 +590,35 @@ const ApiServerProvider = ({ children }) => {
|
||||
document.body.appendChild(fileLink)
|
||||
fileLink.click()
|
||||
fileLink.parentNode.removeChild(fileLink)
|
||||
} catch (error) {
|
||||
logger.error('Failed to download GCode file content:', error)
|
||||
} catch (err) {
|
||||
showError(err, () => {
|
||||
fetchObjectContent(id, type, fileName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,19 +628,24 @@ const ApiServerProvider = ({ children }) => {
|
||||
apiServer: socketRef.current,
|
||||
error,
|
||||
connecting,
|
||||
connected,
|
||||
lockObject,
|
||||
unlockObject,
|
||||
fetchObjectLock,
|
||||
updateObjectInfo,
|
||||
updateObject,
|
||||
createObject,
|
||||
onLockEvent,
|
||||
onUpdateEvent,
|
||||
deleteObject,
|
||||
subscribeToObject,
|
||||
subscribeToType,
|
||||
subscribeToLock,
|
||||
offUpdateEvent,
|
||||
fetchObjectInfo,
|
||||
fetchTableData,
|
||||
fetchObject,
|
||||
fetchObjects,
|
||||
fetchObjectsByProperty,
|
||||
fetchLoading,
|
||||
showError,
|
||||
fetchObjectContent
|
||||
fetchObjectContent,
|
||||
fetchNotes
|
||||
}}
|
||||
>
|
||||
{contextHolder}
|
||||
|
||||
@ -53,8 +53,7 @@ const AuthProvider = ({ children }) => {
|
||||
})
|
||||
|
||||
if (response.status === 200 && response.data) {
|
||||
logger.debug('User is authenticated!')
|
||||
setAuthenticated(true)
|
||||
logger.debug('Got auth token!')
|
||||
setToken(response.data.access_token)
|
||||
setExpiresAt(response.data.expires_at)
|
||||
setUserProfile(response.data)
|
||||
@ -89,19 +88,32 @@ const AuthProvider = ({ children }) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const showTokenExpirationMessage = useCallback(
|
||||
(expiresAt) => {
|
||||
const handleSessionExpiredModalOk = () => {
|
||||
setShowSessionExpiredModal(false)
|
||||
loginWithSSO()
|
||||
}
|
||||
|
||||
// Initialize on component mount
|
||||
useEffect(() => {
|
||||
let intervalId
|
||||
|
||||
const tokenRefresh = () => {
|
||||
if (expiresAt) {
|
||||
const now = new Date()
|
||||
const expirationDate = new Date(expiresAt)
|
||||
const timeRemaining = expirationDate - now
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
if (authenticated) {
|
||||
setShowSessionExpiredModal(true)
|
||||
if (authenticated == true) {
|
||||
setAuthenticated(false)
|
||||
notificationApi.destroy('token-expiration')
|
||||
}
|
||||
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)
|
||||
|
||||
@ -154,35 +166,20 @@ const AuthProvider = ({ children }) => {
|
||||
notificationApi.destroy('token-expiration')
|
||||
}
|
||||
}
|
||||
},
|
||||
[authenticated, notificationApi]
|
||||
)
|
||||
|
||||
const handleSessionExpiredModalOk = () => {
|
||||
setShowSessionExpiredModal(false)
|
||||
loginWithSSO()
|
||||
}
|
||||
|
||||
// Initialize on component mount
|
||||
useEffect(() => {
|
||||
let intervalId
|
||||
|
||||
const tokenRefreshInterval = () => {
|
||||
if (expiresAt) {
|
||||
showTokenExpirationMessage(expiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
if (authenticated) {
|
||||
intervalId = setInterval(tokenRefreshInterval, 1000)
|
||||
}
|
||||
intervalId = setInterval(tokenRefresh, 1000)
|
||||
|
||||
console.log('fresh', authenticated)
|
||||
tokenRefresh()
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
}, [expiresAt, authenticated, showTokenExpirationMessage])
|
||||
}, [expiresAt, authenticated, notificationApi, refreshToken])
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus()
|
||||
|
||||
@ -461,16 +461,9 @@ const SpotlightProvider = ({ children }) => {
|
||||
align='center'
|
||||
justify='space-between'
|
||||
>
|
||||
<Flex
|
||||
gap={'small'}
|
||||
style={{ marginBottom: '2px' }}
|
||||
align='center'
|
||||
>
|
||||
<Text>
|
||||
{Icon ? (
|
||||
<Icon style={{ fontSize: '20px' }} />
|
||||
) : null}
|
||||
</Text>
|
||||
<Flex gap={'small'} align='center'>
|
||||
{Icon ? <Icon style={{ fontSize: '20px' }} /> : null}
|
||||
|
||||
{item.name ? (
|
||||
<Text ellipsis style={{ maxWidth: 170 }}>
|
||||
{item.name}
|
||||
|
||||
@ -6,8 +6,16 @@ export const AuditLog = {
|
||||
prefix: 'ADL',
|
||||
icon: AuditLogIcon,
|
||||
actions: [],
|
||||
columns: ['_id', 'owner', 'owner._id', 'parent._id', 'changes', 'createdAt'],
|
||||
filters: ['_id', 'owner._id', 'parent._id'],
|
||||
columns: [
|
||||
'_id',
|
||||
'owner',
|
||||
'owner._id',
|
||||
'parent._id',
|
||||
'operation',
|
||||
'changes',
|
||||
'createdAt'
|
||||
],
|
||||
filters: ['_id', 'owner._id', 'parent._id', 'operation'],
|
||||
sorters: ['createdAt'],
|
||||
properties: [
|
||||
{
|
||||
@ -75,6 +83,12 @@ export const AuditLog = {
|
||||
showHyperlink: true,
|
||||
showCopy: true
|
||||
},
|
||||
{
|
||||
name: 'operation',
|
||||
label: 'Operation',
|
||||
columnWidth: 120,
|
||||
type: 'operation'
|
||||
},
|
||||
{
|
||||
name: 'changes',
|
||||
label: 'Changes',
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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',
|
||||
@ -22,13 +21,6 @@ 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: [
|
||||
@ -68,7 +60,7 @@ export const Job = {
|
||||
type: 'object',
|
||||
columnFixed: 'left',
|
||||
objectType: 'gcodeFile',
|
||||
readOnly: true
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'gcodeFile._id',
|
||||
@ -98,9 +90,10 @@ export const Job = {
|
||||
},
|
||||
{
|
||||
name: 'printers',
|
||||
label: 'Assigned Printers',
|
||||
type: 'number',
|
||||
readOnly: true
|
||||
label: 'Printers',
|
||||
type: 'objectList',
|
||||
objectType: 'printer',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -16,5 +16,58 @@ export const Note = {
|
||||
url: (_id) => `/dashboard/management/notes/info?noteId=${_id}`
|
||||
}
|
||||
],
|
||||
url: () => `#`
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ export const StockEvent = {
|
||||
label: 'Parent',
|
||||
type: 'object',
|
||||
objectType: (objectData) => {
|
||||
return objectData.parentType
|
||||
return objectData?.parentType
|
||||
},
|
||||
value: null,
|
||||
showCopy: true
|
||||
@ -78,9 +78,12 @@ export const StockEvent = {
|
||||
{
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
columnWidth: 100,
|
||||
type: 'number',
|
||||
showCopy: true
|
||||
columnWidth: 120,
|
||||
type: 'variance',
|
||||
showCopy: true,
|
||||
suffix: (objectData) => {
|
||||
return objectData.unit
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user