Added more icons, ui tweaks etc.

This commit is contained in:
Tom Butcher 2025-06-01 22:24:00 +01:00
parent 792d2e1c1c
commit 2ccf770525
88 changed files with 5551 additions and 4897 deletions

55
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,55 @@
node {
env.NODE_ENV = 'production'
try {
stage('Checkout') {
checkout scm
}
stage('Setup Node.js') {
nodejs(nodeJSInstallationName: 'Node23') {
sh 'node -v'
sh 'npm -v'
}
}
stage('Install Dependencies') {
nodejs(nodeJSInstallationName: 'Node23') {
sh 'npm ci --include=dev'
}
}
stage('Build') {
nodejs(nodeJSInstallationName: 'Node23') {
sh 'npm run build'
sh 'ls -la build || echo "Build directory not found"'
}
}
stage('Verify Build') {
sh 'test -d build || (echo "Build directory does not exist" && exit 1)'
}
stage('Deploy to printer1') {
def remote = [:]
remote.name = 'farmcontrolserver'
remote.host = 'farmcontrol.tombutcher.local'
remote.user = 'ci'
remote.password = 'ci'
remote.allowAnyHosts = true
// Copy the build directory to the remote server
sshPut remote: remote, from: 'build/*', into: '/srv/farmcontrol-server/'
// Restart the service using sudo
sshCommand remote: remote, command: 'sudo /bin/systemctl restart farmcontrol-server.service'
}
echo 'Pipeline completed successfully!'
} catch (Exception e) {
echo 'Pipeline failed!'
throw e
} finally {
cleanWs()
}
}

7662
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,30 +3,44 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@simplewebauthn/browser": "^10.0.0",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@simplewebauthn/browser": "^13.1.0",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.5.0",
"antd": "^5.25.1",
"@tsparticles/slim": "^3.8.1",
"antd": "^5.25.4",
"antd-style": "^3.7.1",
"axios": "*",
"axios": "^1.9.0",
"country-list": "^2.3.0",
"dotenv": "^16.5.0",
"gcode-preview": "^2.17.0",
"keycloak-js": "^26.1.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"gcode-preview": "^2.18.0",
"keycloak-js": "^26.2.0",
"log4js": "^6.9.1",
"moment": "*",
"prettier": "^3.5.3",
"prettier-eslint": "^16.4.2",
"prop-types": "^15.8.1",
"react": "*",
"react": "^18.3.1",
"react-country-flag": "^3.1.0",
"react-dom": "*",
"react-dom": "^18.3.1",
"react-router-dom": "*",
"react-scripts": "*",
"react-stl-viewer": "^2.5.0",
"socket.io-client": "*",
"standard": "^17.1.2",
"styled-components": "*",
"three": "^0.166.1",
"tsparticles": "^3.5.0",
"svgo": "^3.3.2",
"svgo-loader": "^4.0.0",
"three": "^0.177.0",
"tsparticles": "^3.8.1",
"virtualizedtableforantd4": "^1.3.1",
"web-vitals": "*"
"web-vitals": "*",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
},
"scripts": {
"dev": "react-scripts start",
@ -54,14 +68,15 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"prettier": "^3.3.3",
"prettier-eslint": "^16.3.0",
"standard": "^17.1.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"prettier": "^3.5.3",
"prettier-eslint": "^16.4.2",
"standard": "^17.1.2",
"svgo": "^3.3.2",
"svgo-loader": "^4.0.0",
"webpack": "^5.99.9",

View File

@ -5,7 +5,7 @@ import {
Route,
Navigate
} from 'react-router-dom'
import { App, ConfigProvider, theme } from 'antd'
import { App, ConfigProvider } from 'antd'
import AuthLayout from './components/Auth/AuthLayout.jsx'
import ProductionOverview from './components/Dashboard/Production/ProductionOverview'
@ -36,6 +36,8 @@ import Materials from './components/Dashboard/Management/Materials'
import FilamentStocks from './components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from './components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from './components/Dashboard/Inventory/PartStocks.jsx'
import StockAudits from './components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from './components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
@ -46,27 +48,18 @@ import './App.css'
import { SocketProvider } from './components/Dashboard/context/SocketContext.js'
import { AuthProvider } from './components/Auth/AuthContext.js'
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js'
import StockEvents from './components/Dashboard/Inventory/StockEvents.jsx'
import Settings from './components/Dashboard/Management/Settings'
import {
ThemeProvider,
useThemeContext
} from './components/Dashboard/context/ThemeContext'
const AppContent = () => {
const { themeConfig } = useThemeContext()
const FarmControlApp = () => {
return (
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#007AFF',
colorSuccess: '#32D74B',
colorWarning: '#FF9F0A',
colorInfo: '#007AFF',
colorLink: '#5AC8F5',
borderRadius: '10px'
},
components: {
Layout: {
headerBg: '#141414'
}
}
}}
>
<ConfigProvider theme={themeConfig}>
<App>
<AuthProvider>
<SocketProvider>
@ -135,6 +128,14 @@ const FarmControlApp = () => {
path='inventory/filamentstocks/info'
element={<FilamentStockInfo />}
/>
<Route
path='inventory/partstocks'
element={<PartStocks />}
/>
<Route
path='inventory/stockevents'
element={<StockEvents />}
/>
<Route
path='inventory/stockaudits'
element={<StockAudits />}
@ -172,6 +173,7 @@ const FarmControlApp = () => {
path='management/materials'
element={<Materials />}
/>
<Route path='management/settings' element={<Settings />} />
</Route>
</Routes>
</Router>
@ -183,4 +185,12 @@ const FarmControlApp = () => {
)
}
const FarmControlApp = () => {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
)
}
export default FarmControlApp

Binary file not shown.

View 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="M0 0h48.569v59.324H0z" style="fill-opacity:0" transform="translate(8.257 3)scale(.97769)"/><path d="M24.188 0c-2.013 0-3.411 1.401-3.411 3.453v38.822l.374 10.184 2.054-1.024-10.428-11.629-7.086-7c-.592-.618-1.455-.986-2.424-.986C1.384 31.82 0 33.227 0 35.129c0 .913.354 1.733 1.066 2.47l20.578 20.635a3.515 3.515 0 0 0 5.088 0l20.604-20.635c.717-.737 1.046-1.557 1.046-2.47 0-1.902-1.384-3.309-3.267-3.309-.969 0-1.812.368-2.424.986l-7.097 7-10.448 11.629 2.073 1.024.375-10.184V3.453C27.594 1.401 26.201 0 24.188 0" style="fill-rule:nonzero" transform="translate(8.349 3)scale(.97769)"/></svg>

After

Width:  |  Height:  |  Size: 770 B

View File

@ -0,0 +1,10 @@
<?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.977687,0,0,0.977687,8.25722,3)">
<rect x="0" y="0" width="48.569" height="59.324" style="fill-opacity:0;"/>
<g transform="matrix(1,0,0,1,0.09375,0)">
<path d="M24.188,0C22.175,0 20.777,1.401 20.777,3.453L20.777,42.275L21.151,52.459L23.205,51.435L12.777,39.806L5.691,32.806C5.099,32.188 4.236,31.82 3.267,31.82C1.384,31.82 0,33.227 0,35.129C0,36.042 0.354,36.862 1.066,37.599L21.644,58.234C22.323,58.944 23.244,59.324 24.188,59.324C25.133,59.324 26.048,58.944 26.732,58.234L47.336,37.599C48.053,36.862 48.382,36.042 48.382,35.129C48.382,33.227 46.998,31.82 45.115,31.82C44.146,31.82 43.303,32.188 42.691,32.806L35.594,39.806L25.146,51.435L27.219,52.459L27.594,42.275L27.594,3.453C27.594,1.401 26.201,0 24.188,0Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

View 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="M0 24.194c0 .944.411 1.86 1.121 2.544L21.73 47.31c.738.718 1.557 1.072 2.436 1.072 1.937 0 3.369-1.384 3.369-3.267 0-.969-.394-1.837-1.012-2.424l-7-7.086L7.879 24.971l-1.112 1.998 10.811.636h38.018c2.053 0 3.448-1.398 3.448-3.411s-1.395-3.411-3.448-3.411H17.578l-10.811.636 1.112 2.029 11.644-10.665 7-7.092a3.4 3.4 0 0 0 1.012-2.424C27.535 1.384 26.103 0 24.166 0c-.879 0-1.698.329-2.487 1.123L1.121 21.65C.411 22.334 0 23.249 0 24.194" style="fill-rule:nonzero" transform="translate(3 8.237)scale(.98232)"/></svg>

After

Width:  |  Height:  |  Size: 691 B

View 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.982323,0,0,0.982323,3,8.23672)">
<path d="M0,24.194C0,25.138 0.411,26.054 1.121,26.738L21.73,47.31C22.468,48.028 23.287,48.382 24.166,48.382C26.103,48.382 27.535,46.998 27.535,45.115C27.535,44.146 27.141,43.278 26.523,42.691L19.523,35.605L7.879,24.971L6.767,26.969L17.578,27.605L55.596,27.605C57.649,27.605 59.044,26.207 59.044,24.194C59.044,22.181 57.649,20.783 55.596,20.783L17.578,20.783L6.767,21.419L7.879,23.448L19.523,12.783L26.523,5.691C27.141,5.073 27.535,4.236 27.535,3.267C27.535,1.384 26.103,0 24.166,0C23.287,0 22.468,0.329 21.679,1.123L1.121,21.65C0.411,22.334 0,23.249 0,24.194Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View 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="M0 24.194c0 .944.411 1.86 1.121 2.544L21.73 47.31c.738.718 1.557 1.072 2.436 1.072 1.937 0 3.369-1.384 3.369-3.267 0-.969-.394-1.837-1.012-2.424l-7-7.086L7.879 24.971l-1.112 1.998 10.811.636h38.018c2.053 0 3.448-1.398 3.448-3.411s-1.395-3.411-3.448-3.411H17.578l-10.811.636 1.112 2.029 11.644-10.665 7-7.092a3.4 3.4 0 0 0 1.012-2.424C27.535 1.384 26.103 0 24.166 0c-.879 0-1.698.329-2.487 1.123L1.121 21.65C.411 22.334 0 23.249 0 24.194" style="fill-rule:nonzero" transform="matrix(-.98232 0 0 .98232 61 8.237)"/></svg>

After

Width:  |  Height:  |  Size: 695 B

View 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.982323,0,0,0.982323,61,8.23672)">
<path d="M0,24.194C0,25.138 0.411,26.054 1.121,26.738L21.73,47.31C22.468,48.028 23.287,48.382 24.166,48.382C26.103,48.382 27.535,46.998 27.535,45.115C27.535,44.146 27.141,43.278 26.523,42.691L19.523,35.605L7.879,24.971L6.767,26.969L17.578,27.605L55.596,27.605C57.649,27.605 59.044,26.207 59.044,24.194C59.044,22.181 57.649,20.783 55.596,20.783L17.578,20.783L6.767,21.419L7.879,23.448L19.523,12.783L26.523,5.691C27.141,5.073 27.535,4.236 27.535,3.267C27.535,1.384 26.103,0 24.166,0C23.287,0 22.468,0.329 21.679,1.123L1.121,21.65C0.411,22.334 0,23.249 0,24.194Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View 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="M0 0h48.569v59.324H0z" style="fill-opacity:0" transform="translate(8.257 3)scale(.97769)"/><path d="M24.188 0c-2.013 0-3.411 1.401-3.411 3.453v38.822l.374 10.184 2.054-1.024-10.428-11.629-7.086-7c-.592-.618-1.455-.986-2.424-.986C1.384 31.82 0 33.227 0 35.129c0 .913.354 1.733 1.066 2.47l20.578 20.635a3.515 3.515 0 0 0 5.088 0l20.604-20.635c.717-.737 1.046-1.557 1.046-2.47 0-1.902-1.384-3.309-3.267-3.309-.969 0-1.812.368-2.424.986l-7.097 7-10.448 11.629 2.073 1.024.375-10.184V3.453C27.594 1.401 26.201 0 24.188 0" style="fill-rule:nonzero" transform="matrix(.97769 0 0 -.97769 8.349 61)"/></svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@ -0,0 +1,10 @@
<?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.977687,0,0,0.977687,8.25722,3)">
<rect x="0" y="0" width="48.569" height="59.324" style="fill-opacity:0;"/>
<g transform="matrix(1,0,0,-1,0.09375,59.3237)">
<path d="M24.188,0C22.175,0 20.777,1.401 20.777,3.453L20.777,42.275L21.151,52.459L23.205,51.435L12.777,39.806L5.691,32.806C5.099,32.188 4.236,31.82 3.267,31.82C1.384,31.82 0,33.227 0,35.129C0,36.042 0.354,36.862 1.066,37.599L21.644,58.234C22.323,58.944 23.244,59.324 24.188,59.324C25.133,59.324 26.048,58.944 26.732,58.234L47.336,37.599C48.053,36.862 48.382,36.042 48.382,35.129C48.382,33.227 46.998,31.82 45.115,31.82C44.146,31.82 43.303,32.188 42.691,32.806L35.594,39.806L25.146,51.435L27.219,52.459L27.594,42.275L27.594,3.453C27.594,1.401 26.201,0 24.188,0Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99133)"/><path d="M28.899 47.141c1.121 0 2.097-.557 2.762-1.587l13.977-21.833c.395-.676.773-1.403.773-2.107 0-1.564-1.385-2.605-2.855-2.605-.917 0-1.765.545-2.427 1.575L28.791 40.406 22.983 33c-.767-.989-1.505-1.295-2.447-1.295-1.507 0-2.722 1.223-2.722 2.756 0 .755.293 1.433.809 2.112l7.389 9.003c.837 1.065 1.754 1.565 2.887 1.565" style="fill-rule:nonzero" transform="scale(.99133)"/></svg>

After

Width:  |  Height:  |  Size: 857 B

View File

