Compare commits
7 Commits
cd83679232
...
66e137fac2
| Author | SHA1 | Date | |
|---|---|---|---|
| 66e137fac2 | |||
| 587ef7f480 | |||
| ecca21fd6e | |||
| fdd16b2301 | |||
| 3a1eb27b50 | |||
| da0ea83e57 | |||
| 8dbd87ff4c |
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/app_dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
4128
package-lock.json
generated
48
package.json
@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "farmcontrol-ui",
|
"name": "farmcontrol-ui",
|
||||||
|
"author": {
|
||||||
|
"name": "Tom Butcher",
|
||||||
|
"email": "tom@tombutcher.work"
|
||||||
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"homepage": "./",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/charts": "^2.3.0",
|
"@ant-design/charts": "^2.3.0",
|
||||||
"@ant-design/pro-components": "^2.8.7",
|
"@ant-design/pro-components": "^2.8.7",
|
||||||
@ -49,13 +54,20 @@
|
|||||||
"webpack": "^5.99.9",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"main": "build/electron.js",
|
||||||
|
"description": "3D Printer ERP and Control Software.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "react-scripts start",
|
"dev": "react-scripts start",
|
||||||
|
"electron": "ELECTRON_START_URL=http://192.168.68.53:3000 electron .",
|
||||||
"start": "serve -s build",
|
"start": "serve -s build",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"minify-svgs": "node scripts/minify-svgs.js"
|
"minify-svgs": "node scripts/minify-svgs.js",
|
||||||
|
|
||||||
|
"dev:electron": "concurrently \"react-scripts start\" \"ELECTRON_START_URL=http://192.168.68.53:3000 electron src/electron/main.js\"",
|
||||||
|
"build:electron": "npm run build && electron-builder"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -77,6 +89,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"electron": "^29.0.0",
|
||||||
|
"electron-builder": "^26.0.12",
|
||||||
|
"electron-packager": "^17.1.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^5.4.1",
|
"eslint-plugin-prettier": "^5.4.1",
|
||||||
@ -90,5 +106,35 @@
|
|||||||
"svgo-loader": "^4.0.0",
|
"svgo-loader": "^4.0.0",
|
||||||
"webpack": "^5.99.9",
|
"webpack": "^5.99.9",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.tombutcher.farmcontrol",
|
||||||
|
"icon": "src/assets/logos/farmcontrolicon.png",
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "app_dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build/**/*",
|
||||||
|
"node_modules/**/*"
|
||||||
|
],
|
||||||
|
"mac": {
|
||||||
|
"target": "dmg",
|
||||||
|
"extendInfo": {
|
||||||
|
"CFBundleURLTypes": [
|
||||||
|
{
|
||||||
|
"CFBundleURLName": "com.tombutcher.farmcontrol",
|
||||||
|
"CFBundleURLSchemes": ["farmcontrol"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": "AppImage"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
public/electron.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
const { app, BrowserWindow, ipcMain, shell } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
let win
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
frame: false,
|
||||||
|
backgroundColor: '#141414',
|
||||||
|
icon: path.join(__dirname, './logo512.png'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// For development, load from localhost; for production, load the built index.html
|
||||||
|
if (process.env.ELECTRON_START_URL) {
|
||||||
|
win.loadURL(process.env.ELECTRON_START_URL)
|
||||||
|
} else {
|
||||||
|
win.loadFile(path.join(__dirname, '../build/index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWindowEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(createWindow)
|
||||||
|
|
||||||
|
// IPC handler to get OS
|
||||||
|
ipcMain.handle('os-info', () => {
|
||||||
|
return {
|
||||||
|
platform: process.platform
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// IPC handler to get window state
|
||||||
|
ipcMain.handle('window-state', () => {
|
||||||
|
return {
|
||||||
|
isMaximized: win ? win.isMaximized() : false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emit events to renderer when window is maximized/unmaximized
|
||||||
|
function setupWindowEvents() {
|
||||||
|
if (!win) return
|
||||||
|
win.on('maximize', () => {
|
||||||
|
win.webContents.send('window-state', {
|
||||||
|
isMaximized: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
win.on('unmaximize', () => {
|
||||||
|
win.webContents.send('window-state', {
|
||||||
|
isMaximized: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC handlers for window controls
|
||||||
|
ipcMain.on('window-control', (event, action) => {
|
||||||
|
if (!win) return
|
||||||
|
switch (action) {
|
||||||
|
case 'minimize':
|
||||||
|
win.minimize()
|
||||||
|
break
|
||||||
|
case 'maximize':
|
||||||
|
if (win.isMaximized()) {
|
||||||
|
win.unmaximize()
|
||||||
|
} else {
|
||||||
|
win.maximize()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'close':
|
||||||
|
win.close()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add this after other ipcMain handlers
|
||||||
|
ipcMain.handle('open-external-url', (event, url) => {
|
||||||
|
console.log('Opening external url...')
|
||||||
|
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('open-url', (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
console.log('App opened with URL:', url)
|
||||||
|
if (url.startsWith('farmcontrol://app')) {
|
||||||
|
// Extract the path/query after 'farmcontrol://app'
|
||||||
|
const redirectPath = url.replace('farmcontrol://app', '') || '/'
|
||||||
|
if (win && win.webContents) {
|
||||||
|
win.webContents.send('navigate', redirectPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 4.0 MiB |
24
src/App.css
@ -48,9 +48,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flag {
|
.flag {
|
||||||
line-height: 1;
|
line-height: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.electron-navigation-wrapper {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electron-navigation-wrapper li, .electron-navigation-wrapper button, .electron-navigation-wrapper .ant-tag{
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electron-navigation {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.electron-sidebar .ant-menu-item {
|
||||||
|
height: 32.5px;
|
||||||
|
line-height: 32.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--unit-100vh: 100vh;
|
--unit-100vh: 100vh;
|
||||||
}
|
}
|
||||||
@ -60,6 +78,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-cards-header .ant-table-tbody {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-menu-overflow-item-rest::after {
|
.ant-menu-overflow-item-rest::after {
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/App.jsx
@ -66,6 +66,9 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
|
|||||||
import Users from './components/Dashboard/Management/Users.jsx'
|
import Users from './components/Dashboard/Management/Users.jsx'
|
||||||
import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx'
|
import UserInfo from './components/Dashboard/Management/Users/UserInfo.jsx'
|
||||||
import SubJobs from './components/Dashboard/Production/SubJobs.jsx'
|
import SubJobs from './components/Dashboard/Production/SubJobs.jsx'
|
||||||
|
import Hosts from './components/Dashboard/Management/Hosts.jsx'
|
||||||
|
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.js'
|
||||||
|
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||||
|
|
||||||
const AppContent = () => {
|
const AppContent = () => {
|
||||||
const { themeConfig } = useThemeContext()
|
const { themeConfig } = useThemeContext()
|
||||||
@ -73,8 +76,9 @@ const AppContent = () => {
|
|||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<App>
|
<App>
|
||||||
<AuthProvider>
|
|
||||||
<Router>
|
<Router>
|
||||||
|
<ElectronProvider>
|
||||||
|
<AuthProvider>
|
||||||
<PrintServerProvider>
|
<PrintServerProvider>
|
||||||
<ApiServerProvider>
|
<ApiServerProvider>
|
||||||
<SpotlightProvider>
|
<SpotlightProvider>
|
||||||
@ -92,9 +96,12 @@ const AppContent = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route path='/auth/callback' element={<AuthCallback />} />
|
||||||
<Route
|
<Route
|
||||||
path='/dashboard'
|
path='/dashboard'
|
||||||
element={<PrivateRoute component={() => <Dashboard />} />}
|
element={
|
||||||
|
<PrivateRoute component={() => <Dashboard />} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Production Routes */}
|
{/* Production Routes */}
|
||||||
<Route
|
<Route
|
||||||
@ -114,7 +121,10 @@ const AppContent = () => {
|
|||||||
element={<PrinterInfo />}
|
element={<PrinterInfo />}
|
||||||
/>
|
/>
|
||||||
<Route path='production/jobs' element={<Jobs />} />
|
<Route path='production/jobs' element={<Jobs />} />
|
||||||
<Route path='production/subjobs' element={<SubJobs />} />
|
<Route
|
||||||
|
path='production/subjobs'
|
||||||
|
element={<SubJobs />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='production/jobs/info'
|
path='production/jobs/info'
|
||||||
element={<JobInfo />}
|
element={<JobInfo />}
|
||||||
@ -176,7 +186,11 @@ const AppContent = () => {
|
|||||||
path='management/products/info'
|
path='management/products/info'
|
||||||
element={<ProductInfo />}
|
element={<ProductInfo />}
|
||||||
/>
|
/>
|
||||||
<Route path='management/vendors' element={<Vendors />} />
|
<Route
|
||||||
|
path='management/vendors'
|
||||||
|
element={<Vendors />}
|
||||||
|
/>
|
||||||
|
<Route path='management/hosts' element={<Hosts />} />
|
||||||
<Route
|
<Route
|
||||||
path='management/users/info'
|
path='management/users/info'
|
||||||
element={<UserInfo />}
|
element={<UserInfo />}
|
||||||
@ -232,8 +246,9 @@ const AppContent = () => {
|
|||||||
</SpotlightProvider>
|
</SpotlightProvider>
|
||||||
</ApiServerProvider>
|
</ApiServerProvider>
|
||||||
</PrintServerProvider>
|
</PrintServerProvider>
|
||||||
</Router>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ElectronProvider>
|
||||||
|
</Router>
|
||||||
</App>
|
</App>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
src/assets/icons/contracticon.afdesign
Normal file
1
src/assets/icons/contracticon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M5.49 28.706h17.316c3.564 0 5.633-2.094 5.633-5.658V5.954c0-1.928-1.461-3.39-3.41-3.39-1.954 0-3.383 1.436-3.383 3.39v2.243l.673 9.826-7.322-7.694-8.992-9.08A3.3 3.3 0 0 0 3.611.253C1.509.253 0 1.694 0 3.776c0 .936.38 1.813 1.032 2.471l9.035 9.023 7.694 7.291-9.86-.648H5.49c-1.954 0-3.416 1.404-3.416 3.384 0 1.96 1.436 3.409 3.416 3.409m30.784 31.607c1.955 0 3.384-1.41 3.384-3.39v-2.519l-.673-9.807 7.322 7.701 9.171 9.228c.633.663 1.466.995 2.394.995 2.076 0 3.586-1.44 3.586-3.517 0-.941-.355-1.818-1.013-2.471l-9.208-9.208-7.726-7.291 9.892.654h2.693c1.954 0 3.415-1.404 3.415-3.359 0-1.985-1.435-3.409-3.415-3.409H38.498c-3.57 0-5.633 2.063-5.633 5.628v17.375c0 1.954 1.429 3.39 3.409 3.39" style="fill-rule:nonzero" transform="matrix(.94087 0 0 .92768 3.088 2.883)"/></svg>
|
||||||
|
After Width: | Height: | Size: 957 B |
7
src/assets/icons/contracticon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.940871,0,0,0.927684,3.08821,2.88259)">
|
||||||
|
<path d="M5.49,28.706L22.806,28.706C26.37,28.706 28.439,26.612 28.439,23.048L28.439,5.954C28.439,4.026 26.978,2.564 25.029,2.564C23.075,2.564 21.646,4 21.646,5.954L21.646,8.197L22.319,18.023L14.997,10.329L6.005,1.249C5.367,0.591 4.507,0.253 3.611,0.253C1.509,0.253 0,1.694 0,3.776C0,4.712 0.38,5.589 1.032,6.247L10.067,15.27L17.761,22.561L7.901,21.913L5.49,21.913C3.536,21.913 2.074,23.317 2.074,25.297C2.074,27.257 3.51,28.706 5.49,28.706ZM36.274,60.313C38.229,60.313 39.658,58.903 39.658,56.923L39.658,54.404L38.985,44.597L46.307,52.298L55.478,61.526C56.111,62.189 56.944,62.521 57.872,62.521C59.948,62.521 61.458,61.081 61.458,59.004C61.458,58.063 61.103,57.186 60.445,56.533L51.237,47.325L43.511,40.034L53.403,40.688L56.096,40.688C58.05,40.688 59.511,39.284 59.511,37.329C59.511,35.344 58.076,33.92 56.096,33.92L38.498,33.92C34.928,33.92 32.865,35.983 32.865,39.548L32.865,56.923C32.865,58.877 34.294,60.313 36.274,60.313Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/icons/expandicon.afdesign
Normal file
1
src/assets/icons/expandicon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M3.409 26.139c1.955 0 3.384-1.436 3.384-3.39v-2.237l-.647-9.832 7.296 7.694 9.017 9.08c.633.664 1.467.97 2.369.97 2.101 0 3.611-1.415 3.611-3.497a3.42 3.42 0 0 0-1.013-2.471l-9.054-9.023-7.7-7.291 9.891.648h2.386c1.954 0 3.415-1.404 3.415-3.378 0-1.986-1.435-3.41-3.415-3.41H5.633C2.063.002 0 2.066 0 5.63v17.119c0 1.928 1.455 3.39 3.409 3.39m33.775 34.074h17.322c3.564 0 5.653-2.064 5.653-5.634V37.461c0-1.923-1.455-3.385-3.435-3.385-1.929 0-3.384 1.436-3.384 3.385v2.242l.673 9.826-7.322-7.694-8.992-9.074c-.632-.664-1.491-.97-2.393-.97-2.077 0-3.612 1.415-3.612 3.491 0 .962.38 1.819 1.039 2.471l9.028 9.023 7.726 7.291-9.891-.648h-2.412c-1.948 0-3.415 1.404-3.415 3.384 0 1.986 1.467 3.41 3.415 3.41" style="fill-rule:nonzero" transform="matrix(.96112 0 0 .96325 3 3)"/></svg>
|
||||||
|
After Width: | Height: | Size: 956 B |
7
src/assets/icons/expandicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.961121,0,0,0.963255,3,3)">
|
||||||
|
<path d="M3.409,26.139C5.364,26.139 6.793,24.703 6.793,22.749L6.793,20.512L6.146,10.68L13.442,18.374L22.459,27.454C23.092,28.118 23.926,28.424 24.828,28.424C26.929,28.424 28.439,27.009 28.439,24.927C28.439,23.966 28.084,23.114 27.426,22.456L18.372,13.433L10.672,6.142L20.563,6.79L22.949,6.79C24.903,6.79 26.364,5.386 26.364,3.412C26.364,1.426 24.929,0.002 22.949,0.002L5.633,0.002C2.063,0.002 0,2.066 0,5.63L0,22.749C0,24.677 1.455,26.139 3.409,26.139ZM37.184,60.213L54.506,60.213C58.07,60.213 60.159,58.149 60.159,54.579L60.159,37.461C60.159,35.538 58.704,34.076 56.724,34.076C54.795,34.076 53.34,35.512 53.34,37.461L53.34,39.703L54.013,49.529L46.691,41.835L37.699,32.761C37.067,32.097 36.208,31.791 35.306,31.791C33.229,31.791 31.694,33.206 31.694,35.282C31.694,36.244 32.074,37.101 32.733,37.753L41.761,46.776L49.487,54.067L39.596,53.419L37.184,53.419C35.236,53.419 33.769,54.823 33.769,56.803C33.769,58.789 35.236,60.213 37.184,60.213Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/icons/hosticon.afdesign
Normal file
1
src/assets/icons/hosticon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M10.251 46.567h54.035c6.66 0 10.251-3.539 10.251-10.148V25.612c0-4.444-1.135-7.628-3.757-10.275L60.737 5.006C57.346 1.558 54.946.02 50.458.02H24.084c-4.462 0-6.887 1.538-10.258 4.986L3.757 15.337C1.121 18.035 0 21.089 0 25.612v10.807c0 6.609 3.591 10.148 10.251 10.148m.277-5.993c-2.867 0-4.484-1.566-4.484-4.535v-6.391c0-1.904.897-2.827 2.775-2.827h56.899c1.878 0 2.775.923 2.775 2.827v6.391c0 2.969-1.617 4.535-4.484 4.535zM8.292 21.549c-1.071 0-1.373-.983-.733-1.628L19.028 7.799c1.751-1.835 3.025-2.508 5.391-2.508h25.705c2.36 0 3.634.673 5.391 2.508l11.489 12.122c.614.645.338 1.628-.759 1.628zm4.555 15.102c1.656 0 3.051-1.375 3.051-3.052 0-1.656-1.395-3.025-3.051-3.025-1.676 0-3.046 1.369-3.046 3.025a3.06 3.06 0 0 0 3.046 3.052m8.362.971c1.037 0 1.925-.812 1.925-1.828V31.61c0-1.031-.888-1.843-1.925-1.843a1.823 1.823 0 0 0-1.828 1.843v4.184a1.82 1.82 0 0 0 1.828 1.828m6.062 0c1.037 0 1.925-.812 1.925-1.828V31.61c0-1.031-.888-1.843-1.925-1.843-1.01 0-1.822.812-1.822 1.843v4.184c0 1.016.812 1.828 1.822 1.828m6.535-2.105a1.9 1.9 0 0 0 1.892-1.918 1.89 1.89 0 0 0-1.892-1.891 1.89 1.89 0 0 0-1.897 1.891c0 1.068.849 1.918 1.897 1.918m6.555 2.105a1.817 1.817 0 0 0 1.817-1.828V31.61a1.82 1.82 0 0 0-1.817-1.843c-1.042 0-1.93.812-1.93 1.843v4.184c0 1.016.888 1.828 1.93 1.828m6.062 0a1.803 1.803 0 0 0 1.823-1.828V31.61c0-1.031-.787-1.843-1.823-1.843s-1.905.812-1.905 1.843v4.184c0 1.016.869 1.828 1.905 1.828m7.228-2.041h9.447c1.048 0 1.848-.877 1.848-1.925 0-1.027-.8-1.822-1.848-1.822h-9.447a1.8 1.8 0 0 0-1.828 1.822c0 1.048.801 1.925 1.828 1.925" style="fill-rule:nonzero" transform="translate(2 13.25)scale(.80497)"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
7
src/assets/icons/hosticon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.804972,0,0,0.804972,2,13.2492)">
|
||||||
|
<path d="M10.251,46.567L64.286,46.567C70.946,46.567 74.537,43.028 74.537,36.419L74.537,25.612C74.537,21.168 73.402,17.984 70.78,15.337L60.737,5.006C57.346,1.558 54.946,0.02 50.458,0.02L24.084,0.02C19.622,0.02 17.197,1.558 13.826,5.006L3.757,15.337C1.121,18.035 0,21.089 0,25.612L0,36.419C0,43.028 3.591,46.567 10.251,46.567ZM10.528,40.574C7.661,40.574 6.044,39.008 6.044,36.039L6.044,29.648C6.044,27.744 6.941,26.821 8.819,26.821L65.718,26.821C67.596,26.821 68.493,27.744 68.493,29.648L68.493,36.039C68.493,39.008 66.876,40.574 64.009,40.574L10.528,40.574ZM8.292,21.549C7.221,21.549 6.919,20.566 7.559,19.921L19.028,7.799C20.779,5.964 22.053,5.291 24.419,5.291L50.124,5.291C52.484,5.291 53.758,5.964 55.515,7.799L67.004,19.921C67.618,20.566 67.342,21.549 66.245,21.549L8.292,21.549ZM12.847,36.651C14.503,36.651 15.898,35.276 15.898,33.599C15.898,31.943 14.503,30.574 12.847,30.574C11.171,30.574 9.801,31.943 9.801,33.599C9.801,35.276 11.171,36.651 12.847,36.651ZM21.209,37.622C22.246,37.622 23.134,36.81 23.134,35.794L23.134,31.61C23.134,30.579 22.246,29.767 21.209,29.767C20.193,29.767 19.381,30.579 19.381,31.61L19.381,35.794C19.381,36.81 20.193,37.622 21.209,37.622ZM27.271,37.622C28.308,37.622 29.196,36.81 29.196,35.794L29.196,31.61C29.196,30.579 28.308,29.767 27.271,29.767C26.261,29.767 25.449,30.579 25.449,31.61L25.449,35.794C25.449,36.81 26.261,37.622 27.271,37.622ZM33.806,35.517C34.849,35.517 35.698,34.667 35.698,33.599C35.698,32.551 34.849,31.708 33.806,31.708C32.758,31.708 31.909,32.551 31.909,33.599C31.909,34.667 32.758,35.517 33.806,35.517ZM40.361,37.622C41.366,37.622 42.178,36.81 42.178,35.794L42.178,31.61C42.178,30.579 41.366,29.767 40.361,29.767C39.319,29.767 38.431,30.579 38.431,31.61L38.431,35.794C38.431,36.81 39.319,37.622 40.361,37.622ZM46.423,37.622C47.459,37.622 48.246,36.81 48.246,35.794L48.246,31.61C48.246,30.579 47.459,29.767 46.423,29.767C45.387,29.767 44.518,30.579 44.518,31.61L44.518,35.794C44.518,36.81 45.387,37.622 46.423,37.622ZM53.651,35.581L63.098,35.581C64.146,35.581 64.946,34.704 64.946,33.656C64.946,32.629 64.146,31.834 63.098,31.834L53.651,31.834C52.624,31.834 51.823,32.629 51.823,33.656C51.823,34.704 52.624,35.581 53.651,35.581Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/icons/minusicon.afdesign
Normal file
1
src/assets/icons/minusicon.min.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="0 0 64 64"><path d="M7.327 43.04c1.358 1.327 3.648 1.31 4.941.017l30.717-30.73c1.316-1.315 1.336-3.578-.023-4.91-1.358-1.359-3.62-1.364-4.936-.023L7.31 38.129c-1.293 1.288-1.336 3.578.017 4.911" style="fill-rule:nonzero" transform="rotate(45 26.952 34.187)scale(1.15242)"/></svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
7
src/assets/icons/minusicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.814885,0.814885,-0.814885,0.814885,32.0681,-9.04462)">
|
||||||
|
<path d="M7.327,43.04C8.685,44.367 10.975,44.35 12.268,43.057L42.985,12.327C44.301,11.012 44.321,8.749 42.962,7.417C41.604,6.058 39.342,6.053 38.026,7.394L7.31,38.129C6.017,39.417 5.974,41.707 7.327,43.04Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 785 B |
@ -30,7 +30,7 @@ const AppError = ({
|
|||||||
style={{ height: '100vh' }}
|
style={{ height: '100vh' }}
|
||||||
gap={'large'}
|
gap={'large'}
|
||||||
>
|
>
|
||||||
<Card>
|
<Card style={{ borderRadius: 20 }}>
|
||||||
<Flex vertical align='center'>
|
<Flex vertical align='center'>
|
||||||
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const AppLoading = () => {
|
|||||||
style={{ height: '100vh' }}
|
style={{ height: '100vh' }}
|
||||||
gap={'large'}
|
gap={'large'}
|
||||||
>
|
>
|
||||||
<Card>
|
<Card style={{ borderRadius: 20 }}>
|
||||||
<Flex vertical align='center'>
|
<Flex vertical align='center'>
|
||||||
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
75
src/components/App/AuthCallback.jsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'black'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'black',
|
||||||
|
minHeight: '100vh',
|
||||||
|
transition: 'opacity 0.5s ease-in-out',
|
||||||
|
opacity: isVisible ? 1 : 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AuthParticles />
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
justify='center'
|
||||||
|
vertical
|
||||||
|
style={{ height: '100vh' }}
|
||||||
|
gap={'large'}
|
||||||
|
>
|
||||||
|
<Card style={{ borderRadius: 20 }}>
|
||||||
|
<Flex vertical align='center'>
|
||||||
|
<FarmControlLogo style={{ fontSize: '500px', height: '40px' }} />
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
<Alert
|
||||||
|
message='Loading Farm Control please wait...'
|
||||||
|
icon={<LoadingOutlined />}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthCallback
|
||||||
@ -11,7 +11,7 @@ import EditButtons from '../../common/EditButtons'
|
|||||||
import ActionHandler from '../../common/ActionHandler'
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import InfoCollapse from '../../common/InfoCollapse'
|
import InfoCollapse from '../../common/InfoCollapse'
|
||||||
import NotesPanel from '../../common/NotesPanel'
|
import NotesPanel from '../../common/NotesPanel'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||||
import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
|
import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon'
|
import NoteIcon from '../../../Icons/NoteIcon'
|
||||||
|
|||||||
@ -90,7 +90,6 @@ const LoadFilamentStock = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug(statusUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printServer.emit('printer.objects.subscribe', params)
|
printServer.emit('printer.objects.subscribe', params)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
|||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from './LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler'
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
|||||||
105
src/components/Dashboard/Management/Hosts.jsx
Normal file
@ -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: <PlusIcon />
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
label: 'Reload List',
|
||||||
|
key: 'reloadList',
|
||||||
|
icon: <ReloadIcon />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onClick: ({ key }) => {
|
||||||
|
if (key === 'reloadList') {
|
||||||
|
tableRef.current?.reload()
|
||||||
|
} else if (key === 'newHost') {
|
||||||
|
setNewHostOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex vertical={'true'} gap='large'>
|
||||||
|
{contextHolder}
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space>
|
||||||
|
<Dropdown menu={actionItems}>
|
||||||
|
<Button>Actions</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<ColumnViewButton
|
||||||
|
type='host'
|
||||||
|
loading={false}
|
||||||
|
visibleState={columnVisibility}
|
||||||
|
updateVisibleState={setColumnVisibility}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<ObjectTable
|
||||||
|
ref={tableRef}
|
||||||
|
type='host'
|
||||||
|
cards={viewMode === 'cards'}
|
||||||
|
visibleColumns={columnVisibility}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={newHostOpen}
|
||||||
|
footer={null}
|
||||||
|
width={700}
|
||||||
|
onCancel={() => {
|
||||||
|
setNewHostOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NewHost
|
||||||
|
onOk={() => {
|
||||||
|
setNewHostOpen(false)
|
||||||
|
messageApi.success('New host added successfully.')
|
||||||
|
tableRef.current?.reload()
|
||||||
|
}}
|
||||||
|
reset={newHostOpen}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hosts
|
||||||
186
src/components/Dashboard/Management/Hosts/HostInfo.jsx
Normal file
@ -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 (
|
||||||
|
<EditObjectForm id={hostId} type='host' style={{ height: '100%' }}>
|
||||||
|
{({
|
||||||
|
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 (
|
||||||
|
<ActionHandler actions={actions} loading={loading}>
|
||||||
|
{({ callAction }) => (
|
||||||
|
<Flex
|
||||||
|
gap='large'
|
||||||
|
vertical='true'
|
||||||
|
style={{
|
||||||
|
height: 'calc(var(--unit-100vh) - 155px)',
|
||||||
|
minHeight: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='host'
|
||||||
|
id={hostId}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Host Information' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
<LockIndicator lock={lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={editLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
disabled={lock?.locked || loading}
|
||||||
|
loading={editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Host Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='info'
|
||||||
|
>
|
||||||
|
<ObjectInfo
|
||||||
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='host'
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('notes', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='notes'
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<NotesPanel _id={hostId} type='host' />
|
||||||
|
</Card>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Audit Logs'
|
||||||
|
icon={<AuditLogIcon />}
|
||||||
|
active={collapseState.auditLogs}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('auditLogs', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='auditLogs'
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
|
) : (
|
||||||
|
<ObjectTable
|
||||||
|
type='auditLog'
|
||||||
|
masterFilter={{ 'parent._id': hostId }}
|
||||||
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ActionHandler>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</EditObjectForm>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HostInfo
|
||||||
117
src/components/Dashboard/Management/Hosts/NewHost.jsx
Normal file
@ -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 (
|
||||||
|
<NewObjectForm type={'host'}>
|
||||||
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: 'Required',
|
||||||
|
key: 'required',
|
||||||
|
content: (
|
||||||
|
<ObjectInfo
|
||||||
|
type='host'
|
||||||
|
column={1}
|
||||||
|
bordered={false}
|
||||||
|
isEditing={true}
|
||||||
|
required={true}
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Optional',
|
||||||
|
key: 'optional',
|
||||||
|
content: (
|
||||||
|
<ObjectInfo
|
||||||
|
type='host'
|
||||||
|
column={1}
|
||||||
|
bordered={false}
|
||||||
|
isEditing={true}
|
||||||
|
required={false}
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Summary',
|
||||||
|
key: 'summary',
|
||||||
|
content: (
|
||||||
|
<ObjectInfo
|
||||||
|
type='host'
|
||||||
|
column={1}
|
||||||
|
bordered={false}
|
||||||
|
visibleProperties={{
|
||||||
|
_id: false,
|
||||||
|
createdAt: false,
|
||||||
|
updatedAt: false
|
||||||
|
}}
|
||||||
|
isEditing={false}
|
||||||
|
objectData={objectData}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<Flex gap='middle'>
|
||||||
|
{!isMobile && (
|
||||||
|
<div style={{ minWidth: '160px' }}>
|
||||||
|
<Steps
|
||||||
|
current={currentStep}
|
||||||
|
items={steps}
|
||||||
|
direction='vertical'
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isMobile && (
|
||||||
|
<Divider type='vertical' style={{ height: 'unset' }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
||||||
|
<Title level={2} style={{ margin: 0 }}>
|
||||||
|
New Host
|
||||||
|
</Title>
|
||||||
|
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
||||||
|
{steps[currentStep].content}
|
||||||
|
</div>
|
||||||
|
<NewObjectButtons
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={steps.length}
|
||||||
|
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
||||||
|
onNext={() => setCurrentStep((prev) => prev + 1)}
|
||||||
|
onSubmit={() => {
|
||||||
|
handleSubmit()
|
||||||
|
onOk()
|
||||||
|
}}
|
||||||
|
formValid={formValid}
|
||||||
|
submitLoading={submitLoading}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</NewObjectForm>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NewHost.propTypes = {
|
||||||
|
onOk: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewHost
|
||||||
@ -11,6 +11,7 @@ import SettingsIcon from '../../Icons/SettingsIcon'
|
|||||||
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
import AuditLogIcon from '../../Icons/AuditLogIcon'
|
||||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||||
import PersonIcon from '../../Icons/PersonIcon'
|
import PersonIcon from '../../Icons/PersonIcon'
|
||||||
|
import HostIcon from '../../Icons/HostIcon'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@ -50,6 +51,12 @@ const items = [
|
|||||||
label: 'Note Types',
|
label: 'Note Types',
|
||||||
path: '/dashboard/management/notetypes'
|
path: '/dashboard/management/notetypes'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'hosts',
|
||||||
|
icon: <HostIcon />,
|
||||||
|
label: 'Hosts',
|
||||||
|
path: '/dashboard/management/hosts'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'users',
|
key: 'users',
|
||||||
icon: <PersonIcon />,
|
icon: <PersonIcon />,
|
||||||
@ -91,7 +98,8 @@ const routeKeyMap = {
|
|||||||
'/dashboard/management/materials': 'materials',
|
'/dashboard/management/materials': 'materials',
|
||||||
'/dashboard/management/notetypes': 'notetypes',
|
'/dashboard/management/notetypes': 'notetypes',
|
||||||
'/dashboard/management/settings': 'settings',
|
'/dashboard/management/settings': 'settings',
|
||||||
'/dashboard/management/auditlogs': 'auditlogs'
|
'/dashboard/management/auditlogs': 'auditlogs',
|
||||||
|
'/dashboard/management/hosts': 'hosts'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ManagementSidebar = (props) => {
|
const ManagementSidebar = (props) => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
|||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
|||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
|||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
|||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler'
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
|||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
|||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler'
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
|
|||||||
@ -9,8 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
|||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import SubJobsTree from '../../common/SubJobsTree'
|
|
||||||
import ActionHandler from '../../common/ActionHandler'
|
import ActionHandler from '../../common/ActionHandler'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
|
||||||
import JobIcon from '../../../Icons/JobIcon'
|
import JobIcon from '../../../Icons/JobIcon'
|
||||||
@ -128,7 +127,11 @@ const JobInfo = () => {
|
|||||||
}
|
}
|
||||||
collapseKey='subJobs'
|
collapseKey='subJobs'
|
||||||
>
|
>
|
||||||
<SubJobsTree jobData={objectData} loading={loading} />
|
<ObjectTable
|
||||||
|
type='subJob'
|
||||||
|
masterFilter={{ 'job._id': jobId }}
|
||||||
|
visibleColumns={{ 'job._id': false }}
|
||||||
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const NewJob = ({ onOk }) => {
|
|||||||
<NewObjectForm
|
<NewObjectForm
|
||||||
type={'job'}
|
type={'job'}
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
active: true
|
state: { type: 'draft' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
@ -36,20 +36,6 @@ const NewJob = ({ onOk }) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Optional',
|
|
||||||
key: 'optional',
|
|
||||||
content: (
|
|
||||||
<ObjectInfo
|
|
||||||
type='job'
|
|
||||||
column={1}
|
|
||||||
bordered={false}
|
|
||||||
isEditing={true}
|
|
||||||
required={false}
|
|
||||||
objectData={objectData}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Summary',
|
title: 'Summary',
|
||||||
key: 'summary',
|
key: 'summary',
|
||||||
@ -61,7 +47,8 @@ const NewJob = ({ onOk }) => {
|
|||||||
visibleProperties={{
|
visibleProperties={{
|
||||||
_id: false,
|
_id: false,
|
||||||
createdAt: false,
|
createdAt: false,
|
||||||
updatedAt: false
|
updatedAt: false,
|
||||||
|
startedAt: false
|
||||||
}}
|
}}
|
||||||
isEditing={false}
|
isEditing={false}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import ObjectInfo from '../../common/ObjectInfo'
|
|||||||
import ViewButton from '../../common/ViewButton'
|
import ViewButton from '../../common/ViewButton'
|
||||||
import EditObjectForm from '../../common/EditObjectForm'
|
import EditObjectForm from '../../common/EditObjectForm'
|
||||||
import EditButtons from '../../common/EditButtons'
|
import EditButtons from '../../common/EditButtons'
|
||||||
import LockIndicator from '../../Management/Filaments/LockIndicator'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import PrinterJobsTree from '../../common/PrinterJobsTree'
|
import PrinterJobsTree from '../../common/PrinterJobsTree'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
|
|||||||
@ -23,7 +23,9 @@ const CountryDisplay = ({ countryCode }) => {
|
|||||||
hasBorderRadius={true}
|
hasBorderRadius={true}
|
||||||
gradient='real-circular'
|
gradient='real-circular'
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
<Text ellipsis>{country.name}</Text>
|
<Text ellipsis>{country.name}</Text>
|
||||||
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,70 +6,55 @@ import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
|
|||||||
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
|
||||||
|
|
||||||
const breadcrumbNameMap = {
|
const breadcrumbNameMap = {
|
||||||
'/dashboard/production': 'Production',
|
production: 'Production',
|
||||||
'/dashboard/inventory': 'Inventory',
|
inventory: 'Inventory',
|
||||||
'/dashboard/management': 'Management',
|
management: 'Management',
|
||||||
'/dashboard/developer': 'Developer',
|
developer: 'Developer',
|
||||||
'/dashboard/production/overview': 'Overview',
|
overview: 'Overview',
|
||||||
'/dashboard/production/printers': 'Printers',
|
printers: 'Printers',
|
||||||
'/dashboard/production/printers/control': 'Control',
|
hosts: 'Hosts',
|
||||||
'/dashboard/production/printers/info': 'Info',
|
control: 'Control',
|
||||||
'/dashboard/production/jobs': 'Jobs',
|
info: 'Info',
|
||||||
'/dashboard/production/subjobs': 'Sub Jobs',
|
jobs: 'Jobs',
|
||||||
'/dashboard/production/jobs/info': 'Info',
|
subjobs: 'Sub Jobs',
|
||||||
'/dashboard/production/gcodefiles': 'G Code Files',
|
gcodefiles: 'G Code Files',
|
||||||
'/dashboard/production/gcodefiles/info': 'Info',
|
filaments: 'Filaments',
|
||||||
'/dashboard/management/filaments': 'Filaments',
|
parts: 'Parts',
|
||||||
'/dashboard/management/filaments/info': 'Info',
|
products: 'Products',
|
||||||
'/dashboard/management/parts': 'Parts',
|
vendors: 'Vendors',
|
||||||
'/dashboard/management/parts/info': 'Info',
|
materials: 'Materials',
|
||||||
'/dashboard/management/products': 'Products',
|
notetypes: 'Note Types',
|
||||||
'/dashboard/management/products/info': 'Info',
|
users: 'Users',
|
||||||
'/dashboard/management/vendors': 'Vendors',
|
settings: 'Settings',
|
||||||
'/dashboard/management/vendors/info': 'Info',
|
auditlogs: 'Audit Logs',
|
||||||
'/dashboard/management/materials': 'Materials',
|
filamentstocks: 'Filament Stocks',
|
||||||
'/dashboard/management/materials/info': 'Info',
|
partstocks: 'Part Stocks',
|
||||||
'/dashboard/management/notetypes': 'Note Types',
|
productstocks: 'Products',
|
||||||
'/dashboard/management/notetypes/info': 'Info',
|
stockevents: 'Stock Events',
|
||||||
'/dashboard/management/users': 'Users',
|
stockaudits: 'Stock Audits',
|
||||||
'/dashboard/management/users/info': 'Info',
|
sessionstorage: 'Session Storage',
|
||||||
'/dashboard/management/settings': 'Settings',
|
authcontextdebug: 'Auth Context Debug',
|
||||||
'/dashboard/management/auditlogs': 'Audit Logs',
|
printservercontextdebug: 'Print Server Context Debug'
|
||||||
'/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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mainSections = ['production', 'inventory', 'management', 'developer']
|
||||||
|
|
||||||
const DashboardBreadcrumb = () => {
|
const DashboardBreadcrumb = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const pathSnippets = location.pathname.split('/').filter((i) => i)
|
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('/')}`
|
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`
|
||||||
if (url != '/dashboard') {
|
if (segment !== 'dashboard') {
|
||||||
// Check if this is a main section (Production, Inventory, or Management)
|
const isMainSection = mainSections.includes(segment)
|
||||||
const isMainSection =
|
const name = breadcrumbNameMap[segment] || segment
|
||||||
url === '/dashboard/production' ||
|
|
||||||
url === '/dashboard/inventory' ||
|
|
||||||
url === '/dashboard/management'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: isMainSection ? (
|
title: isMainSection ? (
|
||||||
<span style={{ padding: '0 12px' }}>{breadcrumbNameMap[url]}</span>
|
<span style={{ padding: '0 12px' }}>{name}</span>
|
||||||
) : (
|
) : (
|
||||||
<Link to={url} style={{ padding: '0 12px' }}>
|
<Link to={url} style={{ padding: '0 12px' }}>
|
||||||
{breadcrumbNameMap[url]}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
key: url
|
key: url
|
||||||
|
|||||||
@ -37,6 +37,8 @@ import SearchIcon from '../../Icons/SearchIcon'
|
|||||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||||
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
||||||
import PrinterIcon from '../../Icons/PrinterIcon'
|
import PrinterIcon from '../../Icons/PrinterIcon'
|
||||||
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
|
import DashboardWindowButtons from './DashboardWindowButtons'
|
||||||
|
|
||||||
const DashboardNavigation = () => {
|
const DashboardNavigation = () => {
|
||||||
const { logout, userProfile } = useContext(AuthContext)
|
const { logout, userProfile } = useContext(AuthContext)
|
||||||
@ -50,6 +52,7 @@ const DashboardNavigation = () => {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [selectedKey, setSelectedKey] = useState('production')
|
const [selectedKey, setSelectedKey] = useState('production')
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
|
const { platform, isElectron } = useContext(ElectronContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pathParts = location.pathname.split('/').filter(Boolean)
|
const pathParts = location.pathname.split('/').filter(Boolean)
|
||||||
@ -135,34 +138,17 @@ const DashboardNavigation = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const navigationContents = (
|
||||||
<Header
|
<>
|
||||||
style={{
|
{isElectron && platform == 'darwin' ? <DashboardWindowButtons /> : null}
|
||||||
width: '100vw',
|
{!isElectron && !isMobile ? (
|
||||||
padding: 0,
|
|
||||||
marginBottom: '0.1px',
|
|
||||||
background: 'unset'
|
|
||||||
}}
|
|
||||||
theme='light'
|
|
||||||
className='ant-menu-horizontal'
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
gap={'large'}
|
|
||||||
align='center'
|
|
||||||
className='ant-menu-light'
|
|
||||||
style={{
|
|
||||||
padding: '0 26px',
|
|
||||||
height: '100%',
|
|
||||||
borderBottom: '1px solid rgba(5, 5, 5, 0.00)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isMobile ? (
|
|
||||||
<FarmControlLogo style={{ fontSize: '200px' }} />
|
<FarmControlLogo style={{ fontSize: '200px' }} />
|
||||||
) : (
|
) : !isElectron && isMobile ? (
|
||||||
<FarmControlLogoSmall style={{ fontSize: '48px' }} />
|
<FarmControlLogoSmall style={{ fontSize: '48px' }} />
|
||||||
)}
|
) : null}
|
||||||
<Menu
|
<Menu
|
||||||
mode='horizontal'
|
mode='horizontal'
|
||||||
|
className={isElectron ? 'electron-navigation' : null}
|
||||||
items={mainMenuItems}
|
items={mainMenuItems}
|
||||||
style={{
|
style={{
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
@ -281,9 +267,47 @@ const DashboardNavigation = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isElectron ? (
|
||||||
|
<Flex
|
||||||
|
className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper'
|
||||||
|
style={{ lineHeight: '40px', padding: '0 8px 0 4px' }}
|
||||||
|
>
|
||||||
|
{navigationContents}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Flex vertical>
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
padding: 0,
|
||||||
|
marginBottom: '0.1px',
|
||||||
|
background: 'unset'
|
||||||
|
}}
|
||||||
|
theme='light'
|
||||||
|
className='ant-menu-horizontal'
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
gap={'large'}
|
||||||
|
align='center'
|
||||||
|
className='ant-menu-light'
|
||||||
|
style={{
|
||||||
|
padding: '0 26px',
|
||||||
|
height: '100%',
|
||||||
|
borderBottom: '1px solid rgba(5, 5, 5, 0.00)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{navigationContents}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider style={{ margin: 0 }} />
|
<Divider style={{ margin: 0 }} />
|
||||||
</Header>
|
</Header>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 { Layout, Menu, Flex, Button } from 'antd'
|
||||||
import { CaretDownFilled } from '@ant-design/icons'
|
import { CaretDownFilled } from '@ant-design/icons'
|
||||||
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
|
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
|
||||||
@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
|
|||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
const { Sider } = Layout
|
const { Sider } = Layout
|
||||||
|
|
||||||
const DashboardSidebar = ({
|
const DashboardSidebar = ({
|
||||||
@ -24,6 +24,8 @@ const DashboardSidebar = ({
|
|||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { isElectron } = useContext(ElectronContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof collapsedProp === 'boolean') {
|
if (typeof collapsedProp === 'boolean') {
|
||||||
setCollapsed(collapsedProp)
|
setCollapsed(collapsedProp)
|
||||||
@ -56,6 +58,7 @@ const DashboardSidebar = ({
|
|||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
items={_items}
|
items={_items}
|
||||||
_internalDisableMenuItemTitleTooltip
|
_internalDisableMenuItemTitleTooltip
|
||||||
|
style={{ lineHeight: '40px' }}
|
||||||
overflowedIndicator={<Button type='text' icon={<CaretDownFilled />} />}
|
overflowedIndicator={<Button type='text' icon={<CaretDownFilled />} />}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -72,6 +75,7 @@ const DashboardSidebar = ({
|
|||||||
mode='inline'
|
mode='inline'
|
||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
items={_items}
|
items={_items}
|
||||||
|
className={isElectron ? 'electron-sidebar' : null}
|
||||||
style={{ flexGrow: 1, border: 'none' }}
|
style={{ flexGrow: 1, border: 'none' }}
|
||||||
_internalDisableMenuItemTitleTooltip
|
_internalDisableMenuItemTitleTooltip
|
||||||
/>
|
/>
|
||||||
|
|||||||
50
src/components/Dashboard/common/DashboardWindowButtons.jsx
Normal file
@ -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 = (
|
||||||
|
<Button
|
||||||
|
icon={<XMarkIcon />}
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => handleWindowControl('close')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const maximizeButton = (
|
||||||
|
<Button
|
||||||
|
icon={<MinusIcon />}
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => handleWindowControl('minimize')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const minimizeButton = (
|
||||||
|
<Button
|
||||||
|
icon={isMaximized ? <ContractIcon /> : <ExpandIcon />}
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => handleWindowControl('maximize')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex align='center'>
|
||||||
|
{platform == 'darwin' ? (
|
||||||
|
<>
|
||||||
|
{closeButton}
|
||||||
|
{minimizeButton}
|
||||||
|
{maximizeButton}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>{closeButton}</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardWindowButtons
|
||||||
@ -6,14 +6,7 @@ import BinIcon from '../../Icons/BinIcon'
|
|||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
const DeleteObjectModal = ({
|
const DeleteObjectModal = ({ open, onOk, onCancel, loading, objectType }) => {
|
||||||
open,
|
|
||||||
onOk,
|
|
||||||
onCancel,
|
|
||||||
loading,
|
|
||||||
objectType,
|
|
||||||
objectName
|
|
||||||
}) => {
|
|
||||||
const model = getModelByName(objectType)
|
const model = getModelByName(objectType)
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -49,8 +42,7 @@ const DeleteObjectModal = ({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
Are you sure you want to delete this {model.label.toLowerCase()}
|
Are you sure you want to delete this {model.label.toLowerCase()}?
|
||||||
{objectName ? ` "${objectName}"` : ''}?
|
|
||||||
</Text>
|
</Text>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
@ -61,8 +53,7 @@ DeleteObjectModal.propTypes = {
|
|||||||
onOk: PropTypes.func.isRequired,
|
onOk: PropTypes.func.isRequired,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
objectType: PropTypes.string.isRequired,
|
objectType: PropTypes.string.isRequired
|
||||||
objectName: PropTypes.string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeleteObjectModal
|
export default DeleteObjectModal
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ApiServerContext } from '../context/ApiServerContext'
|
|||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import DeleteObjectModal from './DeleteObjectModal'
|
import DeleteObjectModal from './DeleteObjectModal'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EditObjectForm is a reusable form component for editing any object type.
|
* EditObjectForm is a reusable form component for editing any object type.
|
||||||
@ -82,7 +83,7 @@ const EditObjectForm = ({ id, type, style, children }) => {
|
|||||||
|
|
||||||
// Update event handler
|
// Update event handler
|
||||||
const updateObjectEventHandler = useCallback((value) => {
|
const updateObjectEventHandler = useCallback((value) => {
|
||||||
setObjectData((prev) => ({ ...prev, ...value }))
|
setObjectData((prev) => merge({}, prev, value))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Update event handler
|
// Update event handler
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const propertyOrder = [
|
|||||||
const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
|
const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
endpoint={`${config.backendUrl}/gcodefiles`}
|
endpoint={`${config.backendUrl}/gcodefiles/properties`}
|
||||||
propertyOrder={propertyOrder}
|
propertyOrder={propertyOrder}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
useFilter={useFilter}
|
useFilter={useFilter}
|
||||||
@ -21,7 +21,7 @@ const GCodeFileSelect = ({ onChange, filter, useFilter = false, style }) => {
|
|||||||
showSearch={true}
|
showSearch={true}
|
||||||
style={style}
|
style={style}
|
||||||
placeholder='Select GCode File'
|
placeholder='Select GCode File'
|
||||||
type='gcodefile'
|
type='gcodeFile'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Flex, Tag } from 'antd'
|
import { Flex, Tag } from 'antd'
|
||||||
import IdDisplay from '../../common/IdDisplay'
|
import IdDisplay from './IdDisplay'
|
||||||
import LockIcon from '../../../Icons/LockIcon'
|
import LockIcon from '../../Icons/LockIcon'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
const LockIndicator = ({ lock }) => {
|
const LockIndicator = ({ lock }) => {
|
||||||
@ -95,7 +95,7 @@ const NoteItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reload child notes when a new child note is added
|
// 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
|
// Always fetch child notes when this function is called
|
||||||
// This ensures child notes are loaded even if the parent wasn't expanded before
|
// This ensures child notes are loaded even if the parent wasn't expanded before
|
||||||
setLoadingChildNotes(note._id)
|
setLoadingChildNotes(note._id)
|
||||||
@ -110,14 +110,14 @@ const NoteItem = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingChildNotes(null)
|
setLoadingChildNotes(null)
|
||||||
}
|
}
|
||||||
}
|
}, [fetchData, note._id])
|
||||||
|
|
||||||
// Listen for child note additions
|
// Listen for child note additions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onChildNoteAdded) {
|
if (onChildNoteAdded) {
|
||||||
onChildNoteAdded(note._id, reloadChildNotes)
|
onChildNoteAdded(note._id, reloadChildNotes)
|
||||||
}
|
}
|
||||||
}, [note._id, onChildNoteAdded])
|
}, [note._id, onChildNoteAdded, reloadChildNotes])
|
||||||
|
|
||||||
// Check if the current user can delete this note
|
// Check if the current user can delete this note
|
||||||
const canDeleteNote = userProfile && userProfile._id === note.user._id
|
const canDeleteNote = userProfile && userProfile._id === note.user._id
|
||||||
|
|||||||
27
src/components/Dashboard/common/ObjectDisplay.jsx
Normal file
@ -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 <Text type='secondary'>n/a</Text>
|
||||||
|
}
|
||||||
|
const model = getModelByName(objectType)
|
||||||
|
const Icon = model.icon
|
||||||
|
return (
|
||||||
|
<Tag color='default' style={{ margin: 0 }} icon={<Icon />}>
|
||||||
|
{object?.name ? object.name : null}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectDisplay.propTypes = {
|
||||||
|
object: PropTypes.object,
|
||||||
|
objectType: PropTypes.string,
|
||||||
|
style: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectDisplay
|
||||||
@ -1,49 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { List, Typography, Flex } from 'antd'
|
import ObjectDisplay from './ObjectDisplay'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { Space, Typography } from 'antd'
|
||||||
import IdDisplay from './IdDisplay'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
const ObjectList = ({ value, objectType, bordered = true }) => {
|
const ObjectList = ({ value, objectType, style }) => {
|
||||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return <Text type='secondary'>n/a</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<Space size={'small'} wrap style={style}>
|
||||||
size='small'
|
{value.map((item) => (
|
||||||
bordered={bordered}
|
<ObjectDisplay object={item} objectType={objectType} key={item._id} />
|
||||||
dataSource={value}
|
))}
|
||||||
renderItem={(item) => {
|
</Space>
|
||||||
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 = {
|
ObjectList.propTypes = {
|
||||||
value: PropTypes.array,
|
value: PropTypes.array,
|
||||||
bordered: PropTypes.bool,
|
objectType: PropTypes.string,
|
||||||
objectType: PropTypes.string
|
style: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectList
|
export default ObjectList
|
||||||
|
|||||||
@ -10,14 +10,9 @@ import {
|
|||||||
DatePicker,
|
DatePicker,
|
||||||
Switch
|
Switch
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import VendorSelect from './VendorSelect'
|
|
||||||
import FilamentSelect from './FilamentSelect'
|
|
||||||
import IdDisplay from './IdDisplay'
|
import IdDisplay from './IdDisplay'
|
||||||
import TimeDisplay from './TimeDisplay'
|
import TimeDisplay from './TimeDisplay'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import PrinterSelect from './PrinterSelect'
|
|
||||||
import GCodeFileSelect from './GCodeFileSelect'
|
|
||||||
import PartSelect from './PartSelect'
|
|
||||||
import EmailDisplay from './EmailDisplay'
|
import EmailDisplay from './EmailDisplay'
|
||||||
import UrlDisplay from './UrlDisplay'
|
import UrlDisplay from './UrlDisplay'
|
||||||
import CountryDisplay from './CountryDisplay'
|
import CountryDisplay from './CountryDisplay'
|
||||||
@ -41,6 +36,8 @@ import ObjectList from './ObjectList'
|
|||||||
import VarianceDisplay from './VarianceDisplay'
|
import VarianceDisplay from './VarianceDisplay'
|
||||||
import OperationDisplay from './OperationDisplay'
|
import OperationDisplay from './OperationDisplay'
|
||||||
import MarkdownDisplay from './MarkdownDisplay'
|
import MarkdownDisplay from './MarkdownDisplay'
|
||||||
|
import ObjectSelect from './ObjectSelect'
|
||||||
|
import ObjectDisplay from './ObjectDisplay'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -291,7 +288,7 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
case 'object': {
|
case 'object': {
|
||||||
if (value && value.name) {
|
if (value && value.name) {
|
||||||
return <Text ellipsis>{value.name}</Text>
|
return <ObjectDisplay object={value} objectType={objectType} />
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text type='secondary' {...textParams}>
|
<Text type='secondary' {...textParams}>
|
||||||
@ -424,7 +421,7 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
const hasRequiredRule = rules.some((rule) => rule && rule.required)
|
const hasRequiredRule = rules.some((rule) => rule && rule.required)
|
||||||
if (!hasRequiredRule) {
|
if (!hasRequiredRule) {
|
||||||
rules.push({ required: true, message: 'This field is required' })
|
rules.push({ required: true, message: '' })
|
||||||
}
|
}
|
||||||
mergedFormItemProps.rules = rules
|
mergedFormItemProps.rules = rules
|
||||||
}
|
}
|
||||||
@ -591,45 +588,17 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'object':
|
case 'object':
|
||||||
switch (objectType) {
|
|
||||||
case 'vendor':
|
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<VendorSelect placeholder={label} />
|
<ObjectSelect type={objectType} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'printer':
|
case 'objectList':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<PrinterSelect placeholder={label} />
|
<ObjectSelect type={objectType} multiple />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'gcodeFile':
|
|
||||||
return (
|
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
|
||||||
<GCodeFileSelect placeholder={label} />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
case 'filament':
|
|
||||||
return (
|
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
|
||||||
<FilamentSelect placeholder={label} />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
case 'part':
|
|
||||||
return (
|
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
|
||||||
<PartSelect placeholder={label} />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Text type='secondary' {...textParams}>
|
|
||||||
n/a
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'tags':
|
case 'tags':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
|
|||||||
@ -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 PropTypes from 'prop-types'
|
||||||
import { TreeSelect, Typography, Flex, Badge, Space, Button, Input } from 'antd'
|
import { TreeSelect, Space, Button, Input } from 'antd'
|
||||||
import axios from 'axios'
|
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
|
||||||
import IdDisplay from './IdDisplay'
|
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
const { Text } = Typography
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
const { SHOW_CHILD } = TreeSelect
|
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 = ({
|
const ObjectSelect = ({
|
||||||
endpoint,
|
|
||||||
propertyOrder,
|
|
||||||
filter = {},
|
|
||||||
useFilter = false,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
showSearch = false,
|
|
||||||
treeCheckable = false,
|
|
||||||
treeSelectProps = {},
|
|
||||||
type = 'unknown',
|
type = 'unknown',
|
||||||
|
showSearch = false,
|
||||||
|
multiple = false,
|
||||||
|
treeSelectProps = {},
|
||||||
|
filter = {},
|
||||||
|
value,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
|
const { fetchObjectsByProperty } = useContext(ApiServerContext)
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const [treeData, setTreeData] = useState([])
|
const [treeData, setTreeData] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
|
||||||
const [defaultValue, setDefaultValue] = useState(treeCheckable ? [] : value)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [searchValue, setSearchValue] = useState('')
|
|
||||||
const [error, setError] = 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 ---
|
// Fetch the object properties tree from the API
|
||||||
const fetchData = useCallback(
|
const handleFetchObjectsProperties = useCallback(
|
||||||
async (property, filter, search) => {
|
async (customFilter = filter) => {
|
||||||
setLoading(true)
|
|
||||||
setError(false)
|
|
||||||
try {
|
try {
|
||||||
const params = { ...filter, property }
|
const data = await fetchObjectsByProperty(type, {
|
||||||
if (search) params.search = search
|
properties: properties,
|
||||||
const response = await axios.get(endpoint, {
|
filter: customFilter
|
||||||
params,
|
|
||||||
withCredentials: true
|
|
||||||
})
|
})
|
||||||
setLoading(false)
|
setObjectPropertiesTree((prev) => merge({}, prev, data))
|
||||||
return response.data
|
setInitialLoading(false)
|
||||||
} catch (err) {
|
|
||||||
setLoading(false)
|
|
||||||
setError(true)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[endpoint]
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- API: Fetch a single object by ID ---
|
|
||||||
const fetchObjectById = useCallback(
|
|
||||||
async (objectId) => {
|
|
||||||
setLoading(true)
|
|
||||||
setError(false)
|
setError(false)
|
||||||
try {
|
return data
|
||||||
const response = await axios.get(`${endpoint}/${objectId}`, {
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
setLoading(false)
|
|
||||||
return response.data
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLoading(false)
|
|
||||||
setError(true)
|
setError(true)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[endpoint]
|
[type, fetchObjectsByProperty, properties, filter]
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Render node title ---
|
// Convert the API response to AntD TreeSelect treeData
|
||||||
const renderTitle = useCallback(
|
const buildTreeData = useCallback(
|
||||||
(item) => {
|
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
||||||
if (item.propertyType) {
|
if (!data) return []
|
||||||
return (
|
if (Array.isArray(data)) {
|
||||||
|
return data.map((object) => {
|
||||||
|
setObjectList((prev) => {
|
||||||
|
const filtered = prev.filter(
|
||||||
|
(prevObject) => prevObject._id != object._id
|
||||||
|
)
|
||||||
|
return [...filtered, object]
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
title: (
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
type={item.propertyType}
|
key={object._id}
|
||||||
value={item.value}
|
type='object'
|
||||||
|
value={object}
|
||||||
objectType={type}
|
objectType={type}
|
||||||
|
objectData={object}
|
||||||
|
isEditing={false}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
} else {
|
value: object._id,
|
||||||
const model = getModelByName(type)
|
key: object._id,
|
||||||
const Icon = model.icon
|
|
||||||
return (
|
|
||||||
<Flex gap={'small'} align='center' style={{ width: '100%' }}>
|
|
||||||
{Icon && <Icon />}
|
|
||||||
{item?.color && <Badge color={item.color}></Badge>}
|
|
||||||
<Text ellipsis>{item.name || type.title}</Text>
|
|
||||||
<IdDisplay id={item._id} longId={false} type={type} />
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[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,
|
isLeaf: true,
|
||||||
raw: item
|
property: properties[pIdx - 1], // previous property
|
||||||
}
|
parentKeys,
|
||||||
})
|
filterPath
|
||||||
},
|
|
||||||
[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
|
|
||||||
)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
if (typeof data == 'object') {
|
||||||
})
|
const property = properties[pIdx] || null
|
||||||
} else {
|
return Object.entries(data)
|
||||||
setDefaultValue([])
|
.map(([key, value]) => {
|
||||||
}
|
if (property != null && typeof value === 'object') {
|
||||||
} else {
|
const newFilterPath = filterPath.concat({ property, value: key })
|
||||||
if (value?._id) {
|
return {
|
||||||
setDefaultValue(value._id)
|
title: <ObjectProperty type={property} value={key} />,
|
||||||
const existingNode = treeData.find((node) => node.value === value._id)
|
value: key,
|
||||||
if (!existingNode) {
|
key: parentKeys.concat(key).join(':'),
|
||||||
fetchObjectById(value._id).then((object) => {
|
property,
|
||||||
if (object) {
|
parentKeys: parentKeys.concat(key),
|
||||||
setTreeData((prev) => [
|
filterPath: newFilterPath,
|
||||||
...prev,
|
selectable: false,
|
||||||
...buildLeafNodes(
|
children: buildTreeData(
|
||||||
[object],
|
|
||||||
object[propertyOrder[propertyOrder.length - 2]] || 0
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
value,
|
value,
|
||||||
treeData,
|
pIdx + 1,
|
||||||
fetchObjectById,
|
parentKeys.concat(key),
|
||||||
buildLeafNodes,
|
newFilterPath
|
||||||
propertyOrder,
|
),
|
||||||
treeCheckable
|
isLeaf: false
|
||||||
])
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[properties, type]
|
||||||
|
)
|
||||||
|
|
||||||
// --- Initial load ---
|
// --- 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (treeData.length === 0 && !error && !loading) {
|
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
||||||
if (!treeCheckable && value && typeof value === 'object' && value._id) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (useFilter || searchValue) {
|
if (!initialized) {
|
||||||
// Flat filter mode
|
handleFetchObjectsProperties()
|
||||||
fetchData(null, filter, searchValue).then((data) => {
|
setInitialized(true)
|
||||||
setTreeData(buildLeafNodes(data, 0))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
handleTreeLoad(null)
|
|
||||||
}
|
}
|
||||||
}
|
}, [value, filter, properties, handleFetchObjectsProperties, initialized])
|
||||||
}, [
|
|
||||||
treeData,
|
|
||||||
useFilter,
|
|
||||||
filter,
|
|
||||||
searchValue,
|
|
||||||
buildLeafNodes,
|
|
||||||
fetchData,
|
|
||||||
handleTreeLoad,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
value,
|
|
||||||
treeCheckable
|
|
||||||
])
|
|
||||||
|
|
||||||
// --- Error UI ---
|
// --- Error UI ---
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -346,6 +221,7 @@ const ObjectSelect = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setError(false)
|
setError(false)
|
||||||
setTreeData([])
|
setTreeData([])
|
||||||
|
setInitialized(false)
|
||||||
}}
|
}}
|
||||||
danger
|
danger
|
||||||
/>
|
/>
|
||||||
@ -353,35 +229,37 @@ const ObjectSelect = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialLoading) {
|
||||||
|
return <TreeSelect disabled loading placeholder='Loading...' />
|
||||||
|
}
|
||||||
|
|
||||||
// --- Main TreeSelect UI ---
|
// --- Main TreeSelect UI ---
|
||||||
return (
|
return (
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
treeDataSimpleMode
|
treeDataSimpleMode={false}
|
||||||
treeDefaultExpandAll={true}
|
treeDefaultExpandAll={true}
|
||||||
loadData={handleTreeLoad}
|
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
onChange={handleOnChange}
|
|
||||||
loading={loading}
|
|
||||||
value={loading ? 'Loading...' : defaultValue}
|
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
onSearch={showSearch ? handleSearch : undefined}
|
multiple={multiple}
|
||||||
treeCheckable={treeCheckable}
|
loadData={loadData}
|
||||||
showCheckedStrategy={treeCheckable ? SHOW_CHILD : undefined}
|
showCheckedStrategy={SHOW_CHILD}
|
||||||
|
placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`}
|
||||||
{...treeSelectProps}
|
{...treeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
value={treeSelectValue}
|
||||||
|
onChange={onTreeSelectChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectSelect.propTypes = {
|
ObjectSelect.propTypes = {
|
||||||
endpoint: PropTypes.string.isRequired,
|
properties: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
propertyOrder: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
filter: PropTypes.object,
|
filter: PropTypes.object,
|
||||||
useFilter: PropTypes.bool,
|
useFilter: PropTypes.bool,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
treeCheckable: PropTypes.bool,
|
multiple: PropTypes.bool,
|
||||||
treeSelectProps: PropTypes.object,
|
treeSelectProps: PropTypes.object,
|
||||||
type: PropTypes.string.isRequired
|
type: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import CheckIcon from '../../Icons/CheckIcon'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
|
import merge from 'lodash/merge'
|
||||||
const logger = loglevel.getLogger('DasboardTable')
|
const logger = loglevel.getLogger('DasboardTable')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ const ObjectTable = forwardRef(
|
|||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 316px)'
|
||||||
}
|
}
|
||||||
if (cards) {
|
if (cards) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 280px)'
|
||||||
}
|
}
|
||||||
const [, contextHolder] = message.useMessage()
|
const [, contextHolder] = message.useMessage()
|
||||||
const tableRef = useRef(null)
|
const tableRef = useRef(null)
|
||||||
@ -108,7 +109,6 @@ const ObjectTable = forwardRef(
|
|||||||
type={'text'}
|
type={'text'}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(objectData)
|
|
||||||
if (action.url) {
|
if (action.url) {
|
||||||
navigate(action.url(objectData._id))
|
navigate(action.url(objectData._id))
|
||||||
}
|
}
|
||||||
@ -135,13 +135,6 @@ const ObjectTable = forwardRef(
|
|||||||
order: sorter.order
|
order: sorter.order
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log('Fetching Objects', {
|
|
||||||
page: pageNum,
|
|
||||||
limit: pageSize,
|
|
||||||
filter: { ...filter, ...masterFilter },
|
|
||||||
sorter,
|
|
||||||
onDataChange
|
|
||||||
})
|
|
||||||
try {
|
try {
|
||||||
const result = await fetchObjects(type, {
|
const result = await fetchObjects(type, {
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
@ -250,12 +243,6 @@ const ObjectTable = forwardRef(
|
|||||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||||
const prevPage = lowestPage - 1
|
const prevPage = lowestPage - 1
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
'Down',
|
|
||||||
scrollHeight - scrollTop - clientHeight < 100,
|
|
||||||
lazyLoading
|
|
||||||
)
|
|
||||||
|
|
||||||
// Load more data when scrolling down
|
// Load more data when scrolling down
|
||||||
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore) {
|
if (scrollHeight - scrollTop - clientHeight < 100 && hasMore) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -276,13 +263,14 @@ const ObjectTable = forwardRef(
|
|||||||
loadPreviousPage()
|
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++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
const page = pages[i]
|
const page = pages[i]
|
||||||
fetchData(page.pageNum)
|
await fetchData(page.pageNum)
|
||||||
}
|
}
|
||||||
}, [fetchData, pages])
|
}, [fetchData, pages])
|
||||||
|
|
||||||
@ -292,7 +280,7 @@ const ObjectTable = forwardRef(
|
|||||||
prevPages.map((page) => ({
|
prevPages.map((page) => ({
|
||||||
...page,
|
...page,
|
||||||
items: page.items.map((item) =>
|
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
|
// Table columns from model properties
|
||||||
const columnsWithSkeleton = [
|
const columnsWithSkeleton = [
|
||||||
{
|
{
|
||||||
title: model.icon,
|
title: lazyLoading ? <LoadingOutlined /> : cards ? model.icon : null,
|
||||||
key: 'icon',
|
key: 'icon',
|
||||||
width: 45,
|
width: 45,
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
@ -688,16 +676,12 @@ const ObjectTable = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
{cards ? (
|
<Flex gap={'middle'} vertical>
|
||||||
<Spin indicator={<LoadingOutlined />} spinning={loading}>
|
|
||||||
{renderCards()}
|
|
||||||
</Spin>
|
|
||||||
) : (
|
|
||||||
<Table
|
<Table
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
dataSource={tableData}
|
dataSource={tableData}
|
||||||
columns={columnsWithSkeleton}
|
columns={columnsWithSkeleton}
|
||||||
className={'dashboard-table'}
|
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: adjustedScrollHeight }}
|
scroll={{ y: adjustedScrollHeight }}
|
||||||
rowKey='_id'
|
rowKey='_id'
|
||||||
@ -707,7 +691,12 @@ const ObjectTable = forwardRef(
|
|||||||
showSorterTooltip={false}
|
showSorterTooltip={false}
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
/>
|
/>
|
||||||
)}
|
{cards ? (
|
||||||
|
<Spin indicator={<LoadingOutlined />} spinning={loading}>
|
||||||
|
{renderCards()}
|
||||||
|
</Spin>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,6 @@ const PrinterMovementPanel = ({ printerId }) => {
|
|||||||
|
|
||||||
const handleHomeAxisClick = (axis) => {
|
const handleHomeAxisClick = (axis) => {
|
||||||
if (printServer) {
|
if (printServer) {
|
||||||
logger.debug('Homeing Axis:', axis)
|
|
||||||
printServer.emit('printer.gcode.script', {
|
printServer.emit('printer.gcode.script', {
|
||||||
printerId,
|
printerId,
|
||||||
script: `G28 ${axis}`
|
script: `G28 ${axis}`
|
||||||
@ -52,7 +51,6 @@ const PrinterMovementPanel = ({ printerId }) => {
|
|||||||
const handleMoveAxisClick = (axis, minus) => {
|
const handleMoveAxisClick = (axis, minus) => {
|
||||||
const distanceValue = !minus ? posValue * -1 : posValue
|
const distanceValue = !minus ? posValue * -1 : posValue
|
||||||
if (printServer) {
|
if (printServer) {
|
||||||
logger.debug('Moving Axis:', axis, distanceValue)
|
|
||||||
printServer.emit('printer.gcode.script', {
|
printServer.emit('printer.gcode.script', {
|
||||||
printerId,
|
printerId,
|
||||||
script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}`
|
script: `_CLIENT_LINEAR_MOVE ${axis}=${distanceValue} F=${rateValue}`
|
||||||
|
|||||||
@ -4,14 +4,25 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
const TagsDisplay = ({ tags = [], style }) => {
|
const TagsDisplay = ({ tags, style }) => {
|
||||||
if (tags.length == 0) {
|
let tagArray = []
|
||||||
|
if (typeof tags === 'string') {
|
||||||
|
tagArray = [tags]
|
||||||
|
} else if (Array.isArray(tags)) {
|
||||||
|
tagArray = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!tagArray ||
|
||||||
|
tagArray.length === 0 ||
|
||||||
|
(tagArray.length === 1 && !tagArray[0])
|
||||||
|
) {
|
||||||
return <Text type='secondary'>n/a</Text>
|
return <Text type='secondary'>n/a</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={'small'} wrap style={style}>
|
<Space size={'small'} wrap style={style}>
|
||||||
{tags.map((tag, index) => (
|
{tagArray.map((tag, index) => (
|
||||||
<Tag key={index} color='blue' style={{ margin: 0 }}>
|
<Tag key={index} color='blue' style={{ margin: 0 }}>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|||||||
@ -244,8 +244,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
subscribedCallbacksRef.current.set(id, callbacks)
|
subscribedCallbacksRef.current.set(id, callbacks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Removed update event listener for object:', id)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -263,26 +261,19 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
subscribedCallbacksRef.current.set(type, callbacks)
|
subscribedCallbacksRef.current.set(type, callbacks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Removed new event listener for type:', type)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const subscribeToObject = useCallback(
|
const subscribeToObject = useCallback(
|
||||||
(id, type, callback) => {
|
(id, type, callback) => {
|
||||||
logger.debug('Subscribing to object:', id, 'type:', type)
|
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
// Add callback to the subscribed callbacks map immediately
|
// Add callback to the subscribed callbacks map immediately
|
||||||
if (!subscribedCallbacksRef.current.has(id)) {
|
if (!subscribedCallbacksRef.current.has(id)) {
|
||||||
subscribedCallbacksRef.current.set(id, [])
|
subscribedCallbacksRef.current.set(id, [])
|
||||||
}
|
}
|
||||||
subscribedCallbacksRef.current.get(id).push(callback)
|
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 })
|
socketRef.current.emit('subscribe', { id: id, type: type })
|
||||||
logger.debug('Registered update event listener for object:', id)
|
|
||||||
|
|
||||||
// Return cleanup function
|
// Return cleanup function
|
||||||
return () => offUpdateEvent(id, type, callback)
|
return () => offUpdateEvent(id, type, callback)
|
||||||
@ -382,9 +373,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(fetchUrl, {
|
const response = await axios.get(fetchUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -426,9 +417,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
order: sorter.order
|
order: sorter.order
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -474,9 +465,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
properties: properties.join(',') // Convert array to comma-separated string
|
properties: properties.join(',') // Convert array to comma-separated string
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -498,9 +489,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.put(updateUrl, value, {
|
const response = await axios.put(updateUrl, value, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
})
|
||||||
logger.debug('Object updated successfully')
|
logger.debug('Object updated successfully')
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
@ -526,9 +517,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.delete(deleteUrl, {
|
const response = await axios.delete(deleteUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
})
|
||||||
logger.debug('Object deleted successfully')
|
logger.debug('Object deleted successfully')
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
@ -554,9 +545,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(createUrl, value, {
|
const response = await axios.post(createUrl, value, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
})
|
||||||
return response.data
|
return response.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -577,9 +568,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
|
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -608,9 +599,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
order: 'ascend'
|
order: 'ascend'
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
Authorization: `Bearer ${token}`
|
||||||
withCredentials: true
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const notesData = response.data
|
const notesData = response.data
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
// src/contexts/AuthContext.js
|
// src/contexts/AuthContext.js
|
||||||
import React, { createContext, useState, useCallback, useEffect } from 'react'
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useContext
|
||||||
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { message, Modal, notification, Progress, Button, Space } from 'antd'
|
import { message, Modal, notification, Progress, Button, Space } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
@ -8,6 +14,8 @@ import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|||||||
import config from '../../../config'
|
import config from '../../../config'
|
||||||
import AppError from '../../App/AppError'
|
import AppError from '../../App/AppError'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
|
import { ElectronContext } from './ElectronContext'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
const logger = loglevel.getLogger('ApiServerContext')
|
const logger = loglevel.getLogger('ApiServerContext')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
@ -18,6 +26,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
const [notificationApi, notificationContextHolder] =
|
const [notificationApi, notificationContextHolder] =
|
||||||
notification.useNotification()
|
notification.useNotification()
|
||||||
const [authenticated, setAuthenticated] = useState(false)
|
const [authenticated, setAuthenticated] = useState(false)
|
||||||
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [token, setToken] = useState(null)
|
const [token, setToken] = useState(null)
|
||||||
const [expiresAt, setExpiresAt] = useState(null)
|
const [expiresAt, setExpiresAt] = useState(null)
|
||||||
@ -25,12 +34,27 @@ const AuthProvider = ({ children }) => {
|
|||||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
|
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false)
|
||||||
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
|
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
|
||||||
const [authError, setAuthError] = useState(null)
|
const [authError, setAuthError] = useState(null)
|
||||||
|
const { openExternalUrl, isElectron } = useContext(ElectronContext)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
// Read token from session storage if present
|
||||||
|
useEffect(() => {
|
||||||
|
const storedToken = sessionStorage.getItem('authToken')
|
||||||
|
const storedExpiresAt = sessionStorage.getItem('authExpiresAt')
|
||||||
|
if (storedToken && storedExpiresAt) {
|
||||||
|
setToken(storedToken)
|
||||||
|
setExpiresAt(storedExpiresAt)
|
||||||
|
setAuthenticated(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const logout = useCallback((redirectUri = '/login') => {
|
const logout = useCallback((redirectUri = '/login') => {
|
||||||
setAuthenticated(false)
|
setAuthenticated(false)
|
||||||
setToken(null)
|
setToken(null)
|
||||||
setExpiresAt(null)
|
setExpiresAt(null)
|
||||||
setUserProfile(null)
|
setUserProfile(null)
|
||||||
|
sessionStorage.removeItem('authToken')
|
||||||
|
sessionStorage.removeItem('authExpiresAt')
|
||||||
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -38,25 +62,37 @@ const AuthProvider = ({ children }) => {
|
|||||||
const loginWithSSO = useCallback(
|
const loginWithSSO = useCallback(
|
||||||
(redirectUri = window.location.pathname + window.location.search) => {
|
(redirectUri = window.location.pathname + window.location.search) => {
|
||||||
messageApi.info('Logging in with tombutcher.work')
|
messageApi.info('Logging in with tombutcher.work')
|
||||||
window.location.href = `${config.backendUrl}/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
const loginUrl = `${config.backendUrl}/auth/${isElectron ? 'app/' : ''}login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||||
|
if (isElectron) {
|
||||||
|
console.log('Opening external url...')
|
||||||
|
openExternalUrl(loginUrl)
|
||||||
|
} else {
|
||||||
|
console.log('Redirecting...')
|
||||||
|
window.location.href = loginUrl
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[messageApi]
|
[messageApi, openExternalUrl, isElectron]
|
||||||
)
|
)
|
||||||
// Function to check if the user is logged in
|
|
||||||
const checkAuthStatus = useCallback(async () => {
|
const getLoginToken = useCallback(
|
||||||
|
async (code) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setShowUnauthorizedModal(false)
|
||||||
|
setShowSessionExpiredModal(false)
|
||||||
setAuthError(null)
|
setAuthError(null)
|
||||||
try {
|
try {
|
||||||
// Make a call to your backend to check auth status
|
// Make a call to your backend to check auth status
|
||||||
const response = await axios.get(`${config.backendUrl}/auth/user`, {
|
const response = await axios.get(
|
||||||
withCredentials: true // Important for including cookies
|
`${config.backendUrl}/auth/${isElectron ? 'app/' : ''}token?code=${code}`
|
||||||
})
|
)
|
||||||
|
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
logger.debug('Got auth token!')
|
logger.debug('Got auth token!')
|
||||||
setToken(response.data.access_token)
|
setToken(response.data.access_token)
|
||||||
setExpiresAt(response.data.expires_at)
|
setExpiresAt(response.data.expires_at)
|
||||||
setUserProfile(response.data)
|
setUserProfile(response.data)
|
||||||
|
sessionStorage.setItem('authToken', response.data.access_token)
|
||||||
|
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||||
} else {
|
} else {
|
||||||
setAuthenticated(false)
|
setAuthenticated(false)
|
||||||
setAuthError('Failed to authenticate user.')
|
setAuthError('Failed to authenticate user.')
|
||||||
@ -72,7 +108,44 @@ const AuthProvider = ({ children }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
},
|
||||||
|
[isElectron]
|
||||||
|
)
|
||||||
|
// Function to check if the user is logged in
|
||||||
|
const checkAuthStatus = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setAuthError(null)
|
||||||
|
try {
|
||||||
|
// Make a call to your backend to check auth status
|
||||||
|
const response = await axios.get(`${config.backendUrl}/auth/user`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 200 && response.data) {
|
||||||
|
logger.debug('Got auth token!')
|
||||||
|
setToken(response.data.access_token)
|
||||||
|
setExpiresAt(response.data.expires_at)
|
||||||
|
setUserProfile(response.data)
|
||||||
|
sessionStorage.setItem('authToken', response.data.access_token)
|
||||||
|
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||||
|
} else {
|
||||||
|
setAuthenticated(false)
|
||||||
|
setAuthError('Failed to authenticate user.')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Auth check failed', error)
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
setShowUnauthorizedModal(true)
|
||||||
|
} else {
|
||||||
|
setAuthError('Error connecting to authentication service.')
|
||||||
|
}
|
||||||
|
setAuthenticated(false)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [token])
|
||||||
|
|
||||||
const refreshToken = useCallback(async () => {
|
const refreshToken = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -82,6 +155,8 @@ const AuthProvider = ({ children }) => {
|
|||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
setToken(response.data.access_token)
|
setToken(response.data.access_token)
|
||||||
setExpiresAt(response.data.expires_at)
|
setExpiresAt(response.data.expires_at)
|
||||||
|
sessionStorage.setItem('authToken', response.data.access_token)
|
||||||
|
sessionStorage.setItem('authExpiresAt', response.data.expires_at)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Token refresh failed', error)
|
console.error('Token refresh failed', error)
|
||||||
@ -170,8 +245,6 @@ const AuthProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
intervalId = setInterval(tokenRefresh, 1000)
|
intervalId = setInterval(tokenRefresh, 1000)
|
||||||
|
|
||||||
console.log('fresh', authenticated)
|
|
||||||
tokenRefresh()
|
tokenRefresh()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -182,8 +255,27 @@ const AuthProvider = ({ children }) => {
|
|||||||
}, [expiresAt, authenticated, notificationApi, refreshToken])
|
}, [expiresAt, authenticated, notificationApi, refreshToken])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuthStatus()
|
if (initialized == false) {
|
||||||
}, [checkAuthStatus])
|
const authCode =
|
||||||
|
new URLSearchParams(location.search).get('authCode') || null
|
||||||
|
if (authCode != null) {
|
||||||
|
getLoginToken(authCode)
|
||||||
|
if (window && window.history && window.location) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
if (url.searchParams.has('authCode')) {
|
||||||
|
url.searchParams.delete('authCode')
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
document.title,
|
||||||
|
url.pathname + url.search
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInitialized(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [checkAuthStatus, location.search, getLoginToken, initialized])
|
||||||
|
|
||||||
if (authError) {
|
if (authError) {
|
||||||
return <AppError message={authError} showBack={false} />
|
return <AppError message={authError} showBack={false} />
|
||||||
@ -197,6 +289,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
value={{
|
value={{
|
||||||
authenticated,
|
authenticated,
|
||||||
loginWithSSO,
|
loginWithSSO,
|
||||||
|
getLoginToken,
|
||||||
token,
|
token,
|
||||||
loading,
|
loading,
|
||||||
userProfile,
|
userProfile,
|
||||||
@ -263,6 +356,19 @@ const AuthProvider = ({ children }) => {
|
|||||||
You need to be logged in to access FarmControl. Please log in with
|
You need to be logged in to access FarmControl. Please log in with
|
||||||
tombutcher.work to continue.
|
tombutcher.work to continue.
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<ExclamationOctogonIcon />
|
||||||
|
Loading...
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
open={loading}
|
||||||
|
style={{ maxWidth: 200, top: '50%', transform: 'translateY(-50%)' }}
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
footer={false}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/components/Dashboard/context/ElectronContext.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { createContext, useEffect, useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
// Only available in Electron renderer
|
||||||
|
const electron = window.require ? window.require('electron') : null
|
||||||
|
const ipcRenderer = electron ? electron.ipcRenderer : null
|
||||||
|
|
||||||
|
// Utility to check if running in Electron
|
||||||
|
export function isElectron() {
|
||||||
|
// Renderer process
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.process &&
|
||||||
|
window.process.type === 'renderer'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Main process
|
||||||
|
if (
|
||||||
|
typeof process !== 'undefined' &&
|
||||||
|
process.versions &&
|
||||||
|
!!process.versions.electron
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// User agent
|
||||||
|
if (
|
||||||
|
typeof navigator === 'object' &&
|
||||||
|
typeof navigator.userAgent === 'string' &&
|
||||||
|
navigator.userAgent.indexOf('Electron') >= 0
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const ElectronContext = createContext()
|
||||||
|
|
||||||
|
const ElectronProvider = ({ children }) => {
|
||||||
|
const [platform, setPlatform] = useState('unknown')
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
|
const [electronAvailable] = useState(isElectron())
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Function to open external URL via Electron
|
||||||
|
const openExternalUrl = (url) => {
|
||||||
|
if (electronAvailable && ipcRenderer) {
|
||||||
|
ipcRenderer.invoke('open-external-url', url)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ipcRenderer) return
|
||||||
|
|
||||||
|
// Get initial platform
|
||||||
|
ipcRenderer.invoke('os-info').then((info) => {
|
||||||
|
if (info && info.platform) setPlatform(info.platform)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get initial window state
|
||||||
|
ipcRenderer.invoke('window-state').then((state) => {
|
||||||
|
if (state && typeof state.isMaximized === 'boolean')
|
||||||
|
setIsMaximized(state.isMaximized)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for window state changes
|
||||||
|
const windowStateHandler = (event, state) => {
|
||||||
|
if (state && typeof state.isMaximized === 'boolean')
|
||||||
|
setIsMaximized(state.isMaximized)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('window-state', windowStateHandler)
|
||||||
|
|
||||||
|
// Listen for navigate
|
||||||
|
const navigateHandler = (event, url) => {
|
||||||
|
console.log('Navigating to:', url)
|
||||||
|
navigate(url)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('navigate', navigateHandler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('navigate', navigateHandler)
|
||||||
|
ipcRenderer.removeListener('window-state', windowStateHandler)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Window control handler
|
||||||
|
const handleWindowControl = (action) => {
|
||||||
|
if (electronAvailable && ipcRenderer) {
|
||||||
|
ipcRenderer.send('window-control', action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElectronContext.Provider
|
||||||
|
value={{
|
||||||
|
platform,
|
||||||
|
isMaximized,
|
||||||
|
isElectron: electronAvailable,
|
||||||
|
handleWindowControl,
|
||||||
|
openExternalUrl
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ElectronContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectronProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ElectronContext, ElectronProvider }
|
||||||
7
src/components/Icons/ContractIcon.jsx
Normal file
@ -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) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default ContractIcon
|
||||||
7
src/components/Icons/ExpandIcon.jsx
Normal file
@ -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) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default ExpandIcon
|
||||||
7
src/components/Icons/HostIcon.jsx
Normal file
@ -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) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default HostIcon
|
||||||
7
src/components/Icons/MinusIcon.jsx
Normal file
@ -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) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default MinusIcon
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { Printer } from './models/Printer.js'
|
import { Printer } from './models/Printer.js'
|
||||||
|
import { Host } from './models/Host.js'
|
||||||
import { Filament } from './models/Filament.js'
|
import { Filament } from './models/Filament.js'
|
||||||
import { Spool } from './models/Spool'
|
import { Spool } from './models/Spool'
|
||||||
import { GCodeFile } from './models/GCodeFile'
|
import { GCodeFile } from './models/GCodeFile'
|
||||||
@ -21,6 +22,7 @@ import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
|
|||||||
|
|
||||||
export const objectModels = [
|
export const objectModels = [
|
||||||
Printer,
|
Printer,
|
||||||
|
Host,
|
||||||
Filament,
|
Filament,
|
||||||
Spool,
|
Spool,
|
||||||
GCodeFile,
|
GCodeFile,
|
||||||
@ -44,6 +46,7 @@ export const objectModels = [
|
|||||||
// Re-export individual models for direct access
|
// Re-export individual models for direct access
|
||||||
export {
|
export {
|
||||||
Printer,
|
Printer,
|
||||||
|
Host,
|
||||||
Filament,
|
Filament,
|
||||||
Spool,
|
Spool,
|
||||||
GCodeFile,
|
GCodeFile,
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export const Filament = {
|
|||||||
],
|
],
|
||||||
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'],
|
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor', 'vendor._id'],
|
||||||
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
|
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
|
||||||
|
group: ['diameter', 'type', 'vendor'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const GCodeFile = {
|
|||||||
],
|
],
|
||||||
filters: ['_id', 'name', 'updatedAt'],
|
filters: ['_id', 'name', 'updatedAt'],
|
||||||
sorters: ['name', 'createdAt', 'updatedAt'],
|
sorters: ['name', 'createdAt', 'updatedAt'],
|
||||||
|
group: ['filament'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
90
src/database/models/Host.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -74,7 +74,7 @@ export const Job = {
|
|||||||
label: 'Quantity',
|
label: 'Quantity',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
columnWidth: 125,
|
columnWidth: 125,
|
||||||
readOnly: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const Printer = {
|
|||||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||||
filters: ['name', '_id', 'state', 'tags'],
|
filters: ['name', '_id', 'state', 'tags'],
|
||||||
sorters: ['name', 'state', 'connectedAt'],
|
sorters: ['name', 'state', 'connectedAt'],
|
||||||
|
group: ['tags'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -82,12 +83,6 @@ export const Printer = {
|
|||||||
objectType: 'vendor',
|
objectType: 'vendor',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'moonraker.host',
|
|
||||||
label: 'Host',
|
|
||||||
type: 'text',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'vendor._id',
|
name: 'vendor._id',
|
||||||
label: 'Vendor ID',
|
label: 'Vendor ID',
|
||||||
@ -96,6 +91,12 @@ export const Printer = {
|
|||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'moonraker.host',
|
||||||
|
label: 'Host',
|
||||||
|
type: 'text',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'moonraker.port',
|
name: 'moonraker.port',
|
||||||
label: 'Port',
|
label: 'Port',
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const SubJob = {
|
|||||||
icon: SubJobIcon,
|
icon: SubJobIcon,
|
||||||
actions: [],
|
actions: [],
|
||||||
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
|
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
|
||||||
filters: ['state', '_id'],
|
filters: ['state', '_id', 'job._id', 'printer._id'],
|
||||||
sorters: ['createdAt', 'state'],
|
sorters: ['createdAt', 'state'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export const Vendor = {
|
|||||||
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
||||||
filters: ['name', '_id', 'country', 'email'],
|
filters: ['name', '_id', 'country', 'email'],
|
||||||
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
||||||
|
group: ['country'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||