diff --git a/src/components/App/AuthCallback.jsx b/src/components/App/AuthCallback.jsx
new file mode 100644
index 0000000..ffc5ff1
--- /dev/null
+++ b/src/components/App/AuthCallback.jsx
@@ -0,0 +1,75 @@
+import React, { useState, useEffect, useContext, useCallback } from 'react'
+import { Flex, Card, Alert } from 'antd'
+import { LoadingOutlined } from '@ant-design/icons'
+import AuthParticles from './AppParticles'
+import FarmControlLogo from '../Logos/FarmControlLogo'
+import { AuthContext } from '../Dashboard/context/AuthContext'
+import { useLocation, useNavigate } from 'react-router-dom'
+
+const AuthCallback = () => {
+ const [isVisible, setIsVisible] = useState(false)
+ const [loading, setLoading] = useState(false)
+ const [initialized, setInitialized] = useState(false)
+ const { getLoginToken } = useContext(AuthContext)
+ const location = useLocation()
+ const navigate = useNavigate()
+ const code = new URLSearchParams(location.search).get('code')
+ const state = new URLSearchParams(location.search).get('state')
+
+ const handleGetloginToken = useCallback(async () => {
+ await getLoginToken(code)
+ setLoading(false)
+ navigate(state)
+ }, [code, state, navigate, getLoginToken])
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsVisible(true)
+ }, 1000)
+ if (!initialized && !loading) {
+ handleGetloginToken()
+ setInitialized(true)
+ setLoading(true)
+ }
+ return () => clearTimeout(timer)
+ }, [handleGetloginToken, initialized, loading])
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ showIcon
+ />
+
+
+
+ )
+}
+
+export default AuthCallback
diff --git a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx
index 8dc0c38..b175b6c 100644
--- a/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx
+++ b/src/components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx
@@ -11,7 +11,7 @@ import EditButtons from '../../common/EditButtons'
import ActionHandler from '../../common/ActionHandler'
import InfoCollapse from '../../common/InfoCollapse'
import NotesPanel from '../../common/NotesPanel'
-import LockIndicator from '../../Management/Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
import NoteIcon from '../../../Icons/NoteIcon'
diff --git a/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx b/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx
index b18685a..19d5ee1 100644
--- a/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx
+++ b/src/components/Dashboard/Inventory/FilamentStocks/LoadFilamentStock.jsx
@@ -90,7 +90,6 @@ const LoadFilamentStock = ({
)
)
}
- logger.debug(statusUpdate)
}
printServer.emit('printer.objects.subscribe', params)
diff --git a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx
index cf2fc4c..b7604b1 100644
--- a/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx
+++ b/src/components/Dashboard/Management/Filaments/FilamentInfo.jsx
@@ -14,7 +14,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from './LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
diff --git a/src/components/Dashboard/Management/Hosts.jsx b/src/components/Dashboard/Management/Hosts.jsx
new file mode 100644
index 0000000..67108a0
--- /dev/null
+++ b/src/components/Dashboard/Management/Hosts.jsx
@@ -0,0 +1,105 @@
+// src/hosts.js
+
+import React, { useRef, useState } from 'react'
+import { Button, Flex, Space, Modal, message, Dropdown } from 'antd'
+
+import NewHost from './Hosts/NewHost'
+
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import ColumnViewButton from '../common/ColumnViewButton'
+import ObjectTable from '../common/ObjectTable'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import ListIcon from '../../Icons/ListIcon'
+import GridIcon from '../../Icons/GridIcon'
+import useViewMode from '../hooks/useViewMode'
+
+const Hosts = () => {
+ const [messageApi, contextHolder] = message.useMessage()
+ const [newHostOpen, setNewHostOpen] = useState(false)
+ const tableRef = useRef()
+
+ // View mode state (cards/list), persisted in sessionStorage via custom hook
+ const [viewMode, setViewMode] = useViewMode('host')
+
+ const [columnVisibility, setColumnVisibility] = useColumnVisibility('host')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Host',
+ key: 'newHost',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newHost') {
+ setNewHostOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+ {contextHolder}
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+
+ {
+ setNewHostOpen(false)
+ }}
+ >
+ {
+ setNewHostOpen(false)
+ messageApi.success('New host added successfully.')
+ tableRef.current?.reload()
+ }}
+ reset={newHostOpen}
+ />
+
+
+ >
+ )
+}
+
+export default Hosts
diff --git a/src/components/Dashboard/Management/Hosts/HostInfo.jsx b/src/components/Dashboard/Management/Hosts/HostInfo.jsx
new file mode 100644
index 0000000..3ae2a0f
--- /dev/null
+++ b/src/components/Dashboard/Management/Hosts/HostInfo.jsx
@@ -0,0 +1,186 @@
+import React from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Card } from 'antd'
+import { LoadingOutlined } from '@ant-design/icons'
+import loglevel from 'loglevel'
+import config from '../../../../config.js'
+import useCollapseState from '../../hooks/useCollapseState.js'
+import NotesPanel from '../../common/NotesPanel.jsx'
+import InfoCollapse from '../../common/InfoCollapse.jsx'
+import ObjectInfo from '../../common/ObjectInfo.jsx'
+import ViewButton from '../../common/ViewButton.jsx'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import EditObjectForm from '../../common/EditObjectForm.jsx'
+import EditButtons from '../../common/EditButtons.jsx'
+import LockIndicator from '../../common/LockIndicator.jsx'
+import ActionHandler from '../../common/ActionHandler.jsx'
+import ObjectActions from '../../common/ObjectActions.jsx'
+import ObjectTable from '../../common/ObjectTable.jsx'
+import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
+
+const log = loglevel.getLogger('HostInfo')
+log.setLevel(config.logLevel)
+
+const HostInfo = () => {
+ const location = useLocation()
+ const hostId = new URLSearchParams(location.search).get('hostId')
+ const [collapseState, updateCollapseState] = useCollapseState('HostInfo', {
+ info: true,
+ stocks: true,
+ notes: true,
+ auditLogs: true
+ })
+
+ return (
+
+ {({
+ loading,
+ isEditing,
+ startEditing,
+ cancelEditing,
+ handleUpdate,
+ formValid,
+ objectData,
+ editLoading,
+ lock,
+ fetchObject
+ }) => {
+ // Define actions for ActionHandler
+ const actions = {
+ reload: () => {
+ fetchObject()
+ return true
+ },
+ edit: () => {
+ startEditing()
+ return false
+ },
+ cancelEdit: () => {
+ cancelEditing()
+ return true
+ },
+ finishEdit: () => {
+ handleUpdate()
+ return true
+ }
+ }
+
+ return (
+
+ {({ callAction }) => (
+
+
+
+
+
+
+
+
+
+
+ {
+ callAction('finishEdit')
+ }}
+ cancelEditing={() => {
+ callAction('cancelEdit')
+ }}
+ startEditing={() => {
+ callAction('edit')
+ }}
+ editLoading={editLoading}
+ formValid={formValid}
+ disabled={lock?.locked || loading}
+ loading={editLoading}
+ />
+
+
+
+
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) =>
+ updateCollapseState('info', expanded)
+ }
+ collapseKey='info'
+ >
+ }
+ isEditing={isEditing}
+ type='host'
+ objectData={objectData}
+ />
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) =>
+ updateCollapseState('notes', expanded)
+ }
+ collapseKey='notes'
+ >
+
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ )}
+
+ )
+ }}
+
+ )
+}
+
+export default HostInfo
diff --git a/src/components/Dashboard/Management/Hosts/NewHost.jsx b/src/components/Dashboard/Management/Hosts/NewHost.jsx
new file mode 100644
index 0000000..851c8ff
--- /dev/null
+++ b/src/components/Dashboard/Management/Hosts/NewHost.jsx
@@ -0,0 +1,117 @@
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+import { useMediaQuery } from 'react-responsive'
+import { Typography, Flex, Steps, Divider } from 'antd'
+
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import NewObjectButtons from '../../common/NewObjectButtons'
+
+const { Title } = Typography
+
+const NewHost = ({ onOk }) => {
+ const [currentStep, setCurrentStep] = useState(0)
+
+ const isMobile = useMediaQuery({ maxWidth: 768 })
+
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Optional',
+ key: 'optional',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+
+ {!isMobile && (
+
+
+
+ )}
+
+ {!isMobile && (
+
+ )}
+
+
+
+ New Host
+
+
+ {steps[currentStep].content}
+
+ setCurrentStep((prev) => prev - 1)}
+ onNext={() => setCurrentStep((prev) => prev + 1)}
+ onSubmit={() => {
+ handleSubmit()
+ onOk()
+ }}
+ formValid={formValid}
+ submitLoading={submitLoading}
+ />
+
+
+ )
+ }}
+
+ )
+}
+
+NewHost.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool
+}
+
+export default NewHost
diff --git a/src/components/Dashboard/Management/ManagementSidebar.jsx b/src/components/Dashboard/Management/ManagementSidebar.jsx
index e25a2ef..5e65bf1 100644
--- a/src/components/Dashboard/Management/ManagementSidebar.jsx
+++ b/src/components/Dashboard/Management/ManagementSidebar.jsx
@@ -11,6 +11,7 @@ import SettingsIcon from '../../Icons/SettingsIcon'
import AuditLogIcon from '../../Icons/AuditLogIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import PersonIcon from '../../Icons/PersonIcon'
+import HostIcon from '../../Icons/HostIcon'
const items = [
{
@@ -50,6 +51,12 @@ const items = [
label: 'Note Types',
path: '/dashboard/management/notetypes'
},
+ {
+ key: 'hosts',
+ icon: ,
+ label: 'Hosts',
+ path: '/dashboard/management/hosts'
+ },
{
key: 'users',
icon: ,
@@ -91,7 +98,8 @@ const routeKeyMap = {
'/dashboard/management/materials': 'materials',
'/dashboard/management/notetypes': 'notetypes',
'/dashboard/management/settings': 'settings',
- '/dashboard/management/auditlogs': 'auditlogs'
+ '/dashboard/management/auditlogs': 'auditlogs',
+ '/dashboard/management/hosts': 'hosts'
}
const ManagementSidebar = (props) => {
diff --git a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx
index 97865af..0c906d3 100644
--- a/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx
+++ b/src/components/Dashboard/Management/NoteTypes/NoteTypeInfo.jsx
@@ -10,7 +10,7 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
diff --git a/src/components/Dashboard/Management/Parts/PartInfo.jsx b/src/components/Dashboard/Management/Parts/PartInfo.jsx
index 327ddc7..74f0673 100644
--- a/src/components/Dashboard/Management/Parts/PartInfo.jsx
+++ b/src/components/Dashboard/Management/Parts/PartInfo.jsx
@@ -8,7 +8,7 @@ import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx
index ea442c3..8c17b1c 100644
--- a/src/components/Dashboard/Management/Products/ProductInfo.jsx
+++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx
@@ -8,7 +8,7 @@ import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx
index 7bc1583..af71da8 100644
--- a/src/components/Dashboard/Management/Users/UserInfo.jsx
+++ b/src/components/Dashboard/Management/Users/UserInfo.jsx
@@ -12,7 +12,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
diff --git a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx
index d04784d..3137bbf 100644
--- a/src/components/Dashboard/Management/Vendors/VendorInfo.jsx
+++ b/src/components/Dashboard/Management/Vendors/VendorInfo.jsx
@@ -13,7 +13,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
diff --git a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
index 78ff900..6bbc75a 100644
--- a/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
+++ b/src/components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx
@@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../../Management/Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
diff --git a/src/components/Dashboard/Production/Jobs/JobInfo.jsx b/src/components/Dashboard/Production/Jobs/JobInfo.jsx
index c923ea8..2388214 100644
--- a/src/components/Dashboard/Production/Jobs/JobInfo.jsx
+++ b/src/components/Dashboard/Production/Jobs/JobInfo.jsx
@@ -9,8 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../../Management/Filaments/LockIndicator'
-import SubJobsTree from '../../common/SubJobsTree'
+import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import JobIcon from '../../../Icons/JobIcon'
@@ -128,7 +127,11 @@ const JobInfo = () => {
}
collapseKey='subJobs'
>
-
+
{
{({ handleSubmit, submitLoading, objectData, formValid }) => {
@@ -36,20 +36,6 @@ const NewJob = ({ onOk }) => {
/>
)
},
- {
- title: 'Optional',
- key: 'optional',
- content: (
-
- )
- },
{
title: 'Summary',
key: 'summary',
@@ -61,7 +47,8 @@ const NewJob = ({ onOk }) => {
visibleProperties={{
_id: false,
createdAt: false,
- updatedAt: false
+ updatedAt: false,
+ startedAt: false
}}
isEditing={false}
objectData={objectData}
diff --git a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx
index 3c8b48a..8d2ec45 100644
--- a/src/components/Dashboard/Production/Printers/PrinterInfo.jsx
+++ b/src/components/Dashboard/Production/Printers/PrinterInfo.jsx
@@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import EditObjectForm from '../../common/EditObjectForm'
import EditButtons from '../../common/EditButtons'
-import LockIndicator from '../../Management/Filaments/LockIndicator'
+import LockIndicator from '../../common/LockIndicator.jsx'
import PrinterJobsTree from '../../common/PrinterJobsTree'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
diff --git a/src/components/Dashboard/common/CountryDisplay.jsx b/src/components/Dashboard/common/CountryDisplay.jsx
index 59ed97c..864233c 100644
--- a/src/components/Dashboard/common/CountryDisplay.jsx
+++ b/src/components/Dashboard/common/CountryDisplay.jsx
@@ -23,7 +23,9 @@ const CountryDisplay = ({ countryCode }) => {
hasBorderRadius={true}
gradient='real-circular'
/>
- {country.name}
+
+ {country.name}
+
)
}
diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
index 38a3869..8d77bdf 100644
--- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx
+++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
@@ -6,70 +6,55 @@ import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
const breadcrumbNameMap = {
- '/dashboard/production': 'Production',
- '/dashboard/inventory': 'Inventory',
- '/dashboard/management': 'Management',
- '/dashboard/developer': 'Developer',
- '/dashboard/production/overview': 'Overview',
- '/dashboard/production/printers': 'Printers',
- '/dashboard/production/printers/control': 'Control',
- '/dashboard/production/printers/info': 'Info',
- '/dashboard/production/jobs': 'Jobs',
- '/dashboard/production/subjobs': 'Sub Jobs',
- '/dashboard/production/jobs/info': 'Info',
- '/dashboard/production/gcodefiles': 'G Code Files',
- '/dashboard/production/gcodefiles/info': 'Info',
- '/dashboard/management/filaments': 'Filaments',
- '/dashboard/management/filaments/info': 'Info',
- '/dashboard/management/parts': 'Parts',
- '/dashboard/management/parts/info': 'Info',
- '/dashboard/management/products': 'Products',
- '/dashboard/management/products/info': 'Info',
- '/dashboard/management/vendors': 'Vendors',
- '/dashboard/management/vendors/info': 'Info',
- '/dashboard/management/materials': 'Materials',
- '/dashboard/management/materials/info': 'Info',
- '/dashboard/management/notetypes': 'Note Types',
- '/dashboard/management/notetypes/info': 'Info',
- '/dashboard/management/users': 'Users',
- '/dashboard/management/users/info': 'Info',
- '/dashboard/management/settings': 'Settings',
- '/dashboard/management/auditlogs': 'Audit Logs',
- '/dashboard/inventory/filamentstocks': 'Filament Stocks',
- '/dashboard/inventory/filamentstocks/info': 'Info',
- '/dashboard/inventory/partstocks': 'Part Stocks',
- '/dashboard/inventory/partstocks/info': 'Info',
- '/dashboard/inventory/productstocks': 'Products',
- '/dashboard/inventory/productstocks/info': 'Info',
- '/dashboard/inventory/stockevents': 'Stock Events',
- '/dashboard/inventory/stockevents/info': 'Info',
- '/dashboard/inventory/stockaudits': 'Stock Audits',
- '/dashboard/inventory/stockaudits/info': 'Info',
- '/dashboard/developer/sessionstorage': 'Session Storage',
- '/dashboard/developer/authcontextdebug': 'Auth Context Debug',
- '/dashboard/developer/printservercontextdebug': 'Print Server Context Debug'
+ production: 'Production',
+ inventory: 'Inventory',
+ management: 'Management',
+ developer: 'Developer',
+ overview: 'Overview',
+ printers: 'Printers',
+ hosts: 'Hosts',
+ control: 'Control',
+ info: 'Info',
+ jobs: 'Jobs',
+ subjobs: 'Sub Jobs',
+ gcodefiles: 'G Code Files',
+ filaments: 'Filaments',
+ parts: 'Parts',
+ products: 'Products',
+ vendors: 'Vendors',
+ materials: 'Materials',
+ notetypes: 'Note Types',
+ users: 'Users',
+ settings: 'Settings',
+ auditlogs: 'Audit Logs',
+ filamentstocks: 'Filament Stocks',
+ partstocks: 'Part Stocks',
+ productstocks: 'Products',
+ stockevents: 'Stock Events',
+ stockaudits: 'Stock Audits',
+ sessionstorage: 'Session Storage',
+ authcontextdebug: 'Auth Context Debug',
+ printservercontextdebug: 'Print Server Context Debug'
}
+const mainSections = ['production', 'inventory', 'management', 'developer']
+
const DashboardBreadcrumb = () => {
const location = useLocation()
const navigate = useNavigate()
const pathSnippets = location.pathname.split('/').filter((i) => i)
- const breadcrumbItems = pathSnippets.map((_, index) => {
+ const breadcrumbItems = pathSnippets.map((segment, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
- if (url != '/dashboard') {
- // Check if this is a main section (Production, Inventory, or Management)
- const isMainSection =
- url === '/dashboard/production' ||
- url === '/dashboard/inventory' ||
- url === '/dashboard/management'
-
+ if (segment !== 'dashboard') {
+ const isMainSection = mainSections.includes(segment)
+ const name = breadcrumbNameMap[segment] || segment
return {
title: isMainSection ? (
- {breadcrumbNameMap[url]}
+ {name}
) : (
- {breadcrumbNameMap[url]}
+ {name}
),
key: url
diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx
index f87ad1a..fa01e3c 100644
--- a/src/components/Dashboard/common/DashboardNavigation.jsx
+++ b/src/components/Dashboard/common/DashboardNavigation.jsx
@@ -37,6 +37,8 @@ import SearchIcon from '../../Icons/SearchIcon'
import SettingsIcon from '../../Icons/SettingsIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import PrinterIcon from '../../Icons/PrinterIcon'
+import { ElectronContext } from '../context/ElectronContext'
+import DashboardWindowButtons from './DashboardWindowButtons'
const DashboardNavigation = () => {
const { logout, userProfile } = useContext(AuthContext)
@@ -50,6 +52,7 @@ const DashboardNavigation = () => {
const location = useLocation()
const [selectedKey, setSelectedKey] = useState('production')
const isMobile = useMediaQuery({ maxWidth: 768 })
+ const { platform, isElectron } = useContext(ElectronContext)
useEffect(() => {
const pathParts = location.pathname.split('/').filter(Boolean)
@@ -135,155 +138,176 @@ const DashboardNavigation = () => {
}
}
- return (
-
-
+ {isElectron && platform == 'darwin' ? : null}
+ {!isElectron && !isMobile ? (
+
+ ) : !isElectron && isMobile ? (
+
+ ) : null}
+
-
-
+ >
+ )
+
+ return (
+ <>
+ {isElectron ? (
+
+ {navigationContents}
+
+ ) : (
+
+
+
+ {navigationContents}
+
+
+
+
+ )}
+ >
)
}
diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx
index 4ae8b34..235ad34 100644
--- a/src/components/Dashboard/common/DashboardSidebar.jsx
+++ b/src/components/Dashboard/common/DashboardSidebar.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react'
+import React, { useState, useEffect, useContext } from 'react'
import { Layout, Menu, Flex, Button } from 'antd'
import { CaretDownFilled } from '@ant-design/icons'
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
@@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
import { useMediaQuery } from 'react-responsive'
import { useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
-
+import { ElectronContext } from '../context/ElectronContext'
const { Sider } = Layout
const DashboardSidebar = ({
@@ -24,6 +24,8 @@ const DashboardSidebar = ({
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
+ const { isElectron } = useContext(ElectronContext)
+
useEffect(() => {
if (typeof collapsedProp === 'boolean') {
setCollapsed(collapsedProp)
@@ -56,6 +58,7 @@ const DashboardSidebar = ({
selectedKeys={[selectedKey]}
items={_items}
_internalDisableMenuItemTitleTooltip
+ style={{ lineHeight: '40px' }}
overflowedIndicator={} />}
/>
)
@@ -72,6 +75,7 @@ const DashboardSidebar = ({
mode='inline'
selectedKeys={[selectedKey]}
items={_items}
+ className={isElectron ? 'electron-sidebar' : null}
style={{ flexGrow: 1, border: 'none' }}
_internalDisableMenuItemTitleTooltip
/>
diff --git a/src/components/Dashboard/common/DashboardWindowButtons.jsx b/src/components/Dashboard/common/DashboardWindowButtons.jsx
new file mode 100644
index 0000000..31d3a06
--- /dev/null
+++ b/src/components/Dashboard/common/DashboardWindowButtons.jsx
@@ -0,0 +1,50 @@
+import React, { useContext } from 'react'
+import { Flex, Button } from 'antd'
+import { ElectronContext } from '../context/ElectronContext'
+import XMarkIcon from '../../Icons/XMarkIcon'
+import MinusIcon from '../../Icons/MinusIcon'
+import ContractIcon from '../../Icons/ContractIcon'
+import ExpandIcon from '../../Icons/ExpandIcon'
+
+const DashboardWindowButtons = () => {
+ const { isMaximized, handleWindowControl, platform } =
+ useContext(ElectronContext)
+
+ const closeButton = (
+ }
+ type={'text'}
+ onClick={() => handleWindowControl('close')}
+ />
+ )
+ const maximizeButton = (
+ }
+ type={'text'}
+ onClick={() => handleWindowControl('minimize')}
+ />
+ )
+ const minimizeButton = (
+ : }
+ type={'text'}
+ onClick={() => handleWindowControl('maximize')}
+ />
+ )
+
+ return (
+
+ {platform == 'darwin' ? (
+ <>
+ {closeButton}
+ {minimizeButton}
+ {maximizeButton}
+ >
+ ) : (
+ <>{closeButton}>
+ )}
+
+ )
+}
+
+export default DashboardWindowButtons
diff --git a/src/components/Dashboard/common/DeleteObjectModal.jsx b/src/components/Dashboard/common/DeleteObjectModal.jsx
index 430e6c1..bc9a768 100644
--- a/src/components/Dashboard/common/DeleteObjectModal.jsx
+++ b/src/components/Dashboard/common/DeleteObjectModal.jsx
@@ -6,14 +6,7 @@ import BinIcon from '../../Icons/BinIcon'
const { Text } = Typography
-const DeleteObjectModal = ({
- open,
- onOk,
- onCancel,
- loading,
- objectType,
- objectName
-}) => {
+const DeleteObjectModal = ({ open, onOk, onCancel, loading, objectType }) => {
const model = getModelByName(objectType)
return (
- Are you sure you want to delete this {model.label.toLowerCase()}
- {objectName ? ` "${objectName}"` : ''}?
+ Are you sure you want to delete this {model.label.toLowerCase()}?
)
@@ -61,8 +53,7 @@ DeleteObjectModal.propTypes = {
onOk: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
loading: PropTypes.bool,
- objectType: PropTypes.string.isRequired,
- objectName: PropTypes.string
+ objectType: PropTypes.string.isRequired
}
export default DeleteObjectModal
diff --git a/src/components/Dashboard/common/EditObjectForm.jsx b/src/components/Dashboard/common/EditObjectForm.jsx
index e3e8835..cbebdbb 100644
--- a/src/components/Dashboard/common/EditObjectForm.jsx
+++ b/src/components/Dashboard/common/EditObjectForm.jsx
@@ -4,6 +4,7 @@ import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import PropTypes from 'prop-types'
import DeleteObjectModal from './DeleteObjectModal'
+import merge from 'lodash/merge'
/**
* EditObjectForm is a reusable form component for editing any object type.
@@ -82,7 +83,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
- setObjectData((prev) => ({ ...prev, ...value }))
+ setObjectData((prev) => merge({}, prev, value))
}, [])
// Update event handler
diff --git a/src/components/Dashboard/common/GCodeFileSelect.jsx b/src/components/Dashboard/common/GCodeFileSelect.jsx
index 6eb612f..935d1c1 100644
--- a/src/components/Dashboard/common/GCodeFileSelect.jsx
+++ b/src/components/Dashboard/common/GCodeFileSelect.jsx
@@ -13,7 +13,7 @@ const propertyOrder = [
const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
return (
{
showSearch={true}
style={style}
placeholder='Select GCode File'
- type='gcodefile'
+ type='gcodeFile'
/>
)
}
diff --git a/src/components/Dashboard/Management/Filaments/LockIndicator.jsx b/src/components/Dashboard/common/LockIndicator.jsx
similarity index 85%
rename from src/components/Dashboard/Management/Filaments/LockIndicator.jsx
rename to src/components/Dashboard/common/LockIndicator.jsx
index 5f7e35d..3d86ee5 100644
--- a/src/components/Dashboard/Management/Filaments/LockIndicator.jsx
+++ b/src/components/Dashboard/common/LockIndicator.jsx
@@ -1,7 +1,7 @@
import React from 'react'
import { Flex, Tag } from 'antd'
-import IdDisplay from '../../common/IdDisplay'
-import LockIcon from '../../../Icons/LockIcon'
+import IdDisplay from './IdDisplay'
+import LockIcon from '../../Icons/LockIcon'
import PropTypes from 'prop-types'
const LockIndicator = ({ lock }) => {
diff --git a/src/components/Dashboard/common/NotesPanel.jsx b/src/components/Dashboard/common/NotesPanel.jsx
index ecae51a..0542123 100644
--- a/src/components/Dashboard/common/NotesPanel.jsx
+++ b/src/components/Dashboard/common/NotesPanel.jsx
@@ -95,7 +95,7 @@ const NoteItem = ({
}
// Reload child notes when a new child note is added
- const reloadChildNotes = async () => {
+ const reloadChildNotes = useCallback(async () => {
// Always fetch child notes when this function is called
// This ensures child notes are loaded even if the parent wasn't expanded before
setLoadingChildNotes(note._id)
@@ -110,14 +110,14 @@ const NoteItem = ({
} finally {
setLoadingChildNotes(null)
}
- }
+ }, [fetchData, note._id])
// Listen for child note additions
useEffect(() => {
if (onChildNoteAdded) {
onChildNoteAdded(note._id, reloadChildNotes)
}
- }, [note._id, onChildNoteAdded])
+ }, [note._id, onChildNoteAdded, reloadChildNotes])
// Check if the current user can delete this note
const canDeleteNote = userProfile && userProfile._id === note.user._id
diff --git a/src/components/Dashboard/common/ObjectDisplay.jsx b/src/components/Dashboard/common/ObjectDisplay.jsx
new file mode 100644
index 0000000..cbad204
--- /dev/null
+++ b/src/components/Dashboard/common/ObjectDisplay.jsx
@@ -0,0 +1,27 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Tag, Typography } from 'antd'
+import { getModelByName } from '../../../database/ObjectModels'
+
+const { Text } = Typography
+
+const ObjectDisplay = ({ object, objectType }) => {
+ if (!object) {
+ return n/a
+ }
+ const model = getModelByName(objectType)
+ const Icon = model.icon
+ return (
+ }>
+ {object?.name ? object.name : null}
+
+ )
+}
+
+ObjectDisplay.propTypes = {
+ object: PropTypes.object,
+ objectType: PropTypes.string,
+ style: PropTypes.object
+}
+
+export default ObjectDisplay
diff --git a/src/components/Dashboard/common/ObjectList.jsx b/src/components/Dashboard/common/ObjectList.jsx
index aad9252..c266684 100644
--- a/src/components/Dashboard/common/ObjectList.jsx
+++ b/src/components/Dashboard/common/ObjectList.jsx
@@ -1,49 +1,28 @@
import React from 'react'
import PropTypes from 'prop-types'
-import { List, Typography, Flex } from 'antd'
-import { getModelByName } from '../../../database/ObjectModels'
-import IdDisplay from './IdDisplay'
+import ObjectDisplay from './ObjectDisplay'
+import { Space, Typography } from 'antd'
const { Text } = Typography
-const ObjectList = ({ value, objectType, bordered = true }) => {
+const ObjectList = ({ value, objectType, style }) => {
if (!value || !Array.isArray(value) || value.length === 0) {
return n/a
}
+
return (
- {
- const model = getModelByName(objectType)
- const Icon = model.icon
- return (
-
-
-
- {item?.name ? {item.name} : null}
- {item?._id ? (
-
- ) : null}
-
-
- )
- }}
- style={{ width: '100%' }}
- />
+
+ {value.map((item) => (
+
+ ))}
+
)
}
ObjectList.propTypes = {
value: PropTypes.array,
- bordered: PropTypes.bool,
- objectType: PropTypes.string
+ objectType: PropTypes.string,
+ style: PropTypes.object
}
export default ObjectList
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index c702d88..38d6e71 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -10,14 +10,9 @@ import {
DatePicker,
Switch
} from 'antd'
-import VendorSelect from './VendorSelect'
-import FilamentSelect from './FilamentSelect'
import IdDisplay from './IdDisplay'
import TimeDisplay from './TimeDisplay'
import dayjs from 'dayjs'
-import PrinterSelect from './PrinterSelect'
-import GCodeFileSelect from './GCodeFileSelect'
-import PartSelect from './PartSelect'
import EmailDisplay from './EmailDisplay'
import UrlDisplay from './UrlDisplay'
import CountryDisplay from './CountryDisplay'
@@ -41,6 +36,8 @@ import ObjectList from './ObjectList'
import VarianceDisplay from './VarianceDisplay'
import OperationDisplay from './OperationDisplay'
import MarkdownDisplay from './MarkdownDisplay'
+import ObjectSelect from './ObjectSelect'
+import ObjectDisplay from './ObjectDisplay'
const { Text } = Typography
@@ -291,7 +288,7 @@ const ObjectProperty = ({
}
case 'object': {
if (value && value.name) {
- return {value.name}
+ return
} else {
return (
@@ -424,7 +421,7 @@ const ObjectProperty = ({
}
const hasRequiredRule = rules.some((rule) => rule && rule.required)
if (!hasRequiredRule) {
- rules.push({ required: true, message: 'This field is required' })
+ rules.push({ required: true, message: '' })
}
mergedFormItemProps.rules = rules
}
@@ -591,45 +588,17 @@ const ObjectProperty = ({
)
}
case 'object':
- switch (objectType) {
- case 'vendor':
- return (
-
-
-
- )
- case 'printer':
- return (
-
-
-
- )
- case 'gcodeFile':
- return (
-
-
-
- )
- case 'filament':
- return (
-
-
-
- )
- case 'part':
- return (
-
-
-
- )
- default:
- return (
-
- n/a
-
- )
- }
-
+ return (
+
+
+
+ )
+ case 'objectList':
+ return (
+
+
+
+ )
case 'tags':
return (
diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index 9dbf836..114a1e4 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -1,340 +1,215 @@
-import React, { useEffect, useState, useCallback } from 'react'
+import React, {
+ useEffect,
+ useState,
+ useContext,
+ useCallback,
+ useMemo
+} from 'react'
import PropTypes from 'prop-types'
-import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd'
-import axios from 'axios'
-import { getModelByName } from '../../../database/ObjectModels'
-import IdDisplay from './IdDisplay'
+import { TreeSelect, Space, Button, Input } from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon'
+import { ApiServerContext } from '../context/ApiServerContext'
import ObjectProperty from './ObjectProperty'
-const { Text } = Typography
+import { getModelByName } from '../../../database/ObjectModels'
+import merge from 'lodash/merge'
const { SHOW_CHILD } = TreeSelect
-// --- Utility: Resolve nested property path (e.g., 'filament.diameter') ---
-function resolvePropertyPath(obj, path) {
- if (!obj || !path) return { value: undefined, finalProperty: undefined }
- const props = path.split('.')
- let value = obj
- for (const prop of props) {
- if (value && typeof value === 'object') {
- value = value[prop]
- } else {
- return { value: undefined, finalProperty: prop }
- }
- }
- return { value, finalProperty: props[props.length - 1] }
-}
-
-// --- Utility: Build filter object for a node based on propertyOrder ---
-function buildFilterForNode(node, treeData, propertyOrder) {
- let filterObj = {}
- let currentId = node.id
- while (currentId !== 0) {
- const currentNode = treeData.find((d) => d.id === currentId)
- if (!currentNode) break
- filterObj[propertyOrder[currentNode.propertyId]] = currentNode.value
- currentId = currentNode.pId
- }
- return filterObj
-}
-
-/**
- * ObjectSelect - a generic, reusable async TreeSelect for hierarchical object selection.
- *
- * Props:
- * - endpoint: API endpoint to fetch data from (required)
- * - 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, 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 = ({
- endpoint,
- propertyOrder,
- filter = {},
- useFilter = false,
- value,
- onChange,
- showSearch = false,
- treeCheckable = false,
- treeSelectProps = {},
type = 'unknown',
+ showSearch = false,
+ multiple = false,
+ treeSelectProps = {},
+ filter = {},
+ value,
...rest
}) => {
+ const { fetchObjectsByProperty } = useContext(ApiServerContext)
// --- State ---
const [treeData, setTreeData] = useState([])
- const [loading, setLoading] = useState(false)
- const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
- const [searchValue, setSearchValue] = useState('')
+ const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
+ const [initialized, setInitialized] = useState(false)
const [error, setError] = useState(false)
+ const properties = useMemo(() => getModelByName(type).group || [], [type])
+ const [objectList, setObjectList] = useState([])
+ const [treeSelectValue, setTreeSelectValue] = useState(null)
+ const [initialLoading, setInitialLoading] = useState(true)
- // --- API: Fetch data for a property level or leaf ---
- const fetchData = useCallback(
- async (property, filter, search) => {
- setLoading(true)
- setError(false)
+ // Fetch the object properties tree from the API
+ const handleFetchObjectsProperties = useCallback(
+ async (customFilter = filter) => {
try {
- const params = { ...filter, property }
- if (search) params.search = search
- const response = await axios.get(endpoint, {
- params,
- withCredentials: true
+ const data = await fetchObjectsByProperty(type, {
+ properties: properties,
+ filter: customFilter
})
- setLoading(false)
- return response.data
+ setObjectPropertiesTree((prev) => merge({}, prev, data))
+ setInitialLoading(false)
+ setError(false)
+ return data
} catch (err) {
- setLoading(false)
- setError(true)
- return []
- }
- },
- [endpoint]
- )
-
- // --- API: Fetch a single object by ID ---
- const fetchObjectById = useCallback(
- async (objectId) => {
- setLoading(true)
- setError(false)
- try {
- const response = await axios.get(`${endpoint}/${objectId}`, {
- withCredentials: true
- })
- setLoading(false)
- return response.data
- } catch (err) {
- setLoading(false)
setError(true)
return null
}
},
- [endpoint]
+ [type, fetchObjectsByProperty, properties, filter]
)
- // --- Render node title ---
- const renderTitle = useCallback(
- (item) => {
- if (item.propertyType) {
- return (
-
- )
- } else {
- const model = getModelByName(type)
- const Icon = model.icon
- return (
-
- {Icon && }
- {item?.color && }
- {item.name || type.title}
-
-
- )
- }
- },
- [type]
- )
-
- // --- Build tree nodes for a property level ---
- const buildCategoryNodes = useCallback(
- (data, propertyName, propertyId, parentId) => {
- return data.map((item) => {
- let resolved = resolvePropertyPath(item, propertyName)
- let value = resolved.value
- let propertyType = resolved.finalProperty
- return {
- id: value,
- pId: parentId,
- value: value,
- key: value,
- propertyId: propertyId,
- title: renderTitle({ ...item, value, propertyType }),
- isLeaf: false,
- selectable: false,
- raw: item
- }
- })
- },
- [renderTitle]
- )
-
- // --- Build tree nodes for leaf level ---
- const buildLeafNodes = useCallback(
- (data, parentId) => {
- return data.map((item) => {
- const value = item._id || item.id || item.value
- return {
- id: value,
- pId: parentId,
- value: value,
- key: value,
- title: renderTitle(item),
- isLeaf: true,
- raw: item
- }
- })
- },
- [renderTitle]
- )
-
- // --- Tree loader: load children for a node or root ---
- const handleTreeLoad = useCallback(
- async (node) => {
- if (!propertyOrder.length) return
- if (node) {
- // Not at leaf level yet
- if (node.propertyId !== propertyOrder.length - 1) {
- const nextPropertyId = node.propertyId + 1
- const propertyName = propertyOrder[nextPropertyId]
- const filterObj = buildFilterForNode(node, treeData, propertyOrder)
- const data = await fetchData(propertyName, filterObj, searchValue)
- setTreeData((prev) => [
- ...prev,
- ...buildCategoryNodes(data, propertyName, nextPropertyId, node.id)
- ])
- } else {
- // At leaf level
- const filterObj = buildFilterForNode(node, treeData, propertyOrder)
- const data = await fetchData(null, filterObj, searchValue)
- setTreeData((prev) => [...prev, ...buildLeafNodes(data, node.id)])
- }
- } else {
- // Root load
- const propertyName = propertyOrder[0]
- const data = await fetchData(propertyName, {}, searchValue)
- setTreeData(buildCategoryNodes(data, propertyName, 0, 0))
- }
- },
- [
- propertyOrder,
- treeData,
- fetchData,
- buildCategoryNodes,
- buildLeafNodes,
- searchValue
- ]
- )
-
- // --- OnChange handler ---
- const handleOnChange = (val, selectedOptions) => {
- if (onChange) {
- if (treeCheckable) {
- // Multi-select
- let selectedObjects = []
- if (Array.isArray(val)) {
- selectedObjects = val.map((selectedValue) => {
- const node = treeData.find((n) => n.value === selectedValue)
- return node ? node.raw : selectedValue
- })
- }
- onChange(selectedObjects, selectedOptions)
- } else {
- // Single select
- const node = treeData.find((n) => n.value === val)
- onChange(node ? node.raw : val, selectedOptions)
- }
- }
- setDefaultValue(val)
- }
-
- // --- Search handler ---
- const handleSearch = (val) => {
- setSearchValue(val)
- setTreeData([])
- }
-
- // --- Sync defaultValue and load tree path for object values ---
- useEffect(() => {
- if (treeCheckable) {
- if (Array.isArray(value)) {
- const valueIds = value.map((v) => v._id || v.id || v)
- setDefaultValue(valueIds)
- value.forEach((item) => {
- if (item && typeof item === 'object' && item._id) {
- const existingNode = treeData.find(
- (node) => node.value === item._id
+ // Convert the API response to AntD TreeSelect treeData
+ const buildTreeData = useCallback(
+ (data, pIdx = 0, parentKeys = [], filterPath = []) => {
+ if (!data) return []
+ if (Array.isArray(data)) {
+ return data.map((object) => {
+ setObjectList((prev) => {
+ const filtered = prev.filter(
+ (prevObject) => prevObject._id != object._id
)
- if (!existingNode) {
- fetchObjectById(item._id).then((object) => {
- if (object) {
- // For multi-select, just add the leaf node
- setTreeData((prev) => [
- ...prev,
- ...buildLeafNodes(
- [object],
- object[propertyOrder[propertyOrder.length - 2]] || 0
- )
- ])
- }
- })
- }
+ return [...filtered, object]
+ })
+ return {
+ title: (
+
+ ),
+ value: object._id,
+ key: object._id,
+ isLeaf: true,
+ property: properties[pIdx - 1], // previous property
+ parentKeys,
+ filterPath
}
})
- } else {
- setDefaultValue([])
}
- } else {
- if (value?._id) {
- setDefaultValue(value._id)
- const existingNode = treeData.find((node) => node.value === value._id)
- if (!existingNode) {
- fetchObjectById(value._id).then((object) => {
- if (object) {
- setTreeData((prev) => [
- ...prev,
- ...buildLeafNodes(
- [object],
- object[propertyOrder[propertyOrder.length - 2]] || 0
- )
- ])
+ if (typeof data == 'object') {
+ const property = properties[pIdx] || null
+ return Object.entries(data)
+ .map(([key, value]) => {
+ if (property != null && typeof value === 'object') {
+ const newFilterPath = filterPath.concat({ property, value: key })
+ return {
+ title: ,
+ value: key,
+ key: parentKeys.concat(key).join(':'),
+ property,
+ parentKeys: parentKeys.concat(key),
+ filterPath: newFilterPath,
+ selectable: false,
+ children: buildTreeData(
+ value,
+ pIdx + 1,
+ parentKeys.concat(key),
+ newFilterPath
+ ),
+ isLeaf: false
+ }
}
})
- }
+ .filter(Boolean)
}
- }
- }, [
- value,
- treeData,
- fetchObjectById,
- buildLeafNodes,
- propertyOrder,
- treeCheckable
- ])
+ },
+ [properties, type]
+ )
- // --- Initial load ---
- useEffect(() => {
- if (treeData.length === 0 && !error && !loading) {
- if (!treeCheckable && value && typeof value === 'object' && value._id) {
- return
- }
- if (useFilter || searchValue) {
- // Flat filter mode
- fetchData(null, filter, searchValue).then((data) => {
- setTreeData(buildLeafNodes(data, 0))
- })
- } else {
- handleTreeLoad(null)
- }
+ // --- loadData for async loading on expand ---
+ const loadData = async (node) => {
+ // node.property is the property name, node.value is the value
+ if (!node.property) return
+ // Build filter for this node by merging all parent property-value pairs
+ const customFilter = { ...filter }
+ if (Array.isArray(node.filterPath)) {
+ node.filterPath.forEach(({ property, value }) => {
+ customFilter[property] = value
+ })
}
- }, [
- treeData,
- useFilter,
- filter,
- searchValue,
- buildLeafNodes,
- fetchData,
- handleTreeLoad,
- error,
- loading,
- value,
- treeCheckable
- ])
+ customFilter[node.property] = node.value
+ // Fetch children for this node
+ const data = await handleFetchObjectsProperties(customFilter)
+ if (!data) return
+ // Build new children
+ const children = buildTreeData(
+ data,
+ properties.indexOf(node.property) + 1,
+ node.parentKeys || [],
+ (node.filterPath || []).concat({
+ property: node.property,
+ value: node.value
+ })
+ )
+ // Update treeData with new children for this node
+ setTreeData((prevTreeData) => {
+ // Helper to recursively update the correct node
+ const updateNode = (nodes) =>
+ nodes.map((n) => {
+ if (n.key === node.key) {
+ return { ...n, children, isLeaf: children.length === 0 }
+ } else if (n.children) {
+ return { ...n, children: updateNode(n.children) }
+ }
+ return n
+ })
+ return updateNode(prevTreeData)
+ })
+ }
+
+ const onTreeSelectChange = (value) => {
+ // value can be a string (single) or array (multiple)
+ if (multiple) {
+ // Multiple selection
+ let selectedObjects = []
+ if (Array.isArray(value)) {
+ selectedObjects = value
+ .map((id) => objectList.find((obj) => obj._id === id))
+ .filter(Boolean)
+ }
+ setTreeSelectValue(value)
+ if (rest.onChange) rest.onChange(selectedObjects)
+ } else {
+ // Single selection
+ const selectedObject = objectList.find((obj) => obj._id === value)
+ setTreeSelectValue(value)
+ if (rest.onChange) rest.onChange(selectedObject)
+ }
+ }
+
+ // Update treeData when objectPropertiesTree changes
+ useEffect(() => {
+ if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
+ const newTreeData = buildTreeData(objectPropertiesTree)
+ setTreeData((prev) => {
+ if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
+ return newTreeData
+ }
+ return prev
+ })
+ }
+ }, [objectPropertiesTree, properties, type, buildTreeData])
+
+ useEffect(() => {
+ if (value && typeof value === 'object' && value !== null && !initialized) {
+ // Build a new filter from value's properties that are in the properties list
+ const valueFilter = { ...filter }
+ properties.forEach((prop) => {
+ if (Object.prototype.hasOwnProperty.call(value, prop)) {
+ valueFilter[prop] = value[prop]
+ }
+ })
+ // Fetch with the new filter
+ handleFetchObjectsProperties(valueFilter)
+ setTreeSelectValue(value._id)
+ setInitialized(true)
+ return
+ }
+ if (!initialized) {
+ handleFetchObjectsProperties()
+ setInitialized(true)
+ }
+ }, [value, filter, properties, handleFetchObjectsProperties, initialized])
// --- Error UI ---
if (error) {
@@ -346,6 +221,7 @@ const ObjectSelect = ({
onClick={() => {
setError(false)
setTreeData([])
+ setInitialized(false)
}}
danger
/>
@@ -353,35 +229,37 @@ const ObjectSelect = ({
)
}
+ if (initialLoading) {
+ return
+ }
+
// --- Main TreeSelect UI ---
return (
)
}
ObjectSelect.propTypes = {
- endpoint: PropTypes.string.isRequired,
- propertyOrder: PropTypes.arrayOf(PropTypes.string).isRequired,
+ properties: PropTypes.arrayOf(PropTypes.string).isRequired,
filter: PropTypes.object,
useFilter: PropTypes.bool,
value: PropTypes.any,
onChange: PropTypes.func,
showSearch: PropTypes.bool,
- treeCheckable: PropTypes.bool,
+ multiple: PropTypes.bool,
treeSelectProps: PropTypes.object,
type: PropTypes.string.isRequired
}
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 7cb119b..ba566d8 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -38,6 +38,7 @@ import CheckIcon from '../../Icons/CheckIcon'
import { useNavigate } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
+import merge from 'lodash/merge'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@@ -65,7 +66,7 @@ const ObjectTable = forwardRef(
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
}
if (cards) {
- adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
+ adjustedScrollHeight = 'calc(var(--unit-100vh) - 280px)'
}
const [, contextHolder] = message.useMessage()
const tableRef = useRef(null)
@@ -108,7 +109,6 @@ const ObjectTable = forwardRef(
type={'text'}
size={'small'}
onClick={() => {
- console.log(objectData)
if (action.url) {
navigate(action.url(objectData._id))
}
@@ -135,13 +135,6 @@ const ObjectTable = forwardRef(
order: sorter.order
})
}
- console.log('Fetching Objects', {
- page: pageNum,
- limit: pageSize,
- filter: { ...filter, ...masterFilter },
- sorter,
- onDataChange
- })
try {
const result = await fetchObjects(type, {
page: pageNum,
@@ -250,12 +243,6 @@ const ObjectTable = forwardRef(
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
const prevPage = lowestPage - 1
- logger.debug(
- 'Down',
- scrollHeight - scrollTop - clientHeight < 100,
- lazyLoading
- )
-
// Load more data when scrolling down
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore) {
setTimeout(() => {
@@ -276,13 +263,14 @@ const ObjectTable = forwardRef(
loadPreviousPage()
}
},
- [lazyLoading, loadNextPage, loadPreviousPage]
+ [lazyLoading, loadNextPage, loadPreviousPage, hasMore, pages]
)
- const reload = useCallback(() => {
+ const reload = useCallback(async () => {
+ setLazyLoading(true)
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
- fetchData(page.pageNum)
+ await fetchData(page.pageNum)
}
}, [fetchData, pages])
@@ -292,7 +280,7 @@ const ObjectTable = forwardRef(
prevPages.map((page) => ({
...page,
items: page.items.map((item) =>
- item._id === updatedData._id ? { ...item, ...updatedData } : item
+ item._id === updatedData._id ? merge({}, item, updatedData) : item
)
}))
)
@@ -453,7 +441,7 @@ const ObjectTable = forwardRef(
// Table columns from model properties
const columnsWithSkeleton = [
{
- title: model.icon,
+ title: lazyLoading ? : cards ? model.icon : null,
key: 'icon',
width: 45,
fixed: 'left',
@@ -688,16 +676,12 @@ const ObjectTable = forwardRef(
return (
<>
{contextHolder}
- {cards ? (
- } spinning={loading}>
- {renderCards()}
-
- ) : (
+
- )}
+ {cards ? (
+ } spinning={loading}>
+ {renderCards()}
+
+ ) : null}
+
>
)
}
diff --git a/src/components/Dashboard/common/PrinterMovementPanel.jsx b/src/components/Dashboard/common/PrinterMovementPanel.jsx
index 1b41daa..86545e7 100644
--- a/src/components/Dashboard/common/PrinterMovementPanel.jsx
+++ b/src/components/Dashboard/common/PrinterMovementPanel.jsx
@@ -41,7 +41,6 @@ const PrinterMovementPanel = ({ printerId }) => {
const handleHomeAxisClick = (axis) => {
if (printServer) {
- logger.debug('Homeing Axis:', axis)
printServer.emit('printer.gcode.script', {
printerId,
script: `G28 ${axis}`
@@ -52,7 +51,6 @@ const PrinterMovementPanel = ({ printerId }) => {
const handleMoveAxisClick = (axis, minus) => {
const distanceValue = !minus ? posValue * -1 : posValue
if (printServer) {
- logger.debug('Moving Axis:', axis, distanceValue)
printServer.emit('printer.gcode.script', {
printerId,
script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}`
diff --git a/src/components/Icons/ContractIcon.jsx b/src/components/Icons/ContractIcon.jsx
new file mode 100644
index 0000000..b4f87e8
--- /dev/null
+++ b/src/components/Icons/ContractIcon.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import Icon from '@ant-design/icons'
+import { ReactComponent as CustomIconSvg } from '../../assets/icons/contracticon.min.svg'
+
+const ContractIcon = (props) =>
+
+export default ContractIcon
diff --git a/src/components/Icons/ExpandIcon.jsx b/src/components/Icons/ExpandIcon.jsx
new file mode 100644
index 0000000..84548a1
--- /dev/null
+++ b/src/components/Icons/ExpandIcon.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import Icon from '@ant-design/icons'
+import { ReactComponent as CustomIconSvg } from '../../assets/icons/expandicon.min.svg'
+
+const ExpandIcon = (props) =>
+
+export default ExpandIcon
diff --git a/src/components/Icons/HostIcon.jsx b/src/components/Icons/HostIcon.jsx
new file mode 100644
index 0000000..9e5c1a7
--- /dev/null
+++ b/src/components/Icons/HostIcon.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import Icon from '@ant-design/icons'
+import { ReactComponent as CustomIconSvg } from '../../assets/icons/hosticon.min.svg'
+
+const HostIcon = (props) =>
+
+export default HostIcon
diff --git a/src/components/Icons/MinusIcon.jsx b/src/components/Icons/MinusIcon.jsx
new file mode 100644
index 0000000..6ca8ab0
--- /dev/null
+++ b/src/components/Icons/MinusIcon.jsx
@@ -0,0 +1,7 @@
+import React from 'react'
+import Icon from '@ant-design/icons'
+import { ReactComponent as CustomIconSvg } from '../../assets/icons/minusicon.min.svg'
+
+const MinusIcon = (props) =>
+
+export default MinusIcon
diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js
index b986b24..b3e7586 100644
--- a/src/database/ObjectModels.js
+++ b/src/database/ObjectModels.js
@@ -1,4 +1,5 @@
import { Printer } from './models/Printer.js'
+import { Host } from './models/Host.js'
import { Filament } from './models/Filament.js'
import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile'
@@ -21,6 +22,7 @@ import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
Printer,
+ Host,
Filament,
Spool,
GCodeFile,
@@ -44,6 +46,7 @@ export const objectModels = [
// Re-export individual models for direct access
export {
Printer,
+ Host,
Filament,
Spool,
GCodeFile,
diff --git a/src/database/models/Host.js b/src/database/models/Host.js
new file mode 100644
index 0000000..89097cb
--- /dev/null
+++ b/src/database/models/Host.js
@@ -0,0 +1,90 @@
+import HostIcon from '../../components/Icons/HostIcon'
+import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
+import ReloadIcon from '../../components/Icons/ReloadIcon'
+import EditIcon from '../../components/Icons/EditIcon'
+
+export const Host = {
+ name: 'host',
+ label: 'Host',
+ prefix: 'HST',
+ icon: HostIcon,
+ actions: [
+ {
+ name: 'info',
+ label: 'Info',
+ default: true,
+ row: true,
+ icon: InfoCircleIcon,
+ url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}`
+ },
+
+ {
+ name: 'reload',
+ label: 'Reload',
+ icon: ReloadIcon,
+ url: (_id) =>
+ `/dashboard/production/hosts/info?hostId=${_id}&action=reload`
+ },
+ {
+ name: 'edit',
+ label: 'Edit',
+ row: true,
+ icon: EditIcon,
+ url: (_id) => `/dashboard/production/hosts/info?hostId=${_id}&action=edit`
+ }
+ ],
+ columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
+ filters: ['name', '_id', 'state', 'tags'],
+ sorters: ['name', 'state', 'connectedAt'],
+ group: ['tags'],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ type: 'id',
+ objectType: 'host',
+ showCopy: true
+ },
+ {
+ name: 'connectedAt',
+ label: 'Connected At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'name',
+ label: 'Name',
+ required: true,
+ type: 'text',
+ columnWidth: 200,
+ columnFixed: 'left'
+ },
+ {
+ name: 'state',
+ label: 'Status',
+ type: 'state',
+ objectType: 'host',
+ showName: false,
+ readOnly: true
+ },
+ {
+ name: 'host',
+ label: 'Host',
+ type: 'text',
+ required: true
+ },
+ {
+ name: 'tags',
+ label: 'Tags',
+ type: 'tags',
+ required: false
+ },
+ {
+ name: 'operatingSystem',
+ label: 'Operating System',
+ type: 'text',
+ required: false,
+ readOnly: true
+ }
+ ]
+}
diff --git a/src/database/models/Job.js b/src/database/models/Job.js
index 7127d2c..6997805 100644
--- a/src/database/models/Job.js
+++ b/src/database/models/Job.js
@@ -74,7 +74,7 @@ export const Job = {
label: 'Quantity',
type: 'number',
columnWidth: 125,
- readOnly: true
+ required: true
},
{
name: 'createdAt',
diff --git a/src/database/models/SubJob.js b/src/database/models/SubJob.js
index 634675a..3dff30d 100644
--- a/src/database/models/SubJob.js
+++ b/src/database/models/SubJob.js
@@ -7,7 +7,7 @@ export const SubJob = {
icon: SubJobIcon,
actions: [],
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
- filters: ['state', '_id'],
+ filters: ['state', '_id', 'job._id', 'printer._id'],
sorters: ['createdAt', 'state'],
properties: [
{