@ -0,0 +1,8 @@
<?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.991326,0,0,0.991326,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M28.899,47.141C30.02,47.141 30.996,46.584 31.661,45.554L45.638,23.721C46.033,23.045 46.411,22.318 46.411,21.614C46.411,20.05 45.026,19.009 43.556,19.009C42.639,19.009 41.791,19.554 41.129,20.584L28.791,40.406L22.983,33C22.216,32.011 21.478,31.705 20.536,31.705C19.029,31.705 17.814,32.928 17.814,34.461C17.814,35.216 18.107,35.894 18.623,36.573L26.012,45.576C26.849,46.641 27.766,47.141 28.899,47.141Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

View File

@ -1 +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="M21.134 55.708c1.625 0 2.907-.679 3.798-2.037L54.306 7.879c.658-1.03.921-1.892.921-2.74 0-2.152-1.513-3.636-3.687-3.636-1.542 0-2.438.518-3.374 1.987L21.009 46.634 7.015 28.64c-.911-1.206-1.866-1.711-3.216-1.711C1.567 26.929 0 28.491 0 30.648c0 .927.346 1.863 1.126 2.81l16.19 20.257c1.078 1.348 2.244 1.993 3.818 1.993" style="fill-rule:nonzero" transform="translate(0 -1.149)scale(1.15885)"/></svg>
<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="M21.134 55.708c1.625 0 2.907-.679 3.798-2.037L54.306 7.879c.658-1.03.921-1.892.921-2.74 0-2.152-1.513-3.636-3.687-3.636-1.542 0-2.438.518-3.374 1.987L21.009 46.634 7.015 28.64c-.911-1.206-1.866-1.711-3.216-1.711C1.567 26.929 0 28.491 0 30.648c0 .927.346 1.863 1.126 2.81l16.19 20.257c1.078 1.348 2.244 1.993 3.818 1.993" style="fill-rule:nonzero" transform="translate(3 1.959)scale(1.0502)"/></svg>

Before

Width:  |  Height:  |  Size: 576 B

After

Width:  |  Height:  |  Size: 574 B

View File

@ -1,7 +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(1.15885,0,0,1.15885,4.88498e-15,-1.14877)">
<g transform="matrix(1.0502,0,0,1.0502,3,1.95893)">
<path d="M21.134,55.708C22.759,55.708 24.041,55.029 24.932,53.671L54.306,7.879C54.964,6.849 55.227,5.987 55.227,5.139C55.227,2.987 53.714,1.503 51.54,1.503C49.998,1.503 49.102,2.021 48.166,3.49L21.009,46.634L7.015,28.64C6.104,27.434 5.149,26.929 3.799,26.929C1.567,26.929 0,28.491 0,30.648C0,31.575 0.346,32.511 1.126,33.458L17.316,53.715C18.394,55.063 19.56,55.708 21.134,55.708Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 947 B

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

View 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="M23.142 62.87h19.121c4.042 0 6.204-1.354 8.373-3.731l12.046-13.425c2.144-2.408 2.724-4.012 2.724-7.291v-13.95c0-3.305-.681-4.805-2.858-7.291L50.636 3.756C48.484 1.363 46.305 0 42.263 0H23.142c-4.041 0-6.229 1.38-8.367 3.756L2.729 17.182C.554 19.589 0 21.168 0 24.473v13.95c0 3.279.554 4.883 2.729 7.291l12.046 13.425c2.138 2.377 4.326 3.731 8.367 3.731m1.519-6.035c-2.978 0-3.947-.693-5.575-2.489L8.249 42.396c-1.213-1.381-1.506-2.23-1.506-4.522V25.016c0-2.292.293-3.141 1.506-4.516L19.086 8.55c1.628-1.822 2.597-2.489 5.575-2.489H40.75c2.973 0 3.916.667 5.57 2.489L57.131 20.5c1.245 1.375 1.532 2.224 1.532 4.516v12.858c0 2.292-.287 3.141-1.532 4.522L46.32 54.346c-1.654 1.796-2.597 2.489-5.57 2.489z" style="fill-rule:nonzero" transform="translate(0 1.24)scale(.9785)"/><path d="M32.701 36.638c1.692 0 2.679-.945 2.733-2.75l.46-15.2c.059-1.814-1.326-3.125-3.224-3.125-1.929 0-3.257 1.286-3.198 3.099l.454 15.237c.054 1.768 1.053 2.739 2.775 2.739m0 10.353c2.004 0 3.651-1.454 3.651-3.424 0-1.964-1.621-3.43-3.651-3.43-2.003 0-3.642 1.486-3.642 3.43s1.664 3.424 3.642 3.424" style="fill-rule:nonzero" transform="translate(0 1.24)scale(.9785)"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,8 @@
<?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.978509,0,0,0.978509,0,1.24055)">
<path d="M23.142,62.87L42.263,62.87C46.305,62.87 48.467,61.516 50.636,59.139L62.682,45.714C64.826,43.306 65.406,41.702 65.406,38.423L65.406,24.473C65.406,21.168 64.725,19.668 62.548,17.182L50.636,3.756C48.484,1.363 46.305,0 42.263,0L23.142,0C19.101,0 16.913,1.38 14.775,3.756L2.729,17.182C0.554,19.589 0,21.168 0,24.473L0,38.423C0,41.702 0.554,43.306 2.729,45.714L14.775,59.139C16.913,61.516 19.101,62.87 23.142,62.87ZM24.661,56.835C21.683,56.835 20.714,56.142 19.086,54.346L8.249,42.396C7.036,41.015 6.743,40.166 6.743,37.874L6.743,25.016C6.743,22.724 7.036,21.875 8.249,20.5L19.086,8.55C20.714,6.728 21.683,6.061 24.661,6.061L40.75,6.061C43.723,6.061 44.666,6.728 46.32,8.55L57.131,20.5C58.376,21.875 58.663,22.724 58.663,25.016L58.663,37.874C58.663,40.166 58.376,41.015 57.131,42.396L46.32,54.346C44.666,56.142 43.723,56.835 40.75,56.835L24.661,56.835Z" style="fill-rule:nonzero;"/>
<path d="M32.701,36.638C34.393,36.638 35.38,35.693 35.434,33.888L35.894,18.688C35.953,16.874 34.568,15.563 32.67,15.563C30.741,15.563 29.413,16.849 29.472,18.662L29.926,33.899C29.98,35.667 30.979,36.638 32.701,36.638ZM32.701,46.991C34.705,46.991 36.352,45.537 36.352,43.567C36.352,41.603 34.731,40.137 32.701,40.137C30.698,40.137 29.059,41.623 29.059,43.567C29.059,45.511 30.723,46.991 32.701,46.991Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

View 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="M0 0h76.494v67.007H0z" style="fill-opacity:0" transform="translate(3 6.597)scale(.75823)"/><path d="M29.122 62.416h18.063V42.667c0-1.46-.949-2.409-2.409-2.409H31.562c-1.46 0-2.44.949-2.44 2.409zM3.194 34.517c.97 0 1.835-.497 2.598-1.129l31.04-26.062c.422-.349.896-.539 1.319-.539.453 0 .902.19 1.324.539L70.54 33.388c.738.632 1.603 1.129 2.573 1.129 1.98 0 3.194-1.376 3.194-2.989 0-.871-.383-1.79-1.225-2.465L42.519 1.725C41.157.579 39.651 0 38.151 0c-1.495 0-3.002.579-4.363 1.725L1.225 29.063C.408 29.738 0 30.657 0 31.528c0 1.613 1.214 2.989 3.194 2.989m54.794-18.118 8.828 7.424V9.155c0-1.409-.897-2.306-2.3-2.306H60.3c-1.377 0-2.312.897-2.312 2.306zM17.026 66.945h42.281c4.73 0 7.509-2.728 7.509-7.328V24.823l-6.122-4.165v37.113c0 1.975-1.077 3.052-3.001 3.052H18.639c-1.949 0-3.026-1.077-3.026-3.052V20.683l-6.122 4.14v34.794c0 4.626 2.779 7.328 7.535 7.328" style="fill-rule:nonzero" transform="translate(3 6.597)scale(.75823)"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<?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.758226,0,0,0.758226,3,6.59657)">
<rect x="0" y="0" width="76.494" height="67.007" style="fill-opacity:0;"/>
<path d="M29.122,62.416L47.185,62.416L47.185,42.667C47.185,41.207 46.236,40.258 44.776,40.258L31.562,40.258C30.102,40.258 29.122,41.207 29.122,42.667L29.122,62.416ZM3.194,34.517C4.164,34.517 5.029,34.02 5.792,33.388L36.832,7.326C37.254,6.977 37.728,6.787 38.151,6.787C38.604,6.787 39.053,6.977 39.475,7.326L70.54,33.388C71.278,34.02 72.143,34.517 73.113,34.517C75.093,34.517 76.307,33.141 76.307,31.528C76.307,30.657 75.924,29.738 75.082,29.063L42.519,1.725C41.157,0.579 39.651,0 38.151,0C36.656,0 35.149,0.579 33.788,1.725L1.225,29.063C0.408,29.738 0,30.657 0,31.528C0,33.141 1.214,34.517 3.194,34.517ZM57.988,16.399L66.816,23.823L66.816,9.155C66.816,7.746 65.919,6.849 64.516,6.849L60.3,6.849C58.923,6.849 57.988,7.746 57.988,9.155L57.988,16.399ZM17.026,66.945L59.307,66.945C64.037,66.945 66.816,64.217 66.816,59.617L66.816,24.823L60.694,20.658L60.694,57.771C60.694,59.746 59.617,60.823 57.693,60.823L18.639,60.823C16.69,60.823 15.613,59.746 15.613,57.771L15.613,20.683L9.491,24.823L9.491,59.617C9.491,64.243 12.27,66.945 17.026,66.945Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99124)"/><path d="M23.358 44.569h3.231c1.607 0 2.388-.889 2.388-2.198V22.138c0-1.278-.781-2.172-2.388-2.172h-3.231c-1.588 0-2.363.894-2.363 2.172v20.233c0 1.309.775 2.198 2.363 2.198m14.619 0h3.231c1.556 0 2.337-.889 2.337-2.198V22.138c0-1.278-.781-2.172-2.337-2.172h-3.231c-1.613 0-2.388.894-2.388 2.172v20.233c0 1.309.775 2.198 2.388 2.198" style="fill-rule:nonzero" transform="scale(.99124)"/></svg>

After

Width:  |  Height:  |  Size: 865 B

View File

