Compare commits

..

12 Commits

14 changed files with 364 additions and 203 deletions

View File

@ -48,7 +48,7 @@ To start the development server:
npm run dev
```
The app will be available at [http://localhost:3000](http://localhost:3000).
The app will be available at [http://localhost:5173](http://localhost:5173).
### Building for Production

View File

@ -27,6 +27,7 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@simplewebauthn/browser": "^13.1.2",
"@tanstack/react-query": "^5.90.10",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
"@uiw/react-codemirror": "^4.25.1",
@ -71,10 +72,10 @@
"description": "3D Printer ERP and Control Software.",
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:3000 && cross-env NODE_ENV=development && electron .",
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5173 && cross-env NODE_ENV=development && electron .",
"start": "serve -s build",
"build": "vite build",
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:3000 cross-env NODE_ENV=development electron public/electron.js\"",
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5173 cross-env NODE_ENV=development electron public/electron.js\"",
"build:electron": "vite build && electron-builder"
},
"eslintConfig": {

View File

@ -15,7 +15,7 @@ const NewFilamentStock = ({ onOk, reset }) => {
return (
<NewObjectForm
type={'filamentstock'}
type={'filamentStock'}
reset={reset}
defaultValues={{ state: { type: 'unconsumed' } }}
>

View File

@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'
import { useState, useRef, useEffect, useContext } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Splitter, Divider } from 'antd'
import loglevel from 'loglevel'
@ -26,6 +26,7 @@ import FilamentStockIcon from '../../../Icons/FilamentStockIcon.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import { useMediaQuery } from 'react-responsive'
import AlertsDisplay from '../../common/AlertsDisplay.jsx'
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel)
@ -49,6 +50,7 @@ const ControlPrinter = () => {
}
)
const { connected, sendObjectAction } = useContext(ApiServerContext)
const [sideBarVisible, setSideBarVisible] = useState(
collapseState.temperature ||
collapseState.position ||
@ -88,6 +90,38 @@ const ControlPrinter = () => {
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
},
restartFirmware: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'restartPrinterFirmware'
})
}
return true
},
restartMoonraker: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'restartMoonraker'
})
}
return true
},
restart: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'restartPrinter'
})
}
return true
},
startQueue: () => {
if (connected == true) {
sendObjectAction(printerId, 'printer', {
type: 'startQueue'
})
}
return true
}
}
@ -128,6 +162,7 @@ const ControlPrinter = () => {
id={printerId}
disabled={objectFormState.loading}
visibleActions={{ edit: false }}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
@ -195,33 +230,33 @@ const ControlPrinter = () => {
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={printerId}
type='printer'
ref={objectFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({
loading: printerObjectLoading,
objectData: printerObjectData
}) => {
return (
<Flex vertical>
<Splitter className={'farmcontrol-splitter'}>
<Splitter.Panel>
<Flex vertical gap={'large'}>
<InfoCollapse
title={'Printer'}
icon={<PrinterIcon />}
collapseKey='printer'
active={collapseState.printer}
onToggle={(expanded) =>
updateCollapseState('printer', expanded)
}
>
<Flex vertical>
<Splitter className={'farmcontrol-splitter'}>
<Splitter.Panel>
<Flex vertical gap={'large'}>
<InfoCollapse
title={'Printer'}
icon={<PrinterIcon />}
collapseKey='printer'
active={collapseState.printer}
onToggle={(expanded) =>
updateCollapseState('printer', expanded)
}
>
<ObjectForm
id={printerId}
type='printer'
ref={objectFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({
loading: printerObjectLoading,
objectData: printerObjectData
}) => {
return (
<ObjectInfo
loading={printerObjectLoading}
column={sideBarVisible ? 1 : undefined}
@ -236,155 +271,158 @@ const ControlPrinter = () => {
'moonraker.protocol': false,
'moonraker.host': false,
tags: false,
firmware: false
firmware: false,
alerts: false
}}
objectData={printerObjectData}
type='printer'
/>
</InfoCollapse>
<InfoCollapse
title={'Job'}
icon={<JobIcon />}
collapseKey='job'
active={collapseState.job}
onToggle={(expanded) =>
updateCollapseState('job', expanded)
}
>
{printerObjectData?.currentJob?._id ? (
<ObjectForm
id={printerObjectData.currentJob._id}
type='job'
onStateChange={() => {}}
>
{({
loading: jobObjectLoading,
objectData: jobObjectData
}) => {
return (
<ObjectInfo
loading={jobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={jobObjectData}
type='job'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Sub Job'}
icon={<SubJobIcon />}
collapseKey='subJob'
active={collapseState.subJob}
onToggle={(expanded) =>
updateCollapseState('subJob', expanded)
}
>
{printerObjectData?.currentSubJob?._id ? (
<ObjectForm
id={printerObjectData.currentSubJob._id}
type='subjob'
onStateChange={() => {}}
>
{({
loading: subJobObjectLoading,
objectData: subJobObjectData
}) => {
return (
<ObjectInfo
loading={subJobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={subJobObjectData}
type='subJob'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No sub job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Filament Stock'}
icon={<FilamentStockIcon />}
collapseKey='filamentStock'
active={collapseState.filamentStock}
onToggle={(expanded) =>
updateCollapseState('filamentStock', expanded)
}
>
{printerObjectData?.currentFilamentStock?._id ? (
<ObjectForm
id={printerObjectData.currentFilamentStock._id}
type='filamentStock'
onStateChange={() => {}}
>
{({
loading: filamentStockObjectLoading,
objectData: filamentStockObjectData
}) => {
return (
<ObjectInfo
loading={filamentStockObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
visibleProperties={{
updatedAt: false,
createdAt: false
}}
objectData={filamentStockObjectData}
type='filamentStock'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No filament stock.'}
hasBackground={false}
/>
)}
</InfoCollapse>
</Flex>
</Splitter.Panel>
{sideBarVisible && !isMobile ? (
<Splitter.Panel
style={{ minWidth: '325px' }}
defaultSize='20%'
max='35%'
)
}}
</ObjectForm>
</InfoCollapse>
<InfoCollapse
title={'Job'}
icon={<JobIcon />}
collapseKey='job'
active={collapseState.job}
onToggle={(expanded) =>
updateCollapseState('job', expanded)
}
>
{objectFormState.objectData?.currentJob?._id ? (
<ObjectForm
id={objectFormState.objectData.currentJob._id}
type='job'
onStateChange={() => {}}
>
{sideBarItems}
</Splitter.Panel>
) : null}
</Splitter>
{isMobile ? (
<>
<Divider />
{sideBarItems}
</>
) : null}
{({
loading: jobObjectLoading,
objectData: jobObjectData
}) => {
return (
<ObjectInfo
loading={jobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={jobObjectData}
type='job'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Sub Job'}
icon={<SubJobIcon />}
collapseKey='subJob'
active={collapseState.subJob}
onToggle={(expanded) =>
updateCollapseState('subJob', expanded)
}
>
{objectFormState.objectData?.currentSubJob?._id ? (
<ObjectForm
id={objectFormState.objectData.currentSubJob._id}
type='subjob'
onStateChange={() => {}}
>
{({
loading: subJobObjectLoading,
objectData: subJobObjectData
}) => {
return (
<ObjectInfo
loading={subJobObjectLoading}
column={sideBarVisible ? 1 : undefined}
visibleProperties={{
printers: false,
createdAt: false
}}
objectData={subJobObjectData}
type='subJob'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No sub job.'}
hasBackground={false}
/>
)}
</InfoCollapse>
<InfoCollapse
title={'Filament Stock'}
icon={<FilamentStockIcon />}
collapseKey='filamentStock'
active={collapseState.filamentStock}
onToggle={(expanded) =>
updateCollapseState('filamentStock', expanded)
}
>
{objectFormState.objectData?.currentFilamentStock?._id ? (
<ObjectForm
id={
objectFormState.objectData.currentFilamentStock._id
}
type='filamentStock'
onStateChange={() => {}}
>
{({
loading: filamentStockObjectLoading,
objectData: filamentStockObjectData
}) => {
return (
<ObjectInfo
loading={filamentStockObjectLoading}
column={sideBarVisible ? 1 : undefined}
showHyperlink={true}
visibleProperties={{
updatedAt: false,
createdAt: false
}}
objectData={filamentStockObjectData}
type='filamentStock'
/>
)
}}
</ObjectForm>
) : (
<MissingPlaceholder
message={'No filament stock.'}
hasBackground={false}
/>
)}
</InfoCollapse>
</Flex>
)
}}
</ObjectForm>
</Splitter.Panel>
{sideBarVisible && !isMobile ? (
<Splitter.Panel
style={{ minWidth: '325px' }}
defaultSize='20%'
max='35%'
>
{sideBarItems}
</Splitter.Panel>
) : null}
</Splitter>
{isMobile ? (
<>
<Divider />
{sideBarItems}
</>
) : null}
</Flex>
</ActionHandler>
<InfoCollapse
title='Notes'

View File

@ -61,9 +61,10 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
// Calculate computed values for initial data
const computedValues = calculateComputedValues(defaultValues, model)
const initialFormData = { ...defaultValues, ...computedValues }
form.setFieldsValue(initialFormData)
setObjectData(initialFormData)
setObjectData((prev) => {
return merge({}, prev, initialFormData)
})
}
}, [form, defaultValues, calculateComputedValues, model])
@ -113,7 +114,6 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
// Merge all values (user input + computed values)
const allValues = { ...values, ...computedValues }
setObjectData((prev) => {
return merge({}, prev, allValues)
})

View File

@ -63,7 +63,8 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
item.children = mapActionsToMenuItems(
action.children,
currentUrlWithActions,
id
id,
objectData
)
}
return item

View File

@ -127,7 +127,7 @@ const ObjectProperty = ({
formItemName = name ? name.split('.') : undefined
}
var textParams = { style: { whiteSpace: 'nowrap' } }
var textParams = { style: { whiteSpace: 'nowrap', minWidth: '0' } }
if (disabled == true) {
textParams = { ...textParams, delete: true, type: 'secondary' }

View File

@ -10,19 +10,30 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
return (
<>
<Flex>
<Flex style={{ minWidth: 0 }}>
{showLink ? (
<Link
href={url}
target='_blank'
rel='noopener noreferrer'
style={{ marginRight: 8 }}
style={{
marginRight: 8,
minWidth: 0,
flex: 1,
overflow: 'hidden',
width: 0
}}
>
<Text ellipsis>{url}</Text>
<Text ellipsis style={{ display: 'block' }}>
{url}
</Text>
</Link>
) : (
<>
<Text style={{ marginRight: 8 }} ellipsis>
<Text
style={{ marginRight: 8, minWidth: 0, flex: 1, width: 0 }}
ellipsis
>
{url}
</Text>
<Tooltip title='Open URL' arrow={false}>
@ -30,7 +41,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
icon={<LinkIcon />}
type='text'
size='small'
style={{ minWidth: 25 }}
style={{ minWidth: 25, flexShrink: 0 }}
onClick={(e) => {
e.preventDefault()
window.open(url, '_blank', 'noopener,noreferrer')

View File

@ -83,6 +83,11 @@ const AuthProvider = ({ children }) => {
// Read token from cookies if present
useEffect(() => {
try {
console.log(
'Retreiving token from cookies...',
getAuthCookies(),
validateAuthCookies()
)
// First validate existing cookies to clean up expired ones
if (validateAuthCookies()) {
const {
@ -210,7 +215,11 @@ const AuthProvider = ({ children }) => {
setUserProfile(authData)
// Store in cookies for persistence between tabs
const cookieSuccess = setAuthCookies(authData)
const cookieSuccess = setAuthCookies({
user: authData,
access_token: authData.access_token,
expires_at: authData.expires_at
})
if (!cookieSuccess) {
messageApi.warning(
'Authentication successful but failed to save login state. You may need to log in again if you close this tab.'

View File

@ -1,8 +1,8 @@
const config = {
development: {
backendUrl: 'http://192.168.68.53:8080',
backendUrl: 'https://dev.tombutcher.work/api',
printServerUrl: 'ws://192.168.68.53:8081',
apiServerUrl: 'ws://192.168.68.53:9090',
apiServerUrl: 'https://dev-wss.tombutcher.work',
logLevel: 'trace'
},
production: {

View File

@ -84,7 +84,7 @@ export const FilamentStock = {
required: true,
columnWidth: 300,
value: (objectData) => {
if (!objectData.currentWeight) {
if (objectData?.state?.type === 'unconsumed') {
return objectData?.startingWeight
} else {
return objectData.currentWeight

View File

@ -3,6 +3,8 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
import StopCircleIcon from '../../components/Icons/StopCircleIcon'
export const Printer = {
name: 'printer',
@ -40,6 +42,97 @@ export const Printer = {
icon: EditIcon,
url: (_id) =>
`/dashboard/production/printers/info?printerId=${_id}&action=edit`
},
{ type: 'divider' },
{
name: 'restartSubmenu',
label: 'Restart',
icon: ReloadIcon,
disabled: (objectData) => {
return objectData?.online == false
},
children: [
{
name: 'restart',
label: 'Restart',
icon: ReloadIcon,
disabled: (objectData) => {
return objectData?.online == false
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=restart`
},
{
name: 'restartFirmware',
label: 'Restart Firmware',
icon: ReloadIcon,
disabled: (objectData) => {
return objectData?.online == false
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=restartFirmware`
},
{ type: 'divider' },
{
name: 'restartMoonraker',
label: 'Restart Moonraker',
icon: ReloadIcon,
disabled: (objectData) => {
return objectData?.online == false
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=restartMoonraker`
}
]
},
{
name: 'queue',
label: 'Queue',
icon: PlayCircleIcon,
disabled: (objectData) => {
return objectData?.online == false
},
children: [
{
name: 'Start',
label: 'Start',
icon: PlayCircleIcon,
disabled: (objectData) => {
console.log(objectData?.subJobs?.length)
return (
objectData?.state?.type == 'error' ||
objectData?.state?.type == 'printing' ||
objectData?.subJobs?.length == 0 ||
objectData?.subJobs?.length == undefined
)
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=startQueue`
},
{
name: 'pause',
label: 'Pause',
icon: PauseCircleIcon,
disabled: (objectData) => {
return objectData?.state?.type != 'printing'
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=pauseQueue`
},
{
name: 'Stop',
label: 'Stop',
icon: StopCircleIcon,
disabled: (objectData) => {
return (
objectData?.state?.type != 'printing' ||
objectData?.state?.type != 'error'
)
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=stopQueue`
}
]
}
],
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],

View File

@ -11,8 +11,9 @@ export default defineConfig({
outDir: 'build'
},
server: {
allowedHosts: ['dev.tombutcher.work'],
host: '0.0.0.0',
open: false,
port: 3000
port: 5173,
open: false
}
})

View File

@ -1289,10 +1289,10 @@
resolved "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz"
integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==
"@esbuild/darwin-arm64@0.25.9":
"@esbuild/darwin-x64@0.25.9":
version "0.25.9"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz"
integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz"
integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
"@eslint-community/eslint-utils@^4.2.0":
version "4.7.0"
@ -1812,10 +1812,10 @@
estree-walker "^2.0.2"
picomatch "^4.0.2"
"@rollup/rollup-darwin-arm64@4.48.0":
"@rollup/rollup-darwin-x64@4.48.0":
version "4.48.0"
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.0.tgz"
integrity sha512-QhR2KA18fPlJWFefySJPDYZELaVqIUVnYgAOdtJ+B/uH96CFg2l1TQpX19XpUMWUqMyIiyY45wje8K6F4w4/CA==
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.0.tgz"
integrity sha512-Q9RMXnQVJ5S1SYpNSTwXDpoQLgJ/fbInWOyjbCnnqTElEyeNvLAB3QvG5xmMQMhFN74bB5ZZJYkKaFPcOG8sGg==
"@rtsao/scc@^1.1.0":
version "1.1.0"
@ -1940,6 +1940,18 @@
dependencies:
defer-to-connect "^2.0.0"
"@tanstack/query-core@5.90.10":
version "5.90.10"
resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz"
integrity sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==
"@tanstack/react-query@^5.90.10":
version "5.90.10"
resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz"
integrity sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==
dependencies:
"@tanstack/query-core" "5.90.10"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
@ -8768,7 +8780,7 @@ react-router@7.8.2:
cookie "^1.0.1"
set-cookie-parser "^2.6.0"
react@*, "react@^18.0.0 || ^19.1.0", react@^19.1.1, "react@>= 16.8.0", "react@>= 16.x", react@>=16, react@>=16.0.0, react@>=16.11.0, react@>=16.8, react@>=16.8.0, react@>=16.8.4, react@>=16.9.0, react@>=17.0.0, react@>=18:
react@*, "react@^18 || ^19", "react@^18.0.0 || ^19.1.0", react@^19.1.1, "react@>= 16.8.0", "react@>= 16.x", react@>=16, react@>=16.0.0, react@>=16.11.0, react@>=16.8, react@>=16.8.0, react@>=16.8.4, react@>=16.9.0, react@>=17.0.0, react@>=18:
version "19.1.1"
resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz"
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
@ -10592,11 +10604,6 @@ yaml@^1.10.0:
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.4.2:
version "2.8.1"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"