@ -0,0 +1,8 @@
<?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.99124,0,0,0.99124,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M23.358,44.569L26.589,44.569C28.196,44.569 28.977,43.68 28.977,42.371L28.977,22.138C28.977,20.86 28.196,19.966 26.589,19.966L23.358,19.966C21.77,19.966 20.995,20.86 20.995,22.138L20.995,42.371C20.995,43.68 21.77,44.569 23.358,44.569ZM37.977,44.569L41.208,44.569C42.764,44.569 43.545,43.68 43.545,42.371L43.545,22.138C43.545,20.86 42.764,19.966 41.208,19.966L37.977,19.966C36.364,19.966 35.589,20.86 35.589,22.138L35.589,42.371C35.589,43.68 36.364,44.569 37.977,44.569Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99133)"/><path d="M26.779 44.527 44.128 34.26c1.548-.896 1.517-3.035 0-3.951L26.779 20.042c-1.602-.933-3.625-.182-3.625 1.664v21.151c0 1.826 1.932 2.68 3.625 1.67" style="fill-rule:nonzero" transform="scale(.99133)"/></svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@ -0,0 +1,8 @@
<?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.991326,0,0,0.991326,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M26.779,44.527L44.128,34.26C45.676,33.364 45.645,31.225 44.128,30.309L26.779,20.042C25.177,19.109 23.154,19.86 23.154,21.706L23.154,42.857C23.154,44.683 25.086,45.537 26.779,44.527Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99124)"/><path d="M31.573 38.518c1.703 0 2.818-1.011 2.897-2.275.006-.119.011-.275.023-.369.084-1.621 1.168-2.712 3.21-4.043 3.093-2.011 5.08-3.801 5.08-7.444 0-5.193-4.681-8.175-10.23-8.175-5.358 0-8.985 2.462-9.941 5.387a4.8 4.8 0 0 0-.289 1.622c0 1.507 1.166 2.437 2.428 2.437 1.136 0 1.919-.489 2.532-1.3l.49-.655c1.06-1.694 2.538-2.616 4.424-2.616 2.534 0 4.22 1.501 4.22 3.617 0 1.99-1.306 2.954-3.946 4.793-2.228 1.551-3.859 3.157-3.859 5.994v.345c0 1.765 1.069 2.682 2.961 2.682m-.045 9.566c2.003 0 3.65-1.46 3.65-3.43 0-1.965-1.621-3.43-3.65-3.43s-3.668 1.485-3.668 3.43c0 1.944 1.664 3.43 3.668 3.43" style="fill-rule:nonzero" transform="scale(.99124)"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<?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.99124,0,0,0.99124,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M31.573,38.518C33.276,38.518 34.391,37.507 34.47,36.243C34.476,36.124 34.481,35.968 34.493,35.874C34.577,34.253 35.661,33.162 37.703,31.831C40.796,29.82 42.783,28.03 42.783,24.387C42.783,19.194 38.102,16.212 32.553,16.212C27.195,16.212 23.568,18.674 22.612,21.599C22.427,22.128 22.323,22.656 22.323,23.221C22.323,24.728 23.489,25.658 24.751,25.658C25.887,25.658 26.67,25.169 27.283,24.358L27.773,23.703C28.833,22.009 30.311,21.087 32.197,21.087C34.731,21.087 36.417,22.588 36.417,24.704C36.417,26.694 35.111,27.658 32.471,29.497C30.243,31.048 28.612,32.654 28.612,35.491L28.612,35.836C28.612,37.601 29.681,38.518 31.573,38.518ZM31.528,48.084C33.531,48.084 35.178,46.624 35.178,44.654C35.178,42.689 33.557,41.224 31.528,41.224C29.498,41.224 27.86,42.709 27.86,44.654C27.86,46.598 29.524,48.084 31.528,48.084Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1 +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="M26.391 65.119c.55 0 1.397-.216 2.189-.648 17.973-9.811 24.201-14.3 24.201-25.7V15.146c0-3.703-1.434-5.01-4.535-6.332-3.473-1.447-15.183-5.565-18.589-6.728-1.035-.332-2.217-.548-3.266-.548-1.05 0-2.232.273-3.241.548-3.406.972-15.141 5.307-18.615 6.728C1.46 10.11 0 11.443 0 15.146v23.625c0 11.4 6.254 15.863 24.201 25.7.818.432 1.639.648 2.19.648m0-6.562c-.45 0-.893-.165-1.803-.716C10.303 49.13 5.869 46.787 5.869 37.603V16.142c0-1.101.21-1.541 1.096-1.891 4.626-1.835 13.609-4.834 17.793-6.478.703-.264 1.188-.354 1.633-.354s.924.116 1.632.354c4.185 1.644 13.116 4.822 17.819 6.478.855.324 1.071.79 1.071 1.891v21.461c0 9.235-4.592 11.715-18.72 20.238-.884.545-1.353.716-1.802.716" style="fill-rule:nonzero" transform="translate(7.926 1.597)scale(.91223)"/><path d="M23.061 46.514c1.121 0 2.097-.556 2.741-1.555L39.8 23.094c.394-.644.773-1.376.773-2.106 0-1.533-1.38-2.58-2.855-2.58-.937 0-1.791.519-2.427 1.575L22.958 39.806l-5.839-7.407c-.741-.988-1.499-1.315-2.416-1.315-1.538 0-2.721 1.218-2.721 2.776 0 .729.287 1.434.803 2.112l7.369 9.004c.857 1.07 1.749 1.538 2.907 1.538" style="fill-rule:nonzero" transform="translate(7.926 1.597)scale(.91223)"/></svg>
<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="M32.468 59.712a.82.82 0 0 1-.383 1.454c-.938.098-1.869-.093-2.785-.611L8.262 48.803c-2.533-1.425-3.821-2.924-3.821-6.676V20.65c0-2.836 1.048-4.63 3.436-5.958L26.541 4.215c3.287-1.848 6.635-1.848 9.922 0l18.686 10.477c2.366 1.328 3.414 3.122 3.414 5.958a1.722 1.722 0 0 1-2.285 1.627 547 547 0 0 0-6.455-2.182l-.03-.009a.598.598 0 0 1-.12-1.093l.001-.001a.966.966 0 0 0 0-1.685L34.365 8.675c-1.958-1.119-3.778-1.088-5.709 0l-4.399 2.49 14.745 8.254a1.171 1.171 0 0 1-.195 2.129c-.808.278-1.655.572-2.494.868a2.8 2.8 0 0 1-2.292-.199l-14.747-8.283-7.418 4.216 14.428 8.09c.513.288.722.92.481 1.456-.321.751-.458 1.673-.458 2.836a.836.836 0 0 1-1.247.728L9.169 22.284v19.38c0 1.395.512 2.255 1.968 3.058l14.946 8.44a6.4 6.4 0 0 1 2.319 2.24c.927 1.528 2.279 2.914 4.066 4.31" style="fill-rule:nonzero"/><path d="M27.141 65.719c.64 0 1.547-.266 2.359-.688 18.313-9.531 24.781-14.5 24.781-25.89V16.016c0-4.453-1.484-6.25-5.375-7.922-3.093-1.297-14.453-5.125-17.359-6.078-1.375-.422-3.047-.688-4.406-.688-1.36 0-3.032.313-4.391.688-2.906.812-14.281 4.797-17.375 6.078C1.5 9.75 0 11.563 0 16.016v23.125c0 11.39 6.484 16.343 24.781 25.89.828.422 1.719.688 2.36.688m0-8.172c-.36 0-.703-.125-1.563-.656C11.563 48.25 7.469 46.297 7.469 37.953v-20.89c0-1.172.25-1.672 1.156-2.032 4.406-1.765 12.969-4.593 16.203-5.828 1.063-.344 1.688-.484 2.313-.484s1.234.156 2.312.484c3.235 1.235 11.766 4.172 16.219 5.828.875.344 1.141.86 1.141 2.032v20.89c0 8.485-4.532 10.875-18.11 18.938-.844.515-1.203.656-1.562.656" style="fill-rule:nonzero" transform="translate(28.594 21.134)scale(.65227)"/><path d="M23.891 46.484c1.281 0 2.437-.656 3.172-1.765L40.5 23.984c.484-.734.813-1.546.813-2.296 0-1.813-1.61-3.11-3.375-3.11-1.157 0-2.141.61-2.907 1.875L23.828 38.516l-5.219-6.407c-.781-.968-1.609-1.375-2.656-1.375-1.828 0-3.281 1.438-3.281 3.266 0 .859.297 1.594.953 2.422l6.969 8.344c.937 1.14 1.969 1.718 3.297 1.718" style="fill-rule:nonzero" transform="translate(28.594 21.134)scale(.65227)"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,8 +1,9 @@
<?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.912228,0,0,0.912228,7.92575,1.59687)">
<path d="M26.391,65.119C26.941,65.119 27.788,64.903 28.58,64.471C46.553,54.66 52.781,50.171 52.781,38.771L52.781,15.146C52.781,11.443 51.347,10.136 48.246,8.814C44.773,7.367 33.063,3.249 29.657,2.086C28.622,1.754 27.44,1.538 26.391,1.538C25.341,1.538 24.159,1.811 23.15,2.086C19.744,3.058 8.009,7.393 4.535,8.814C1.46,10.11 0,11.443 0,15.146L0,38.771C0,50.171 6.254,54.634 24.201,64.471C25.019,64.903 25.84,65.119 26.391,65.119ZM26.391,58.557C25.941,58.557 25.498,58.392 24.588,57.841C10.303,49.13 5.869,46.787 5.869,37.603L5.869,16.142C5.869,15.041 6.079,14.601 6.965,14.251C11.591,12.416 20.574,9.417 24.758,7.773C25.461,7.509 25.946,7.419 26.391,7.419C26.836,7.419 27.315,7.535 28.023,7.773C32.208,9.417 41.139,12.595 45.842,14.251C46.697,14.575 46.913,15.041 46.913,16.142L46.913,37.603C46.913,46.838 42.321,49.318 28.193,57.841C27.309,58.386 26.84,58.557 26.391,58.557Z" style="fill-rule:nonzero;"/>
<path d="M23.061,46.514C24.182,46.514 25.158,45.958 25.802,44.959L39.8,23.094C40.194,22.45 40.573,21.718 40.573,20.988C40.573,19.455 39.193,18.408 37.718,18.408C36.781,18.408 35.927,18.927 35.291,19.983L22.958,39.806L17.119,32.399C16.378,31.411 15.62,31.084 14.703,31.084C13.165,31.084 11.982,32.302 11.982,33.86C11.982,34.589 12.269,35.294 12.785,35.972L20.154,44.976C21.011,46.046 21.903,46.514 23.061,46.514Z" style="fill-rule:nonzero;"/>
<path d="M32.468,59.712C32.725,59.914 32.837,60.249 32.754,60.565C32.671,60.881 32.408,61.117 32.085,61.166C31.147,61.264 30.216,61.073 29.3,60.555L8.262,48.803C5.729,47.378 4.441,45.879 4.441,42.127L4.441,20.65C4.441,17.814 5.489,16.02 7.877,14.692L26.541,4.215C29.828,2.367 33.176,2.367 36.463,4.215L55.149,14.692C57.515,16.02 58.563,17.814 58.563,20.65L58.563,20.65C58.563,21.207 58.294,21.73 57.84,22.053C57.386,22.376 56.804,22.46 56.278,22.277C53.589,21.35 50.911,20.452 49.823,20.095C49.813,20.092 49.803,20.089 49.793,20.086C49.564,20.017 49.398,19.817 49.372,19.579C49.346,19.341 49.464,19.11 49.673,18.993C49.673,18.992 49.674,18.992 49.674,18.992C49.978,18.821 50.166,18.499 50.166,18.15C50.167,17.801 49.979,17.479 49.674,17.307C45.559,14.986 34.365,8.675 34.365,8.675C32.407,7.556 30.587,7.587 28.656,8.675L24.257,11.165C24.257,11.165 34.347,16.813 39.002,19.419C39.405,19.645 39.637,20.087 39.595,20.547C39.553,21.007 39.244,21.399 38.807,21.548C37.999,21.826 37.152,22.12 36.313,22.416C35.556,22.682 34.72,22.61 34.021,22.217C30.425,20.197 19.274,13.934 19.274,13.934L11.856,18.15C11.856,18.15 22.923,24.355 26.284,26.24C26.797,26.528 27.006,27.16 26.765,27.696C26.444,28.447 26.307,29.369 26.307,30.532L26.307,30.532C26.307,30.829 26.149,31.104 25.892,31.254C25.635,31.404 25.318,31.406 25.06,31.26C21.115,29.032 9.169,22.284 9.169,22.284L9.169,41.664C9.169,43.059 9.681,43.919 11.137,44.722C11.137,44.722 22.059,50.89 26.083,53.162C27.034,53.7 27.832,54.471 28.402,55.402C29.329,56.93 30.681,58.316 32.468,59.712Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.652268,0,0,0.652268,28.5941,21.1337)">
<path d="M27.141,65.719C27.781,65.719 28.688,65.453 29.5,65.031C47.813,55.5 54.281,50.531 54.281,39.141L54.281,16.016C54.281,11.563 52.797,9.766 48.906,8.094C45.813,6.797 34.453,2.969 31.547,2.016C30.172,1.594 28.5,1.328 27.141,1.328C25.781,1.328 24.109,1.641 22.75,2.016C19.844,2.828 8.469,6.813 5.375,8.094C1.5,9.75 0,11.563 0,16.016L0,39.141C0,50.531 6.484,55.484 24.781,65.031C25.609,65.453 26.5,65.719 27.141,65.719ZM27.141,57.547C26.781,57.547 26.438,57.422 25.578,56.891C11.563,48.25 7.469,46.297 7.469,37.953L7.469,17.063C7.469,15.891 7.719,15.391 8.625,15.031C13.031,13.266 21.594,10.438 24.828,9.203C25.891,8.859 26.516,8.719 27.141,8.719C27.766,8.719 28.375,8.875 29.453,9.203C32.688,10.438 41.219,13.375 45.672,15.031C46.547,15.375 46.813,15.891 46.813,17.063L46.813,37.953C46.813,46.438 42.281,48.828 28.703,56.891C27.859,57.406 27.5,57.547 27.141,57.547Z" style="fill-rule:nonzero;"/>
<path d="M23.891,46.484C25.172,46.484 26.328,45.828 27.063,44.719L40.5,23.984C40.984,23.25 41.313,22.438 41.313,21.688C41.313,19.875 39.703,18.578 37.938,18.578C36.781,18.578 35.797,19.188 35.031,20.453L23.828,38.516L18.609,32.109C17.828,31.141 17,30.734 15.953,30.734C14.125,30.734 12.672,32.172 12.672,34C12.672,34.859 12.969,35.594 13.625,36.422L20.594,44.766C21.531,45.906 22.563,46.484 23.891,46.484Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 64 64"><path d="M25 54.028a2.42 2.42 0 0 1-3.602 2.113L8.262 48.803c-2.533-1.425-3.821-2.924-3.821-6.676V20.65c0-2.836 1.048-4.63 3.436-5.958L26.541 4.215c3.287-1.848 6.635-1.848 9.922 0l18.686 10.477c2.366 1.328 3.414 3.122 3.414 5.958v1.987A2.363 2.363 0 0 1 56.2 25h-.002a2.363 2.363 0 0 1-2.363-2.363v-.353l-3.45 1.948a5.93 5.93 0 0 1-2.92.768h-6.99a5.93 5.93 0 0 1-2.915-.765L19.274 13.934l-7.418 4.216 13.894 7.79a2.01 2.01 0 0 1 .684 2.868 3.2 3.2 0 0 0-.421.629 1.97 1.97 0 0 1-2.707.789c-4.313-2.393-14.137-7.942-14.137-7.942v19.38c0 1.395.512 2.255 1.968 3.058l12.633 7.134A2.42 2.42 0 0 1 25 53.964zm18.972-31.826L24.257 11.165l4.399-2.49c1.931-1.088 3.751-1.119 5.709 0L51.17 18.15z"/><g fill-rule="nonzero"><path d="M35.462 64h22.076C61.721 64 64 61.74 64 57.584V35.425C64 31.28 61.72 29 57.538 29H35.462C31.288 29 29 31.28 29 35.425v22.159C29 61.739 31.288 64 35.462 64m.512-4.723c-1.473 0-2.25-.714-2.25-2.27V36.011c0-1.556.777-2.28 2.25-2.28h21.052c1.464 0 2.251.724 2.251 2.28v20.996c0 1.556-.787 2.27-2.251 2.27z"/><path d="m40.185 38.134-4.339 4.476c-.247.256-.42.632-.42.961 0 .77.576 1.336 1.354 1.336.357 0 .677-.128.933-.384l1.767-1.849.366-.393-.019.96V53.96c0 .76.586 1.337 1.355 1.337.788 0 1.373-.577 1.373-1.337V43.232l-.027-.951.375.393 1.757 1.858a1.3 1.3 0 0 0 .943.375c.778 0 1.354-.558 1.354-1.336 0-.348-.192-.723-.43-.961l-4.347-4.476a1.38 1.38 0 0 0-1.995 0m12.658 16.74 4.338-4.466c.238-.256.412-.623.412-.97 0-.778-.55-1.337-1.337-1.337-.374 0-.704.129-.942.385l-1.776 1.84-.366.393.019-.942V39.049c0-.768-.586-1.345-1.355-1.345-.778 0-1.364.577-1.364 1.345v10.736l.028.944-.367-.403-1.766-1.84a1.28 1.28 0 0 0-.952-.385c-.787 0-1.327.56-1.327 1.337 0 .339.156.705.403.97l4.347 4.467c.522.522 1.437.576 2.005 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="m25 54.028c0 0.859-0.455 1.653-1.196 2.088-0.74 0.434-1.656 0.444-2.406 0.025-4.919-2.748-13.136-7.338-13.136-7.338-2.533-1.425-3.821-2.924-3.821-6.676v-21.477c0-2.836 1.048-4.63 3.436-5.958l18.664-10.477c3.287-1.848 6.635-1.848 9.922 0l18.686 10.477c2.366 1.328 3.414 3.122 3.414 5.958v1.987c0 1.305-1.058 2.363-2.363 2.363h-2e-3c-0.627 0-1.228-0.249-1.671-0.692s-0.692-1.044-0.692-1.671v-0.353s-2.028 1.145-3.45 1.948c-0.891 0.504-1.897 0.768-2.92 0.768h-6.99c-1.021 0-2.025-0.263-2.915-0.765-4.445-2.504-18.286-10.301-18.286-10.301l-7.418 4.216s9.552 5.355 13.894 7.79c0.488 0.274 0.84 0.74 0.97 1.284 0.129 0.545 0.026 1.119-0.286 1.584-0.17 0.189-0.301 0.406-0.421 0.629-0.25 0.469-0.678 0.817-1.187 0.965-0.51 0.149-1.058 0.085-1.52-0.176-4.313-2.393-14.137-7.942-14.137-7.942v19.38c0 1.395 0.512 2.255 1.968 3.058 0 0 9.24 5.218 12.633 7.134 0.76 0.429 1.23 1.235 1.23 2.108v0.064zm18.972-31.826-19.715-11.037 4.399-2.49c1.931-1.088 3.751-1.119 5.709 0l16.805 9.475-7.198 4.052z"/>
<g transform="matrix(.58577 0 0 .58577 29 29)" fill-rule="nonzero">
<path d="m11.031 59.75h37.688c7.14 0 11.031-3.859 11.031-10.953v-37.828c0-7.078-3.891-10.969-11.031-10.969h-37.688c-7.125 0-11.031 3.891-11.031 10.969v37.828c0 7.094 3.906 10.953 11.031 10.953zm0.875-8.062c-2.515 0-3.843-1.219-3.843-3.875v-35.844c0-2.656 1.328-3.891 3.843-3.891h35.938c2.5 0 3.844 1.235 3.844 3.891v35.844c0 2.656-1.344 3.875-3.844 3.875h-35.938z"/>
<path d="m19.094 15.594-7.406 7.64c-0.422 0.438-0.719 1.079-0.719 1.641 0 1.313 0.984 2.281 2.312 2.281 0.61 0 1.157-0.218 1.594-0.656l3.016-3.156 0.625-0.672-0.032 1.641v18.296c0 1.297 1 2.282 2.313 2.282 1.344 0 2.344-0.985 2.344-2.282v-18.312l-0.047-1.625 0.64 0.672 3 3.172c0.422 0.437 1.016 0.64 1.61 0.64 1.328 0 2.312-0.953 2.312-2.281 0-0.594-0.328-1.234-0.734-1.641l-7.422-7.64c-0.906-0.922-2.406-1.016-3.406 0zm21.609 28.578 7.406-7.625c0.407-0.438 0.704-1.063 0.704-1.656 0-1.328-0.938-2.282-2.282-2.282-0.64 0-1.203 0.219-1.609 0.657l-3.031 3.14-0.625 0.672 0.031-1.609v-18.313c0-1.312-1-2.297-2.313-2.297-1.328 0-2.328 0.985-2.328 2.297v18.328l0.047 1.61-0.625-0.688-3.015-3.14c-0.438-0.453-1-0.657-1.625-0.657-1.344 0-2.266 0.954-2.266 2.282 0 0.578 0.266 1.203 0.687 1.656l7.422 7.625c0.891 0.891 2.453 0.984 3.422 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99133)"/><path d="M24.123 43.633h16.258c2.013 0 3.231-1.19 3.231-3.119V24.041c0-1.955-1.218-3.119-3.231-3.119H24.123c-1.989 0-3.227 1.164-3.227 3.119v16.473c0 1.929 1.238 3.119 3.227 3.119" style="fill-rule:nonzero" transform="scale(.99133)"/></svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1,8 @@
<?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.991326,0,0,0.991326,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M24.123,43.633L40.381,43.633C42.394,43.633 43.612,42.443 43.612,40.514L43.612,24.041C43.612,22.086 42.394,20.922 40.381,20.922L24.123,20.922C22.134,20.922 20.896,22.086 20.896,24.041L20.896,40.514C20.896,42.443 22.134,43.633 24.123,43.633Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View 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="M32.28 64.56c17.824 0 32.286-14.461 32.286-32.28S50.104 0 32.28 0C14.461 0 0 14.461 0 32.28s14.461 32.28 32.28 32.28m0-6.359c-14.333 0-25.921-11.588-25.921-25.921S17.947 6.359 32.28 6.359 58.207 17.947 58.207 32.28 46.613 58.201 32.28 58.201" style="fill-rule:nonzero" transform="scale(.99124)"/><path d="M21.981 45.414c.825 0 1.518-.303 2.054-.865l8.219-8.24 8.282 8.24c.537.536 1.203.865 2.029.865 1.561 0 2.815-1.254 2.815-2.846 0-.78-.289-1.427-.846-1.984l-8.261-8.267 8.287-8.319c.582-.582.846-1.203.846-1.958a2.804 2.804 0 0 0-2.815-2.815c-.769 0-1.39.273-1.978.86l-8.359 8.276-8.27-8.245c-.536-.567-1.178-.84-2.003-.84-1.561 0-2.815 1.223-2.815 2.79 0 .755.315 1.433.845 1.958l8.268 8.293-8.268 8.293c-.53.531-.845 1.209-.845 1.958 0 1.592 1.254 2.846 2.815 2.846" style="fill-rule:nonzero" transform="scale(.99124)"/></svg>

After

Width:  |  Height:  |  Size: 1007 B

View File

@ -0,0 +1,8 @@
<?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.99124,0,0,0.99124,0,0)">
<path d="M32.28,64.56C50.104,64.56 64.566,50.099 64.566,32.28C64.566,14.461 50.104,0 32.28,0C14.461,0 0,14.461 0,32.28C0,50.099 14.461,64.56 32.28,64.56ZM32.28,58.201C17.947,58.201 6.359,46.613 6.359,32.28C6.359,17.947 17.947,6.359 32.28,6.359C46.613,6.359 58.207,17.947 58.207,32.28C58.207,46.613 46.613,58.201 32.28,58.201Z" style="fill-rule:nonzero;"/>
<path d="M21.981,45.414C22.806,45.414 23.499,45.111 24.035,44.549L32.254,36.309L40.536,44.549C41.073,45.085 41.739,45.414 42.565,45.414C44.126,45.414 45.38,44.16 45.38,42.568C45.38,41.788 45.091,41.141 44.534,40.584L36.273,32.317L44.56,23.998C45.142,23.416 45.406,22.795 45.406,22.04C45.406,20.474 44.151,19.225 42.591,19.225C41.822,19.225 41.201,19.498 40.613,20.085L32.254,28.361L23.984,20.116C23.448,19.549 22.806,19.276 21.981,19.276C20.42,19.276 19.166,20.499 19.166,22.066C19.166,22.821 19.481,23.499 20.011,24.024L28.279,32.317L20.011,40.61C19.481,41.141 19.166,41.819 19.166,42.568C19.166,44.16 20.42,45.414 21.981,45.414Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View File

@ -3,7 +3,7 @@ import React, { createContext, useState, useCallback, useEffect } from 'react'
import axios from 'axios'
import { message, Modal, notification, Progress, Button, Space } from 'antd'
import PropTypes from 'prop-types'
import { ExclamationCircleOutlined } from '@ant-design/icons'
import ExclamationOctogonIcon from '../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../Icons/InfoCircleIcon'
import config from '../../config'
@ -224,7 +224,7 @@ const AuthProvider = ({ children }) => {
<Modal
title={
<Space size={'middle'}>
<ExclamationCircleOutlined />
<ExclamationOctogonIcon />
Please log in to continue
</Space>
}

View File

@ -0,0 +1,265 @@
// src/partStocks.js
import React, { useEffect, useState, useContext, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import {
Table,
Button,
Flex,
Space,
Modal,
message,
Dropdown,
Typography
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import NewPartStock from './PartStocks/NewPartStock'
import IdText from '../common/IdText'
import PartStockIcon from '../../Icons/PartStockIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import PartStockState from '../common/PartStockState'
import TimeDisplay from '../common/TimeDisplay'
import config from '../../../config'
const { Text } = Typography
const useStyle = createStyles(({ css, token }) => {
const { antCls } = token
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
scrollbar-gutter: stable;
}
}
}
`
}
})
const PartStocks = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const { styles } = useStyle()
const [partStocksData, setPartStocksData] = useState([])
const [newPartStockOpen, setNewPartStockOpen] = useState(false)
const [loading, setLoading] = useState(true)
const { authenticated } = useContext(AuthContext)
const fetchPartStocksData = useCallback(async () => {
try {
const response = await axios.get(`${config.backendUrl}/partstocks`, {
headers: {
Accept: 'application/json'
},
withCredentials: true // Important for including cookies
})
setPartStocksData(response.data)
setLoading(false)
} catch (err) {
messageApi.info(err)
}
}, [messageApi])
useEffect(() => {
// Fetch initial data
if (authenticated) {
fetchPartStocksData()
}
}, [authenticated, fetchPartStocksData])
const getPartStockActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/inventory/partstocks/info?partStockId=${id}`)
}
}
}
}
// Column definitions
const columns = [
{
title: '',
dataIndex: '',
key: 'icon',
width: 40,
fixed: 'left',
render: () => <PartStockIcon></PartStockIcon>
},
{
title: 'Part Name',
dataIndex: 'part',
key: 'name',
width: 200,
fixed: 'left',
render: (part) => <Text ellipsis>{part.name}</Text>
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 165,
render: (text) => <IdText id={text} type={'partstock'} longId={false} />
},
{
title: 'State',
key: 'state',
width: 350,
render: (record) => <PartStockState partStock={record} />
},
{
title: 'Current Quantity',
dataIndex: 'currentQuantity',
key: 'currentQuantity',
width: 160,
render: (currentQuantity) => <Text ellipsis>{currentQuantity}</Text>
},
{
title: 'Starting Quantity',
dataIndex: 'startingQuantity',
key: 'startingQuantity',
width: 160,
render: (startingQuantity) => <Text ellipsis>{startingQuantity}</Text>
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
}
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
}
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (text, record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/inventory/partstocks/info?partStockId=${record._id}`
)
}
/>
<Dropdown menu={getPartStockActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const actionItems = {
items: [
{
label: 'New Part Stock',
key: 'newPartStock',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
fetchPartStocksData()
} else if (key === 'newPartStock') {
setNewPartStockOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
<Table
dataSource={partStocksData}
className={styles.customTable}
columns={columns}
pagination={false}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
scroll={{ y: 'calc(100vh - 270px)' }}
/>
</Flex>
<Modal
open={newPartStockOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={700}
onCancel={() => {
setNewPartStockOpen(false)
}}
destroyOnClose
>
<NewPartStock
onOk={() => {
setNewPartStockOpen(false)
messageApi.success('New part stock created successfully.')
fetchPartStocksData()
}}
reset={newPartStockOpen}
/>
</Modal>
</>
)
}
export default PartStocks

View File

@ -0,0 +1,129 @@
import React, { useState, useEffect } from 'react'
import { Form, Input, Button, Space, Select, InputNumber } from 'antd'
import axios from 'axios'
import PropTypes from 'prop-types'
import config from '../../../../config'
const NewPartStock = ({ onOk, reset }) => {
const [form] = Form.useForm()
const [parts, setParts] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
// Reset form when reset prop changes
if (reset) {
form.resetFields()
}
}, [reset, form])
useEffect(() => {
// Fetch parts for the select dropdown
const fetchParts = async () => {
try {
const response = await axios.get(`${config.backendUrl}/parts`, {
headers: {
Accept: 'application/json'
},
withCredentials: true
})
setParts(response.data)
} catch (error) {
console.error('Error fetching parts:', error)
}
}
fetchParts()
}, [])
const onFinish = async (values) => {
setLoading(true)
try {
await axios.post(
`${config.backendUrl}/partstocks`,
{
part: values.part,
startingLots: values.startingLots,
currentLots: values.startingLots, // Initially current lots equals starting lots
notes: values.notes
},
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
withCredentials: true
}
)
onOk()
} catch (error) {
console.error('Error creating part stock:', error)
} finally {
setLoading(false)
}
}
return (
<Form
form={form}
layout='vertical'
onFinish={onFinish}
style={{ maxWidth: '100%' }}
>
<Form.Item
name='part'
label='Part'
rules={[{ required: true, message: 'Please select a part' }]}
>
<Select
placeholder='Select a part'
options={parts.map((part) => ({
value: part._id,
label: part.name
}))}
/>
</Form.Item>
<Form.Item
name='startingLots'
label='Starting Lots'
rules={[
{ required: true, message: 'Please enter the starting lots' },
{ type: 'number', min: 1, message: 'Lots must be at least 1' }
]}
>
<InputNumber
style={{ width: '100%' }}
placeholder='Enter starting lots'
min={1}
/>
</Form.Item>
<Form.Item name='notes' label='Notes'>
<Input.TextArea
placeholder='Enter any additional notes'
autoSize={{ minRows: 3, maxRows: 6 }}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type='primary' htmlType='submit' loading={loading}>
Create Part Stock
</Button>
<Button onClick={() => form.resetFields()}>Reset</Button>
</Space>
</Form.Item>
</Form>
)
}
NewPartStock.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
NewPartStock.defaultProps = {
reset: false
}
export default NewPartStock

View File

@ -14,8 +14,6 @@ import {
import {
ArrowLeftOutlined,
LoadingOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined
} from '@ant-design/icons'
@ -24,6 +22,8 @@ import IdText from '../../common/IdText'
import TimeDisplay from '../../common/TimeDisplay'
import config from '../../../../config'
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
const { Text, Title } = Typography
@ -73,7 +73,7 @@ const StockAuditInfo = () => {
switch (status?.toLowerCase()) {
case 'completed':
return (
<Tag icon={<CheckCircleOutlined />} color='success'>
<Tag icon={<CheckCircleIcon />} color='success'>
Completed
</Tag>
)
@ -85,7 +85,7 @@ const StockAuditInfo = () => {
)
case 'failed':
return (
<Tag icon={<CloseCircleOutlined />} color='error'>
<Tag icon={<XMarkCircleIcon />} color='error'>
Failed
</Tag>
)

View File

@ -0,0 +1,422 @@
import React, { useEffect, useState, useContext, useCallback } from 'react'
import axios from 'axios'
import {
Button,
Flex,
Space,
message,
Spin,
Popover,
Checkbox,
Dropdown,
Table,
Typography
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined, AuditOutlined } from '@ant-design/icons'
import moment from 'moment'
import { AuthContext } from '../../Auth/AuthContext'
import { SocketContext } from '../context/SocketContext'
import IdText from '../common/IdText'
import TimeDisplay from '../common/TimeDisplay'
import ReloadIcon from '../../Icons/ReloadIcon'
import PlusMinusIcon from '../../Icons/PlusMinusIcon'
import SubJobIcon from '../../Icons/SubJobIcon'
import PlayCircleIcon from '../../Icons/PlayCircleIcon'
import config from '../../../config'
const { Text } = Typography
const useStyle = createStyles(({ css, token }) => {
const { antCls } = token
return {
customTable: css`
${antCls}-table {
${antCls}-table-container {
${antCls}-table-body,
${antCls}-table-content {
scrollbar-width: thin;
scrollbar-color: #eaeaea transparent;
scrollbar-gutter: stable;
}
}
}
`
}
})
const StockEvents = () => {
const [messageApi, contextHolder] = message.useMessage()
const { styles } = useStyle()
const { socket } = useContext(SocketContext)
const [initialized, setInitialized] = useState(false)
const [stockEventsData, setStockEventsData] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false)
const [filters, setFilters] = useState({})
const [sorter, setSorter] = useState({})
// Column definitions for visibility
const columns = [
{
title: '',
key: 'icon',
width: 50,
render: (record) => {
switch (record.type.toLowerCase()) {
case 'subjob':
return <SubJobIcon />
case 'audit':
return <AuditOutlined />
case 'initial':
return <PlayCircleIcon />
default:
return null
}
}
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
width: 200,
sorter: (a, b) => a.type.localeCompare(b.type),
filters: [
{ text: 'Sub Job', value: 'Sub Job' },
{ text: 'Audit Adjustment', value: 'Audit Adjustment' },
{ text: 'Initial', value: 'Initial' }
],
onFilter: (value, record) => {
const recordType = record.type.toLowerCase()
if (recordType === 'subjob') {
return value === 'Sub Job'
} else if (recordType === 'audit') {
return value === 'Audit Adjustment'
}
return (
value === recordType.charAt(0).toUpperCase() + recordType.slice(1)
)
},
render: (type) => {
switch (type.toLowerCase()) {
case 'subjob':
return 'Sub Job'
case 'audit':
return 'Audit Adjustment'
default:
return type.charAt(0).toUpperCase() + type.slice(1)
}
}
},
{
title: <PlusMinusIcon />,
dataIndex: 'value',
key: 'value',
width: 100,
sorter: (a, b) => a.value - b.value,
render: (value, record) => {
const formattedValue = value.toFixed(2) + record.unit
return (
<Text type={value < 0 ? 'danger' : 'success'}>
{value > 0 ? '+' + formattedValue : formattedValue}
</Text>
)
}
},
{
title: 'Linked ID',
key: 'linkedId',
width: 100,
render: (record) => {
if (record.subJob?.number) {
return (
<IdText
id={record.subJob.number.toString().padStart(6, '0')}
longId={false}
type={'subjob'}
/>
)
}
if (record.stockAudit) {
return (
<IdText
id={record.stockAudit._id}
longId={false}
type={'stockaudit'}
showHyperlink={true}
/>
)
}
return 'n/a'
}
},
{
title: 'Job ID',
key: 'jobId',
width: 100,
render: (record) => {
if (record.subJob) {
return (
<IdText
id={record.job}
longId={false}
type={'job'}
showHyperlink={true}
/>
)
}
return 'n/a'
}
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
defaultSortOrder: 'descend',
sorter: (a, b) => moment(a.createdAt).unix() - moment(b.createdAt).unix(),
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
} else {
return 'n/a'
}
}
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
sorter: (a, b) => moment(a.updatedAt).unix() - moment(b.updatedAt).unix(),
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
} else {
return 'n/a'
}
}
}
]
const [columnVisibility, setColumnVisibility] = useState(
columns.reduce((acc, col) => {
if (col.key) {
acc[col.key] = true
}
return acc
}, {})
)
const { authenticated } = useContext(AuthContext)
const fetchStockEventsData = useCallback(
async (pageNum = 1, append = false) => {
try {
const response = await axios.get(`${config.backendUrl}/stockevents`, {
params: {
page: pageNum,
limit: 25,
...filters,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const newData = response.data
setHasMore(newData.length === 25)
if (append) {
setStockEventsData((prev) => [...prev, ...newData])
} else {
setStockEventsData(newData)
}
setLoading(false)
setLazyLoading(false)
} catch (error) {
if (error.response) {
messageApi.error(
'Error fetching stock events:',
error.response.status
)
} else {
messageApi.error(
'An unexpected error occurred. Please try again later.'
)
}
setLoading(false)
setLazyLoading(false)
}
},
[messageApi, filters, sorter]
)
useEffect(() => {
if (authenticated) {
fetchStockEventsData()
}
}, [authenticated, fetchStockEventsData])
useEffect(() => {
// Add WebSocket event listener for real-time updates
if (socket && !initialized) {
setInitialized(true)
socket.on('notify_stockevent_update', (updateData) => {
console.log('Received stock event update:', updateData)
setStockEventsData((prevData) => {
return prevData.map((stockEvent) => {
if (stockEvent?._id) {
if (stockEvent._id === updateData._id) {
return {
...stockEvent,
...updateData
}
} else {
return stockEvent
}
}
})
})
})
}
return () => {
if (socket && initialized) {
console.log('Deregistering stock event update listener')
socket.off('notify_stockevent_update')
}
}
}, [socket, initialized])
const handleScroll = useCallback(
(e) => {
const { target } = e
const scrollHeight = target.scrollHeight
const scrollTop = target.scrollTop
const clientHeight = target.clientHeight
if (
scrollHeight - scrollTop - clientHeight < 100 &&
!lazyLoading &&
hasMore
) {
setLazyLoading(true)
const nextPage = page + 1
setPage(nextPage)
fetchStockEventsData(nextPage, true)
}
},
[page, lazyLoading, hasMore, fetchStockEventsData]
)
const actionItems = {
items: [
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
setPage(1)
fetchStockEventsData(1)
}
}
}
const handleTableChange = (pagination, filters, sorter) => {
const newFilters = {}
Object.entries(filters).forEach(([key, value]) => {
if (value && value.length > 0) {
newFilters[key] = value[0]
}
})
setPage(1)
setFilters(newFilters)
setSorter({
field: sorter.field,
order: sorter.order
})
}
const getViewDropdownItems = () => {
const columnItems = columns
.filter((col) => col.key && col.title !== '')
.map((col) => (
<Checkbox
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
setColumnVisibility((prev) => ({
...prev,
[col.key]: e.target.checked
}))
}}
>
{col.title}
</Checkbox>
))
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{columnItems}
</Flex>
</Flex>
)
}
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
</Space>
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
</Flex>
<Table
dataSource={stockEventsData}
columns={visibleColumns}
className={styles.customTable}
pagination={false}
scroll={{ y: 'calc(100vh - 270px)' }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
/>
</Flex>
</>
)
}
export default StockEvents

View File

@ -0,0 +1,99 @@
import React from 'react'
import { Select, Typography, Descriptions, Collapse, Flex } from 'antd'
import { CaretRightOutlined } from '@ant-design/icons'
import { useThemeContext } from '../context/ThemeContext'
import useCollapseState from '../hooks/useCollapseState'
const { Title } = Typography
const { Option } = Select
const Settings = () => {
const { isDarkMode, toggleTheme, isCompact, toggleCompact } =
useThemeContext()
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
appearance: true
})
const handleThemeChange = (value) => {
if (value === 'dark' && !isDarkMode) {
toggleTheme()
} else if (value === 'light' && isDarkMode) {
toggleTheme()
}
}
const handleCompactChange = (value) => {
if (value === 'compact' && !isCompact) {
toggleCompact()
} else if (value === 'comfortable' && isCompact) {
toggleCompact()
}
}
return (
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.appearance ? ['1'] : []}
onChange={(keys) => updateCollapseState('appearance', keys.length > 0)}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '9px' }}
/>
)}
>
<Collapse.Panel
header={
<Flex
align='center'
justify='space-between'
style={{ width: '100%' }}
>
<Title level={5} style={{ margin: 0 }}>
Appearance Settings
</Title>
</Flex>
}
key='1'
>
<Descriptions
bordered
column={{
xs: 1,
sm: 1,
md: 1,
lg: 2,
xl: 2,
xxl: 2
}}
>
<Descriptions.Item label='Theme'>
<Select
value={isDarkMode ? 'dark' : 'light'}
onChange={handleThemeChange}
style={{ width: '200px' }}
>
<Option value='light'>Light</Option>
<Option value='dark'>Dark</Option>
</Select>
</Descriptions.Item>
<Descriptions.Item label='UI Density'>
<Select
value={isCompact ? 'compact' : 'comfortable'}
onChange={handleCompactChange}
style={{ width: '200px' }}
>
<Option value='comfortable'>Comfortable</Option>
<Option value='compact'>Compact</Option>
</Select>
</Descriptions.Item>
</Descriptions>
</Collapse.Panel>
</Collapse>
</div>
)
}
export default Settings

View File

@ -233,7 +233,7 @@ const VendorInfo = () => {
<Input />
</Form.Item>
) : (
vendorData.names
vendorData.name
)}
</Descriptions.Item>

View File

@ -19,14 +19,7 @@ import {
Spin
} from 'antd'
import { createStyles } from 'antd-style'
import {
LoadingOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
PauseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../../Auth/AuthContext'
import { SocketContext } from '../context/SocketContext'
@ -43,8 +36,13 @@ import ReloadIcon from '../../Icons/ReloadIcon'
import EditIcon from '../../Icons/EditIcon.jsx'
import XMarkIcon from '../../Icons/XMarkIcon.jsx'
import CheckIcon from '../../Icons/CheckIcon.jsx'
import PlayCircleIcon from '../../Icons/PlayCircleIcon.jsx'
import config from '../../../config.js'
import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import PauseCircleIcon from '../../Icons/PauseCircleIcon.jsx'
import XMarkCircleIcon from '../../Icons/XMarkCircleIcon.jsx'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon.jsx'
const { Text } = Typography
@ -198,7 +196,7 @@ const PrintJobs = () => {
record.state.type.toLowerCase().includes(value.toLowerCase())
},
{
title: <CheckCircleOutlined />,
title: <CheckCircleIcon />,
key: 'complete',
width: 70,
render: (record) => {
@ -206,7 +204,7 @@ const PrintJobs = () => {
}
},
{
title: <PauseCircleOutlined />,
title: <PauseCircleIcon />,
key: 'queued',
width: 70,
render: (record) => {
@ -214,7 +212,7 @@ const PrintJobs = () => {
}
},
{
title: <CloseCircleOutlined />,
title: <XMarkCircleIcon />,
key: 'failed',
width: 70,
render: (record) => {
@ -222,7 +220,7 @@ const PrintJobs = () => {
}
},
{
title: <QuestionCircleOutlined />,
title: <QuestionCircleIcon />,
key: 'draft',
width: 70,
render: (record) => {
@ -267,7 +265,7 @@ const PrintJobs = () => {
<Space size='small'>
{record.state.type === 'draft' ? (
<Button
icon={<PlayCircleOutlined />}
icon={<PlayCircleIcon />}
onClick={() => handleDeployPrintJob(record.id)}
/>
) : (

View File

@ -16,19 +16,15 @@ import {
Badge,
Alert,
Popover,
Checkbox
Checkbox,
Collapse
} from 'antd'
import {
LoadingOutlined,
PlayCircleOutlined,
ExclamationCircleOutlined,
PauseCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons'
import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons'
import { SocketContext } from '../../context/SocketContext'
import PrinterTemperaturePanel from '../../common/PrinterTemperaturePanel'
import PrinterPositionPanel from '../../common/PrinterPositionPanel'
import PrinterMovementPanel from '../../common/PrinterMovementPanel'
import PrinterState from '../../common/PrinterState'
import { AuthContext } from '../../../Auth/AuthContext'
@ -44,10 +40,15 @@ import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock'
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock'
import FilamentStockState from '../../common/FilamentStockState'
import useCollapseState from '../../hooks/useCollapseState'
import config from '../../../../config'
import PlayCircleIcon from '../../../Icons/PlayCircleIcon'
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
import PauseCircleIcon from '../../../Icons/PauseCircleIcon'
import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon'
const { Text } = Typography
const { Text, Title } = Typography
// Helper function to parse query parameters
const useQuery = () => {
@ -69,6 +70,16 @@ const ControlPrinter = () => {
const [klippyErrorMessage, setKlippyErrorMessage] = useState('')
const [klippyStartupMessage, setKlippyStartupMessage] = useState('')
const [collapseState, updateCollapseState] = useCollapseState(
'ControlPrinter',
{
job: true,
filament: true,
gcodefile: true,
jobs: true
}
)
// Load visibility preferences from sessionStorage on component mount
const [componentVisibility, setComponentVisibility] = useState(() => {
const savedVisibility = sessionStorage.getItem('printerControlVisibility')
@ -216,19 +227,19 @@ const ControlPrinter = () => {
{
label: 'Resume Print',
key: 'resumePrint',
icon: <PlayCircleOutlined />,
icon: <PlayCircleIcon />,
disabled: printerData?.state?.type !== 'paused'
},
{
label: 'Pause Print',
key: 'pausePrint',
icon: <PauseCircleOutlined />,
icon: <PauseCircleIcon />,
disabled: printerData?.state?.type !== 'printing'
},
{
label: 'Cancel Print',
key: 'cancelPrint',
icon: <CloseCircleOutlined />,
icon: <XMarkCircleIcon />,
disabled: !(
printerData?.state?.type === 'printing' ||
printerData?.state?.type === 'paused'
@ -250,12 +261,12 @@ const ControlPrinter = () => {
printerData?.state?.type === 'paused' ||
printerData?.state?.type === 'error',
icon: <PlayCircleOutlined />
icon: <PlayCircleIcon />
},
{
label: 'Pause Queue',
key: 'pauseQueue',
icon: <PauseCircleOutlined />
icon: <PauseCircleIcon />
}
]
},
@ -346,6 +357,17 @@ const ControlPrinter = () => {
>
Temperature Panel
</Checkbox>
<Checkbox
checked={componentVisibility.position}
onChange={(e) => {
setComponentVisibility((prev) => ({
...prev,
position: e.target.checked
}))
}}
>
Position Panel
</Checkbox>
<Checkbox
checked={componentVisibility.movement}
onChange={(e) => {
@ -407,16 +429,16 @@ const ControlPrinter = () => {
</Space>
<Space size='small'>
<Button
icon={<ExclamationCircleOutlined />}
icon={<ExclamationOctagonIcon />}
danger
onClick={handleEmergencyStop}
></Button>
<Button
icon={
printerData?.state?.type === 'paused' ? (
<PlayCircleOutlined />
<PlayCircleIcon />
) : (
<PauseCircleOutlined />
<PauseCircleIcon />
)
}
disabled={
@ -434,7 +456,7 @@ const ControlPrinter = () => {
}}
></Button>
<Button
icon={<PlayCircleOutlined />}
icon={<PlayCircleIcon />}
disabled={
printerData?.state?.type === 'printing' ||
printerData?.state?.type === 'deploying' ||
@ -449,7 +471,8 @@ const ControlPrinter = () => {
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
{printerData ? (
<Flex gap={16}>
<Flex>
<Flex vertical style={{ flexGrow: 1 }}>
<Flex gap={16} vertical style={{ flexGrow: 1 }}>
{printerData?.alerts?.some(
(alert) => alert.type === 'klippyError'
@ -457,13 +480,42 @@ const ControlPrinter = () => {
{printerData?.alerts?.some(
(alert) => alert.type === 'klippyStartup'
) && <Alert message={klippyStartupMessage} type='warning' />}
</Flex>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.job ? ['1'] : []}
onChange={(keys) =>
updateCollapseState('job', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '2px' }}
/>
)}
>
<Collapse.Panel
header={
<Flex
align='center'
justify='space-between'
style={{ width: '100%' }}
>
<Title level={5} style={{ margin: 0 }}>
Current Job
</Title>
</Flex>
}
key='1'
>
<Descriptions
bordered
column={{
xs: 1,
sm: 1,
md: 1,
lg: 2,
lg: 1,
xl: 2,
xxl: 2
}}
@ -471,6 +523,7 @@ const ControlPrinter = () => {
<Descriptions.Item label='Printer Name'>
{printerData.name}
</Descriptions.Item>
<Descriptions.Item label='Printer ID'>
{printerData._id ? (
<IdText
@ -483,32 +536,7 @@ const ControlPrinter = () => {
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Print Job ID'>
{printerData.currentJob?.id ? (
<IdText
id={printerData.currentJob.id}
type='job'
longId={false}
showHyperlink={true}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Sub Job ID'>
{printerData.currentSubJob?.id ? (
<IdText
id={printerData.currentSubJob.number
.toString()
.padStart(6, '0')}
type='subjob'
longId={false}
showHyperlink={false}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='GCode File Name'>
{
<Space>
@ -532,38 +560,11 @@ const ControlPrinter = () => {
)}
</Descriptions.Item>
{printerData?.currentFilamentStock && (
<Descriptions.Item label='Filament Stock' span={2}>
<FilamentStockState
filamentStock={printerData?.currentFilamentStock}
/>
</Descriptions.Item>
)}
<Descriptions.Item label='Filament Stock Weight'>
{printerData.currentFilamentStock?.currentNetWeight ? (
<Descriptions style={{ width: '250px' }} column={2}>
<Descriptions.Item label='Net'>
{printerData.currentFilamentStock.currentNetWeight.toFixed(
2
) + 'g'}
</Descriptions.Item>
<Descriptions.Item label='Gross'>
{printerData.currentFilamentStock.currentGrossWeight.toFixed(
2
) + 'g'}
</Descriptions.Item>
</Descriptions>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament Stock ID'>
{printerData.currentFilamentStock ? (
<Descriptions.Item label='Print Job ID'>
{printerData.currentJob?.id ? (
<IdText
id={printerData.currentFilamentStock._id}
type='filamentstock'
id={printerData.currentJob.id}
type='job'
longId={false}
showHyperlink={true}
/>
@ -572,29 +573,15 @@ const ControlPrinter = () => {
)}
</Descriptions.Item>
<Descriptions.Item label='Filament Name'>
{printerData.currentFilamentStock?.filament?.name ? (
<Space>
<FilamentIcon />
<Badge
text={printerData.currentFilamentStock.filament.name}
color={
printerData.currentFilamentStock.filament.color
}
></Badge>
</Space>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament ID'>
{printerData?.currentFilamentStock?.filament ? (
<Descriptions.Item label='Sub Job ID'>
{printerData.currentSubJob?.id ? (
<IdText
id={printerData.currentFilamentStock.filament._id}
type='filament'
id={printerData.currentSubJob.number
.toString()
.padStart(6, '0')}
type='subjob'
longId={false}
showHyperlink={true}
showHyperlink={false}
/>
) : (
'n/a'
@ -602,7 +589,8 @@ const ControlPrinter = () => {
</Descriptions.Item>
{printerData?.state.type === 'printing' && (
<Descriptions.Item label='Progress' span={2}>
<>
<Descriptions.Item label='Progress' span={1}>
<Progress
percent={Math.round(
(printerData.state.progress || 0) * 100
@ -610,6 +598,10 @@ const ControlPrinter = () => {
status='active'
/>
</Descriptions.Item>
<Descriptions.Item label='Started At' span={1}>
{printerData.name}
</Descriptions.Item>
</>
)}
<Descriptions.Item label='Print Profile'>
@ -651,9 +643,149 @@ const ControlPrinter = () => {
})()}
</Descriptions.Item>
</Descriptions>
{componentVisibility.subjobs && (
<PrinterSubJobsTree subJobs={printerData.subJobs} />
</Collapse.Panel>
</Collapse>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.filament ? ['1'] : []}
onChange={(keys) =>
updateCollapseState('filament', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '2px' }}
/>
)}
>
<Collapse.Panel
header={
<Flex
align='center'
justify='space-between'
style={{ width: '100%' }}
>
<Title level={5} style={{ margin: 0 }}>
Loaded Filament Stock
</Title>
</Flex>
}
key='1'
>
<Descriptions
bordered
column={{
xs: 1,
sm: 1,
md: 1,
lg: 1,
xl: 2,
xxl: 2
}}
>
<Descriptions.Item label='Filament Stock' span={1}>
{printerData?.currentFilamentStock ? (
<FilamentStockState
filamentStock={printerData?.currentFilamentStock}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament Stock ID'>
{printerData.currentFilamentStock ? (
<IdText
id={printerData.currentFilamentStock._id}
type='filamentstock'
longId={false}
showHyperlink={true}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament Name'>
{printerData.currentFilamentStock?.filament?.name ? (
<Space>
<FilamentIcon />
<Badge
text={
printerData.currentFilamentStock.filament.name
}
color={
printerData.currentFilamentStock.filament.color
}
></Badge>
</Space>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Filament ID'>
{printerData?.currentFilamentStock?.filament ? (
<IdText
id={printerData.currentFilamentStock.filament._id}
type='filament'
longId={false}
showHyperlink={true}
/>
) : (
'n/a'
)}
</Descriptions.Item>
<Descriptions.Item label='Weight'>
{printerData.currentFilamentStock?.currentNetWeight ? (
<Descriptions style={{ width: '250px' }} column={2}>
<Descriptions.Item label='Net'>
{printerData.currentFilamentStock.currentNetWeight.toFixed(
2
) + 'g'}
</Descriptions.Item>
<Descriptions.Item label='Gross'>
{printerData.currentFilamentStock.currentGrossWeight.toFixed(
2
) + 'g'}
</Descriptions.Item>
</Descriptions>
) : (
'n/a'
)}
</Descriptions.Item>
</Descriptions>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
collapsible='icon'
activeKey={collapseState.jobs ? ['1'] : []}
onChange={(keys) =>
updateCollapseState('jobs', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretRightOutlined
rotate={isActive ? 90 : 0}
style={{ paddingTop: '2px' }}
/>
)}
>
<Collapse.Panel
header={
<Flex
align='center'
justify='space-between'
style={{ width: '100%' }}
>
<Title level={5} style={{ margin: 0 }}>
Printer Jobs
</Title>
</Flex>
}
key='1'
>
<PrinterSubJobsTree subJobs={printerData.subJobs} />
</Collapse.Panel>
</Collapse>
</Flex>
<Flex gap={16} vertical>
{componentVisibility.temperature && (
@ -665,6 +797,16 @@ const ControlPrinter = () => {
</Card>
)}
{componentVisibility.position && (
<Card>
<PrinterPositionPanel
printerId={printerId}
showControls={true}
showMoreInfo={true}
/>
</Card>
)}
{componentVisibility.movement && (
<Card>
<PrinterMovementPanel
@ -718,7 +860,7 @@ const ControlPrinter = () => {
open={klippyErrorModalOpen}
title={
<Space size={'middle'}>
<ExclamationCircleOutlined />
<ExclamationOctagonIcon />
Klipper Error
</Space>
}

View File

@ -2,7 +2,8 @@
import React from 'react'
import { Breadcrumb, Button, Flex, Space } from 'antd'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
const breadcrumbNameMap = {
'/dashboard/production': 'Production',
@ -26,12 +27,15 @@ const breadcrumbNameMap = {
'/dashboard/management/vendors/info': 'Info',
'/dashboard/management/materials': 'Materials',
'/dashboard/management/materials/info': 'Info',
'/dashboard/management/settings': 'Settings',
'/dashboard/inventory/filamentstocks': 'Filament Stocks',
'/dashboard/inventory/filamentstocks/info': 'Info',
'/dashboard/inventory/partstocks': 'Parts',
'/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'
}
@ -71,13 +75,13 @@ const DashboardBreadcrumb = () => {
<Space.Compact>
<Button
type='text'
icon={<ArrowLeftOutlined style={{ fontSize: '14px' }} />}
icon={<ArrowLeftIcon style={{ fontSize: '14px' }} />}
onClick={() => navigate(-1)}
style={{ padding: '0 2px', height: '22px' }}
/>
<Button
type='text'
icon={<ArrowRightOutlined style={{ fontSize: '14px' }} />}
icon={<ArrowRightIcon style={{ fontSize: '14px' }} />}
onClick={() => navigate(1)}
style={{ padding: '0 2px', height: '22px' }}
/>

View File

@ -8,7 +8,8 @@ import {
Dropdown,
Button,
Tooltip,
Typography
Typography,
Divider
} from 'antd'
import {
LogoutOutlined,
@ -124,7 +125,12 @@ const DashboardNavigation = () => {
return (
<Header
style={{ width: '100vw', padding: 0, background: 'unset' }}
style={{
width: '100vw',
padding: 0,
marginBottom: '0.1px',
background: 'unset'
}}
theme='light'
className='ant-menu-horizontal'
>
@ -178,11 +184,13 @@ const DashboardNavigation = () => {
</Tag>
</Space>
) : null}
{process.env.NODE_ENV === 'development' && (
<Space>
<Tag color='yellow' style={{ marginRight: 0 }}>
Dev
</Tag>
</Space>
)}
{userProfile ? (
<Space>
<Dropdown menu={userMenuItems} placement='bottomRight'>
@ -194,6 +202,7 @@ const DashboardNavigation = () => {
) : null}
</Flex>
</Flex>
<Divider style={{ margin: 0 }} />
</Header>
)
}

View File

@ -60,11 +60,11 @@ const FilamentStockState = ({
break
case 'partiallyconsumed':
setBadgeStatus('warning')
setBadgeText('Partially Consumed')
setBadgeText('Partial')
break
case 'fullyconsumed':
setBadgeStatus('error')
setBadgeText('Fully Consumed')
setBadgeText('Consumed')
break
case 'error':
setBadgeStatus('error')

View File

@ -16,7 +16,7 @@ const propertyOrder = [
const { Text } = Typography
const GCodeFileSelect = ({ onChange, filter, useFilter }) => {
const GCodeFileSelect = ({ onChange, filter, useFilter, style }) => {
const [gcodeFilesTreeData, setGCodeFilesTreeData] = useState(null)
const [loading, setLoading] = useState(true)
const [searchValue, setSearchValue] = useState('')
@ -205,6 +205,7 @@ const GCodeFileSelect = ({ onChange, filter, useFilter }) => {
onSearch={handleGCodeFilesSearch}
loading={loading}
placeholder='Select GCode File'
style={style}
/>
)
}
@ -212,7 +213,8 @@ const GCodeFileSelect = ({ onChange, filter, useFilter }) => {
GCodeFileSelect.propTypes = {
onChange: PropTypes.func.isRequired,
filter: PropTypes.string.isRequired,
useFilter: PropTypes.bool.isRequired
useFilter: PropTypes.bool.isRequired,
style: PropTypes.object
}
export default GCodeFileSelect

View File

@ -6,6 +6,7 @@ import FilamentStockIcon from '../../Icons/FilamentStockIcon'
import PartStockIcon from '../../Icons/PartStockIcon'
import ProductStockIcon from '../../Icons/ProductStockIcon'
import StockAuditIcon from '../../Icons/StockAuditIcon'
import StockEventIcon from '../../Icons/StockEventIcon'
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
@ -60,6 +61,11 @@ const InventorySidebar = () => {
icon: <ProductStockIcon />
},
{ type: 'divider' },
{
key: 'stockevents',
label: <Link to='/dashboard/inventory/stockevents'>Stock Events</Link>,
icon: <StockEventIcon />
},
{
key: 'stockaudits',
label: <Link to='/dashboard/inventory/stockaudits'>Stock Audits</Link>,

View File

@ -0,0 +1,112 @@
import PropTypes from 'prop-types'
import { Badge, Progress, Flex, Space, Tag, Typography } from 'antd'
import { green } from '@ant-design/colors'
import React, { useState, useEffect } from 'react'
const { Text } = Typography
const getProgressColor = (percent) => {
if (percent <= 50) {
return green[5]
} else if (percent <= 80) {
// Interpolate between green and yellow
const ratio = (percent - 50) / 30
return `rgb(${Math.round(255 * ratio)}, ${Math.round(255 * (1 - ratio))}, 0)`
} else {
// Interpolate between yellow and red
const ratio = (percent - 80) / 20
return `rgb(255, ${Math.round(255 * (1 - ratio))}, 0)`
}
}
const PartStockState = ({
partStock,
showProgress = true,
showStatus = true
}) => {
const [badgeStatus, setBadgeStatus] = useState('unknown')
const [badgeText, setBadgeText] = useState('Unknown')
const [currentState] = useState(
partStock?.state || {
type: 'unknown',
progress: 0
}
)
useEffect(() => {
switch (currentState.type) {
case 'unused':
setBadgeStatus('success')
setBadgeText('Unused')
break
case 'partiallyused':
setBadgeStatus('warning')
setBadgeText('Partial')
break
case 'fullyused':
setBadgeStatus('error')
setBadgeText('Used')
break
case 'error':
setBadgeStatus('error')
setBadgeText('Error')
break
default:
setBadgeStatus('default')
setBadgeText(currentState.type)
}
}, [currentState])
return (
<Flex gap='middle' align={'center'}>
{showStatus && (
<Space>
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
<Flex gap={6}>
<Badge status={badgeStatus} />
{badgeText}
</Flex>
</Tag>
</Space>
)}
{showProgress && currentState.type === 'partiallyused' ? (
<Flex style={{ width: '150px' }} gap={'small'}>
<div style={{ flexGrow: '1' }}>
<Progress
percent={Math.round(currentState.percent * 100)}
style={{ marginBottom: '2px', width: '100%' }}
strokeColor={getProgressColor(
Math.round(currentState.percent * 100)
)}
showInfo={false}
/>
</div>
<Text style={{ marginTop: '1px' }}>
{Math.round(currentState.percent * 100) + '%'}
</Text>
</Flex>
) : null}
</Flex>
)
}
PartStockState.propTypes = {
partStock: PropTypes.shape({
_id: PropTypes.string,
name: PropTypes.string,
state: PropTypes.shape({
type: PropTypes.oneOf([
'unused',
'partiallyused',
'fullyused',
'error',
'unknown'
]),
progress: PropTypes.number
})
}),
showProgress: PropTypes.bool,
showStatus: PropTypes.bool
}
export default PartStockState

View File

@ -10,17 +10,14 @@ import {
Card,
message // eslint-disable-line
} from 'antd'
import {
ArrowUpOutlined,
ArrowLeftOutlined,
HomeOutlined,
ArrowRightOutlined,
ArrowDownOutlined
} from '@ant-design/icons'
import { SocketContext } from '../context/SocketContext'
import UnloadIcon from '../../Icons/UnloadIcon'
import PropTypes from 'prop-types'
import LevelBedIcon from '../../Icons/LevelBedIcon'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
import ArrowUpIcon from '../../Icons/ArrowUpIcon'
import ArrowDownIcon from '../../Icons/ArrowDownIcon'
import HomeIcon from '../../Icons/HomeIcon'
const PrinterMovementPanel = ({ printerId }) => {
const [posValue, setPosValue] = useState(10)
@ -68,15 +65,6 @@ const PrinterMovementPanel = ({ printerId }) => {
//sendCommand('levelBed')
}
const handleUnloadFilamentClick = () => {
if (socket) {
socket.emit('printer.gcode.script', {
printerId,
script: `UNLOAD_FILAMENT TEMP=`
})
}
}
const homeAxisButtonItems = [
{
key: 'homeXYZ',
@ -115,17 +103,17 @@ const PrinterMovementPanel = ({ printerId }) => {
align='center'
justify='center'
gap='small'
style={{ height: '100%' }}
style={{ height: '100%', minHeight: '120px' }}
>
<Button
icon={<ArrowUpOutlined />}
icon={<ArrowUpIcon />}
onClick={() => {
handleMoveAxisClick('Y', false)
}}
/>
<Space>
<Button
icon={<ArrowLeftOutlined />}
icon={<ArrowLeftIcon />}
onClick={() => {
handleMoveAxisClick('X', false)
}}
@ -134,17 +122,17 @@ const PrinterMovementPanel = ({ printerId }) => {
menu={{ items: homeAxisButtonItems }}
placement='bottom'
>
<Button icon={<HomeOutlined />}></Button>
<Button icon={<HomeIcon />}></Button>
</Dropdown>
<Button
icon={<ArrowRightOutlined />}
icon={<ArrowRightIcon />}
onClick={() => {
handleMoveAxisClick('X', true)
}}
/>
</Space>
<Button
icon={<ArrowDownOutlined />}
icon={<ArrowDownIcon />}
onClick={() => {
handleMoveAxisClick('Y', true)
}}
@ -157,10 +145,10 @@ const PrinterMovementPanel = ({ printerId }) => {
align='center'
justify='center'
gap='small'
style={{ height: '100%' }}
style={{ height: '100%', minHeight: '120px' }}
>
<Button
icon={<ArrowUpOutlined />}
icon={<ArrowUpIcon />}
onClick={() => {
handleMoveAxisClick('Z', true)
}}
@ -172,7 +160,7 @@ const PrinterMovementPanel = ({ printerId }) => {
}}
/>
<Button
icon={<ArrowDownOutlined />}
icon={<ArrowDownIcon />}
onClick={() => {
handleMoveAxisClick('Z', false)
}}
@ -180,21 +168,21 @@ const PrinterMovementPanel = ({ printerId }) => {
</Flex>
</Card>
<Card size='small' title='E'>
<Flex vertical align='center' justify='center' gap='small'>
<Flex
vertical
align='center'
justify='center'
gap='small'
style={{ height: '100%', minHeight: '120px' }}
>
<Button
icon={<ArrowUpOutlined />}
icon={<ArrowUpIcon />}
onClick={() => {
handleMoveAxisClick('E', true)
}}
/>
<Button
icon={<UnloadIcon />}
onClick={() => {
handleUnloadFilamentClick()
}}
/>
<Button
icon={<ArrowDownOutlined />}
icon={<ArrowDownIcon />}
onClick={() => {
handleMoveAxisClick('E', false)
}}
@ -202,6 +190,7 @@ const PrinterMovementPanel = ({ printerId }) => {
</Flex>
</Card>
</Flex>
<Flex vertical gap='small'>
<Radio.Group
onChange={handlePosRadioChange}

View File

@ -0,0 +1,256 @@
import React, { useContext, useState, useEffect } from 'react'
import {
Typography,
Spin,
Flex,
Space,
Collapse,
InputNumber,
Switch,
Descriptions
} from 'antd'
import { LoadingOutlined, CaretRightOutlined } from '@ant-design/icons'
import { SocketContext } from '../context/SocketContext'
import styled from 'styled-components'
import PropTypes from 'prop-types'
const { Text } = Typography
const CustomCollapse = styled(Collapse)`
.ant-collapse-header {
padding: 0 !important;
}
.ant-collapse-content-box {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
`
const PrinterPositionPanel = ({
printerId,
showControls = true,
showMoreInfo = true,
shouldUnsubscribe = true
}) => {
const [positionData, setPositionData] = useState({
speed_factor: 1.0, // eslint-disable-line
speed: 100,
extrude_factor: 1.0, // eslint-disable-line
absolute_foordinates: true, // eslint-disable-line
absolute_extrude: false, // eslint-disable-line
homing_origin: [0.0, 0.0, 0.0, 0.0], // eslint-disable-line
position: [0.0, 0.0, 0.0, 0.0],
gcode_position: [0.0, 0.0, 0.0, 0.0] // eslint-disable-line
})
const [initialized, setInitialized] = useState(false)
const { socket } = useContext(SocketContext)
useEffect(() => {
const params = {
printerId,
objects: {
toolhead: null,
gcode_move: null // eslint-disable-line
}
}
const notifyPositionStatusUpdate = (statusUpdate) => {
console.log(statusUpdate)
if (statusUpdate?.toolhead) {
setPositionData((prevData) => ({
...prevData,
...statusUpdate.toolhead
}))
}
if (statusUpdate?.gcode_move) {
setPositionData((prevData) => ({
...prevData,
...statusUpdate.gcode_move
}))
}
}
if (!initialized && socket) {
setInitialized(true)
socket.on('connect', () => {
console.log('Connected to socket!')
socket.emit('printer.objects.subscribe', params)
socket.emit('printer.objects.query', params)
})
console.log('Subscribing to position data')
socket.emit('printer.objects.subscribe', params)
socket.emit('printer.objects.query', params)
socket.on('notify_status_update', notifyPositionStatusUpdate)
}
return () => {
if (socket && initialized && shouldUnsubscribe) {
console.log('Unsubscribing...')
socket.off('notify_status_update', notifyPositionStatusUpdate)
socket.emit('printer.objects.unsubscribe', params)
}
}
}, [socket, initialized, printerId, shouldUnsubscribe])
const handleSetSpeedFactor = (value) => {
if (socket) {
socket.emit('printer.gcode.script', {
printerId,
script: `M220 S${value * 100}`
})
}
}
const handleSetExtrudeFactor = (value) => {
if (socket) {
socket.emit('printer.gcode.script', {
printerId,
script: `M221 S${value * 100}`
})
}
}
const moreInfoItems = [
{
key: '1',
label: 'More Position Data',
children: (
<>
<Flex vertical gap={'middle'}>
<Text>GCode Position:</Text>
<Descriptions column={1} size='small' bordered>
<Descriptions.Item label='X'>
{positionData.gcode_position[0].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Y'>
{positionData.gcode_position[1].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Z'>
{positionData.gcode_position[2].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='E'>
{positionData.gcode_position[3].toFixed(2)}mm
</Descriptions.Item>
</Descriptions>
<Text>Homing Origin:</Text>
<Descriptions
column={1}
size='small'
bordered
style={{ flexGrow: 1 }}
>
<Descriptions.Item label='X' span={1}>
{positionData.homing_origin[0].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Y' span={1}>
{positionData.homing_origin[1].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Z' span={1}>
{positionData.homing_origin[2].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='E'>
{positionData.homing_origin[3].toFixed(2)}mm
</Descriptions.Item>
</Descriptions>
</Flex>
</>
)
}
]
return (
<div style={{ minWidth: 190 }}>
{positionData ? (
<Flex vertical gap='middle'>
<Flex vertical gap={'middle'}>
<Descriptions column={1} size='small' bordered>
<Descriptions.Item label='X'>
{positionData.position[0].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Y'>
{positionData.position[1].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='Z'>
{positionData.position[2].toFixed(2)}mm
</Descriptions.Item>
<Descriptions.Item label='E'>
{positionData.position[3].toFixed(2)}mm
</Descriptions.Item>
</Descriptions>
<Descriptions column={1} size='small' bordered>
<Descriptions.Item label='Current Speed'>
{positionData.speed}mm/s
</Descriptions.Item>
</Descriptions>
{showControls && (
<>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Speed Factor:</Text>
<InputNumber
value={positionData.speed_factor}
min={0.1}
max={2}
step={0.1}
style={{ width: '100px' }}
onChange={(value) => handleSetSpeedFactor(value)}
/>
</Space>
<Space direction='horizontal'>
<Text>Extrude Factor:</Text>
<InputNumber
value={positionData.extrude_factor}
min={0.1}
max={2}
step={0.1}
style={{ width: '100px' }}
onChange={(value) => handleSetExtrudeFactor(value)}
/>
</Space>
<Space direction='horizontal'>
<Text>Absolute Coordinates:</Text>
<Switch
checked={positionData.absolute_coordinates}
disabled={true}
/>
</Space>
</Space>
</>
)}
</Flex>
{showMoreInfo && (
<CustomCollapse
ghost
size='small'
items={moreInfoItems}
expandIcon={({ isActive }) => (
<CaretRightOutlined rotate={isActive ? 90 : 0} />
)}
/>
)}
</Flex>
) : (
<Flex justify='centre'>
<Spin indicator={<LoadingOutlined spin />} size='large' />
</Flex>
)}
</div>
)
}
PrinterPositionPanel.propTypes = {
printerId: PropTypes.string.isRequired,
showControls: PropTypes.bool,
showMoreInfo: PropTypes.bool,
shouldUnsubscribe: PropTypes.bool
}
export default PrinterPositionPanel

View File

@ -1,6 +1,15 @@
// PrinterSelect.js
import PropTypes from 'prop-types'
import { Badge, Progress, Flex, Space, Tag, Typography, Button } from 'antd'
import {
Badge,
Progress,
Flex,
Space,
Tag,
Typography,
Button,
Tooltip
} from 'antd'
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
import { CaretRightOutlined } from '@ant-design/icons'
@ -130,6 +139,10 @@ const PrinterState = ({
) : null}
{showControls && currentState.type === 'printing' ? (
<Space.Compact>
<Tooltip
title={currentState.type === 'printing' ? 'Pause' : 'Resume'}
arrow={false}
>
<Button
onClick={() => {
if (currentState.type === 'printing') {
@ -146,7 +159,9 @@ const PrinterState = ({
type='text'
icon={
currentState.type === 'printing' ? (
<PauseIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
<PauseIcon
style={{ fontSize: '12px', marginBottom: '3px' }}
/>
) : (
<CaretRightOutlined
style={{ fontSize: '10px', marginBottom: '3px' }}
@ -154,6 +169,8 @@ const PrinterState = ({
)
}
></Button>
</Tooltip>
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
socket.emit('printer.print.cancel', {
@ -166,6 +183,7 @@ const PrinterState = ({
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
</Space.Compact>
) : null}
</Flex>

View File

@ -1,5 +1,5 @@
// PrinterTemperaturePanel.js
import React, { useContext, useState, useEffect } from 'react'
import React, { useContext, useState, useEffect, useCallback } from 'react'
import {
Progress,
Typography,
@ -52,19 +52,10 @@ const PrinterTemperaturePanel = ({
)
const { socket } = useContext(SocketContext)
const [initialized, setInitialized] = useState(false)
useEffect(() => {
const params = {
printerId,
objects: {
extruder: null,
heater_bed: null // eslint-disable-line
}
}
const notifyStatusUpdate = (statusUpdate) => {
var temperatureObject = {
...temperatureData
const notifyTemperatureStatusUpdate = useCallback((statusUpdate) => {
setTemperatureData((prev) => {
const temperatureObject = {
...prev
}
if (statusUpdate?.extruder?.temperature !== undefined) {
temperatureObject.hotEnd.current = statusUpdate?.extruder?.temperature
@ -93,35 +84,38 @@ const PrinterTemperaturePanel = ({
temperatureObject.heatedBed.power = statusUpdate?.heater_bed?.power
}
setTemperatureData(temperatureObject)
}
if (!initialized && socket) {
setInitialized(true)
socket.on('connect', () => {
console.log('Connected to socket!')
socket.emit('printer.objects.subscribe', params)
socket.emit('printer.objects.query', params)
return temperatureObject
})
}, [])
console.log('Subscribing to temperature data')
useEffect(() => {
const params = {
printerId,
objects: {
extruder: null,
heater_bed: null // eslint-disable-line
}
}
if (socket.connected == true) {
console.log('Printer Temperature Panel is subscribing...')
socket.emit('printer.objects.subscribe', params)
socket.emit('printer.objects.query', params)
socket.on('notify_status_update', notifyStatusUpdate)
socket.on('notify_status_update', notifyTemperatureStatusUpdate)
}
return () => {
if (socket && initialized && shouldUnsubscribe == true) {
console.log('Unsubscribing...')
socket.off('notify_status_update', notifyStatusUpdate)
if (socket && shouldUnsubscribe == true) {
console.log('Printer Temperature Panel is unsubscribing...')
socket.off('notify_status_update', notifyTemperatureStatusUpdate)
socket.emit('printer.objects.unsubscribe', params)
}
// Cleanup code here, like:
// - Removing event listeners
// - Clearing timers
// - Closing sockets
}
}, [socket, initialized, printerId])
}, [
socket,
socket.connected,
printerId,
notifyTemperatureStatusUpdate,
shouldUnsubscribe
])
const handleSetTemperatureClick = (target, value) => {
if (socket) {

View File

@ -8,6 +8,7 @@ import moment from 'moment'
import TimeDisplay from '../common/TimeDisplay'
import PlusMinusIcon from '../../Icons/PlusMinusIcon'
import SubJobIcon from '../../Icons/SubJobIcon'
import PlayCircleIcon from '../../Icons/PlayCircleIcon'
const { Text } = Typography
@ -87,6 +88,8 @@ const StockEventTable = ({ stockEvents }) => {
return <SubJobIcon />
case 'audit':
return <AuditOutlined />
case 'initial':
return <PlayCircleIcon />
default:
return null
}
@ -116,8 +119,8 @@ const StockEventTable = ({ stockEvents }) => {
key: 'value',
width: 100,
sorter: (a, b) => a.value - b.value,
render: (value) => {
const formattedValue = value.toFixed(2) + 'g'
render: (value, record) => {
const formattedValue = value.toFixed(2) + record.unit
return (
<Text type={value < 0 ? 'danger' : 'success'}>
{value > 0 ? '+' + formattedValue : formattedValue}
@ -126,7 +129,7 @@ const StockEventTable = ({ stockEvents }) => {
}
},
{
title: 'ID',
title: 'Linked ID',
width: 100,
render: (record) => {
if (record.subJob) {
@ -205,6 +208,7 @@ const StockEventTable = ({ stockEvents }) => {
columns={columns}
rowKey={(record) => record._id}
pagination={false}
scroll={{ x: 'max-content' }}
/>
)
}

View File

@ -1,14 +1,11 @@
import PropTypes from 'prop-types'
import { Typography, Tag } from 'antd' // eslint-disable-line
import {
CheckCircleOutlined,
PauseCircleOutlined,
QuestionCircleOutlined,
PlayCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons' // eslint-disable-line
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import PauseCircleIcon from '../../Icons/PauseCircleIcon'
import XMarkCircleIcon from '../../Icons/XMarkCircleIcon'
import CheckCircleIcon from '../../Icons/CheckCircleIcon'
const SubJobCounter = ({
job,
@ -18,7 +15,7 @@ const SubJobCounter = ({
const { socket } = useContext(SocketContext)
const [initialized, setInitialized] = useState(false)
var badgeStatus = 'unknown'
var badgeIcon = <QuestionCircleOutlined />
var badgeIcon = <QuestionCircleIcon />
const [subJobs, setSubJobs] = useState(job.subJobs)
const [count, setCount] = useState(0)
@ -47,30 +44,30 @@ const SubJobCounter = ({
switch (state.type) {
case 'draft':
badgeStatus = 'default'
badgeIcon = <QuestionCircleOutlined />
badgeIcon = <QuestionCircleIcon />
break
case 'printing':
badgeStatus = 'processing'
badgeIcon = <PlayCircleOutlined />
badgeIcon = <CheckCircleIcon />
break
case 'complete':
badgeStatus = 'success'
badgeIcon = <CheckCircleOutlined />
badgeIcon = <CheckCircleIcon />
break
case 'failed':
badgeStatus = 'error'
badgeIcon = <CloseCircleOutlined />
badgeIcon = <XMarkCircleIcon />
break
case 'queued':
badgeStatus = 'warning'
badgeIcon = <PauseCircleOutlined />
badgeIcon = <PauseCircleIcon />
break
case 'paused':
badgeStatus = 'warning'
badgeIcon = <PauseCircleOutlined />
badgeIcon = <PauseCircleIcon />
break
case 'cancelled':
badgeIcon = <CloseCircleOutlined />
badgeIcon = <XMarkCircleIcon />
break
default:
badgeStatus = 'default'

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'
import { Badge, Progress, Flex, Button, Space, Tag } from 'antd' // eslint-disable-line
import { Badge, Progress, Flex, Button, Space, Tag, Tooltip } from 'antd' // eslint-disable-line
import { CaretRightOutlined } from '@ant-design/icons' // eslint-disable-line
import React, { useState, useContext, useEffect } from 'react'
import { SocketContext } from '../context/SocketContext'
@ -112,6 +112,10 @@ const SubJobState = ({
{showControls &&
(currentState.type === 'printing' || currentState.type === 'paused') ? (
<Space.Compact>
<Tooltip
title={currentState.type === 'printing' ? 'Pause' : 'Resume'}
arrow={false}
>
<Button
onClick={() => {
if (currentState.type === 'printing') {
@ -128,7 +132,9 @@ const SubJobState = ({
type='text'
icon={
currentState.type === 'printing' ? (
<PauseIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
<PauseIcon
style={{ fontSize: '12px', marginBottom: '3px' }}
/>
) : (
<CaretRightOutlined
style={{ fontSize: '10px', marginBottom: '3px' }}
@ -136,6 +142,8 @@ const SubJobState = ({
)
}
></Button>
</Tooltip>
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
socket.emit('printer.print.cancel', {
@ -148,9 +156,11 @@ const SubJobState = ({
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
</Space.Compact>
) : null}
{showControls && currentState.type === 'queued' ? (
<Tooltip title='Cancel' arrow={false}>
<Button
onClick={() => {
socket.emit('server.job_queue.cancel', {
@ -159,19 +169,26 @@ const SubJobState = ({
}}
style={{ height: '22px' }}
type='text'
icon={<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />}
icon={
<XMarkIcon style={{ fontSize: '12px', marginBottom: '3px' }} />
}
/>
</Tooltip>
) : null}
{showControls && currentState.type === 'draft' ? (
<Space>
<Tooltip title='Delete' arrow={false}>
<Button
onClick={() => {
console.log('Hello')
}}
type='text'
style={{ height: 'unset' }}
icon={<BinIcon style={{ fontSize: '14px', marginBottom: '2px' }} />}
icon={
<BinIcon style={{ fontSize: '14px', marginBottom: '2px' }} />
}
/>
</Tooltip>
</Space>
) : null}
</Flex>

View File

@ -1,17 +1,15 @@
import { Input, Flex, List, Typography, Modal, Spin, message, Form } from 'antd'
import React, { createContext, useEffect, useState, useRef } from 'react'
import axios from 'axios'
import {
LoadingOutlined,
PrinterOutlined,
PlayCircleOutlined
} from '@ant-design/icons'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import PrinterState from '../common/PrinterState'
import JobState from '../common/JobState'
import IdText from '../common/IdText'
import config from '../../../config'
import JobIcon from '../../Icons/JobIcon'
import PrinterIcon from '../../Icons/PrinterIcon'
const SpotlightContext = createContext()
@ -316,10 +314,10 @@ const SpotlightProvider = ({ children }) => {
<Flex gap={'middle'} align='center'>
<Text>
{item.printer ? (
<PrinterOutlined style={{ fontSize: '20px' }} />
<PrinterIcon style={{ fontSize: '20px' }} />
) : null}
{item.job ? (
<PlayCircleOutlined style={{ fontSize: '20px' }} />
<JobIcon style={{ fontSize: '20px' }} />
) : null}
</Text>
<Flex

View File

@ -0,0 +1,71 @@
import React, { createContext, useContext, useState } from 'react'
import { theme } from 'antd'
import PropTypes from 'prop-types'
const ThemeContext = createContext()
export const ThemeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(true)
const [isCompact, setIsCompact] = useState(false)
const toggleTheme = () => {
setIsDarkMode(!isDarkMode)
}
const toggleCompact = () => {
setIsCompact(!isCompact)
}
const getThemeAlgorithm = () => {
var baseAlgorithm
if (isDarkMode == true) {
baseAlgorithm = theme.darkAlgorithm
} else {
baseAlgorithm = theme.defaultAlgorithm
}
return isCompact ? [theme.compactAlgorithm, baseAlgorithm] : [baseAlgorithm]
}
const themeConfig = {
algorithm: getThemeAlgorithm(),
token: {
colorPrimary: '#007AFF',
colorSuccess: '#32D74B',
colorWarning: '#FF9F0A',
colorInfo: '#007AFF',
colorLink: '#5AC8F5',
borderRadius: '10px'
},
components: {
Layout: {
headerBg: isDarkMode ? '#141414' : '#ffffff'
}
}
}
return (
<ThemeContext.Provider
value={{
isDarkMode,
toggleTheme,
isCompact,
toggleCompact,
themeConfig
}}
>
{children}
</ThemeContext.Provider>
)
}
ThemeProvider.propTypes = {
children: PropTypes.node.isRequired
}
export const useThemeContext = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider')
}
return context
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/arrowdownicon.min.svg'
const ArrowDownIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ArrowDownIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/arrowlefticon.min.svg'
const ArrowLeftIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ArrowLeftIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/arrowrighticon.min.svg'
const ArrowRightIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ArrowRightIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/arrowupicon.min.svg'
const ArrowUpIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ArrowUpIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/checkcircleicon.min.svg'
const CheckCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default CheckCircleIcon

View File

@ -0,0 +1,9 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/exclamationoctagonicon.min.svg'
const ExclamationOctagonIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default ExclamationOctagonIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/homeicon.min.svg'
const HomeIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default HomeIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/pausecircleicon.min.svg'
const PauseCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PauseCircleIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/playcircleicon.min.svg'
const PlayCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PlayCircleIcon

View File

@ -0,0 +1,9 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/questioncircleicon.min.svg'
const QuestionCircleIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default QuestionCircleIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/stockeventicon.min.svg'
const StockEventIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default StockEventIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/stopcircleicon.min.svg'
const StopCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default StopCircleIcon

View File

@ -0,0 +1,7 @@
import React from 'react'
import Icon from '@ant-design/icons'
import { ReactComponent as CustomIconSvg } from '../../assets/icons/xmarkcircleicon.min.svg'
const XMarkCircleIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default XMarkCircleIcon

View File

@ -4,8 +4,8 @@ const config = {
wsUrl: 'ws://localhost:8081'
},
production: {
backendUrl: 'https://api.farmcontrol.com', // Replace with your production backend URL
wsUrl: 'wss://api.farmcontrol.com' // Replace with your production WebSocket URL
backendUrl: 'http://localhost:8080', // Replace with your production backend URL
wsUrl: 'ws://localhost:8081' // Replace with your production WebSocket URL
}
}