Compare commits

...

6 Commits

Author SHA1 Message Date
d982b8dfb3 Refactor components to improve logging and ID handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Removed console.log statements from FilePreview and ObjectForm components for cleaner code.
- Replaced console.log with loglevel in KeyboardShortcut for better logging management.
- Updated ID handling in ObjectForm, ObjectTable, PrinterMiscPanel, PrinterPositionPanel, and PrinterTemperaturePanel components to ensure IDs are consistently converted to lowercase, enhancing data integrity.
2026-08-01 16:18:37 +01:00
a2d27c692a Add PrinterControlButtons component for enhanced printer action management
- Introduced PrinterControlButtons to encapsulate start, pause, and cancel actions for printers, improving user interaction.
- Updated ControlPrinter component to integrate PrinterControlButtons, enhancing layout with Flex for better alignment.
- Adjusted button states based on printer status and loading state, ensuring appropriate user feedback and action availability.
2026-08-01 15:54:00 +01:00
e3544ef2e2 Refactor PrinterPositionPanel and PrinterTemperaturePanel for improved styling and layout
- Introduced controlLabelStyle and controlInputStyle for consistent styling of control elements in PrinterPositionPanel.
- Replaced Space components with Flex for better alignment and layout management in PrinterPositionPanel.
- Updated input field widths in PrinterTemperaturePanel for enhanced usability and consistency.
2026-08-01 15:37:51 +01:00
e52798d4ac Enhance printer panels to handle offline state more effectively
- Added an `offline` prop to PrinterTemperaturePanel, PrinterMiscPanel, and PrinterPositionPanel components to manage state when printers are offline.
- Implemented useEffect hooks in each panel to reset relevant data when offline, ensuring accurate display of printer status.
- Updated ControlPrinter component to pass the `offline` state based on the printer's online status, improving user experience during offline scenarios.
2026-08-01 15:28:48 +01:00
f3d6bebc38 Integrate TableStateProvider into App and Dashboard components for enhanced state management
- Added TableStateProvider to the App component, encapsulating routing logic to manage table states effectively.
- Removed redundant TableStateProvider from the DashboardLayout component, streamlining the component structure.
- Updated ControlPrinter and ObjectActions components to improve action visibility and navigation button rendering based on slicer integration state.
- Enhanced action filtering in ObjectActions to eliminate unnecessary dividers, improving menu item clarity.
2026-08-01 15:21:58 +01:00
12cae32e33 Enhance Thumbnail and ApiServerContext components for improved functionality
- Removed the default border style from the Thumbnail component to streamline its appearance.
- Updated the Thumbnail component to dynamically set the width in the card styles, enhancing layout flexibility.
- Modified the fetchFileThumbnail function in ApiServerContext to include an optional parameter for handling 404 errors, improving error management during thumbnail fetching.
- Added a thumbnail property to the GCodeFile model, enabling thumbnail support for GCode files.
2026-08-01 15:11:02 +01:00
15 changed files with 534 additions and 280 deletions

View File

@ -34,6 +34,7 @@ import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import AuthLaunch from './components/App/AppLaunch.jsx'
import { TableStateProvider } from './components/Dashboard/context/TableStateContext.jsx'
const SlicerIntegration = lazy(
() =>
import('./components/Dashboard/Production/Printers/SlicerIntegration.jsx')
@ -86,76 +87,78 @@ const AppContent = () => {
<NotificationProvider>
<SpotlightProvider>
<ActionsModalProvider>
<Routes>
<Route
path='/applaunch'
element={<AuthLaunch />}
/>
<Route
path='/dashboard/electron/spotlightcontent'
element={
<PrivateRoute
component={() => (
<ElectronSpotlightContentPage />
)}
/>
}
/>
<Route
path='/'
element={
<PrivateRoute
component={() => (
<Navigate
to='/dashboard/production/overview'
replace
/>
)}
/>
}
/>
<Route
path='/auth/callback'
element={<AuthCallback />}
/>
<Route
path='/auth/marketplace/callback'
element={<MarketplaceAuthCallback />}
/>
<Route
path='/email/notification'
element={<EmailNotificationTemplate />}
/>
<Route
path='/slicer'
element={<SlicerIntegration />}
/>
<TableStateProvider>
<Routes>
<Route
path='/applaunch'
element={<AuthLaunch />}
/>
<Route
path='/dashboard/electron/spotlightcontent'
element={
<PrivateRoute
component={() => (
<ElectronSpotlightContentPage />
)}
/>
}
/>
<Route
path='/'
element={
<PrivateRoute
component={() => (
<Navigate
to='/dashboard/production/overview'
replace
/>
)}
/>
}
/>
<Route
path='/auth/callback'
element={<AuthCallback />}
/>
<Route
path='/auth/marketplace/callback'
element={<MarketplaceAuthCallback />}
/>
<Route
path='/email/notification'
element={<EmailNotificationTemplate />}
/>
<Route
path='/slicer'
element={<SlicerIntegration />}
/>
<Route
path='/dashboard'
element={
<PrivateRoute
component={() => <Dashboard />}
/>
}
>
{ProductionRoutes}
{InventoryRoutes}
{FinanceRoutes}
{SalesRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>
<Route
path='*'
element={
<AppError
message='Error 404. Page not found.'
showRefresh={false}
/>
}
/>
</Routes>
<Route
path='/dashboard'
element={
<PrivateRoute
component={() => <Dashboard />}
/>
}
>
{ProductionRoutes}
{InventoryRoutes}
{FinanceRoutes}
{SalesRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>
<Route
path='*'
element={
<AppError
message='Error 404. Page not found.'
showRefresh={false}
/>
}
/>
</Routes>
</TableStateProvider>
</ActionsModalProvider>
</SpotlightProvider>
</NotificationProvider>

View File

@ -12,7 +12,6 @@ import DashboardBreadcrumb from './common/DashboardBreadcrumb'
import DeveloperSidebar from './Developer/DeveloperSidebar'
import { useThemeContext } from './context/ThemeContext'
import { MessageProvider } from './context/MessageContext'
import { TableStateProvider } from './context/TableStateContext'
const { Content } = Layout
@ -29,42 +28,40 @@ const DashboardLayout = ({ children }) => {
return (
<MessageProvider>
<TableStateProvider>
<Layout
style={{ height: 'var(--unit-100vh)' }}
className={isDarkMode ? 'dark-mode' : 'light-mode'}
>
<DashboardNavigation />
<Layout>
{isProduction ? (
<ProductionSidebar />
) : isInventory ? (
<InventorySidebar />
) : isFinance ? (
<FinanceSidebar />
) : isSales ? (
<SalesSidebar />
) : isManagement ? (
<ManagementSidebar />
) : isDeveloper ? (
<DeveloperSidebar />
) : (
<ProductionSidebar /> // Default to production sidebar
)}
<Layout style={{ padding: '24px' }}>
<Content>
<Flex vertical style={{ height: '100%' }} gap='20px'>
<Flex justify='space-between'>
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
</Flex>
{children}
<Layout
style={{ height: 'var(--unit-100vh)' }}
className={isDarkMode ? 'dark-mode' : 'light-mode'}
>
<DashboardNavigation />
<Layout>
{isProduction ? (
<ProductionSidebar />
) : isInventory ? (
<InventorySidebar />
) : isFinance ? (
<FinanceSidebar />
) : isSales ? (
<SalesSidebar />
) : isManagement ? (
<ManagementSidebar />
) : isDeveloper ? (
<DeveloperSidebar />
) : (
<ProductionSidebar /> // Default to production sidebar
)}
<Layout style={{ padding: '24px' }}>
<Content>
<Flex vertical style={{ height: '100%' }} gap='20px'>
<Flex justify='space-between'>
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
</Flex>
</Content>
</Layout>
{children}
</Flex>
</Content>
</Layout>
</Layout>
</TableStateProvider>
</Layout>
</MessageProvider>
)
}

View File

@ -33,6 +33,7 @@ import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
import PrinterControlButtons from '../../common/PrinterControlButtons.jsx'
const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel)
@ -182,6 +183,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
>
<PrinterTemperaturePanel
id={printerId}
offline={!objectFormState.objectData?.online}
disabled={
!objectFormState.objectData?.online || objectFormState.loading
}
@ -243,6 +245,7 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
>
<PrinterMiscPanel
id={printerId}
offline={!objectFormState.objectData?.online}
disabled={
!objectFormState.objectData?.online || objectFormState.loading
}
@ -272,7 +275,8 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
visibleActions={{
edit: false,
info: !slicerIntegration,
control: !slicerIntegration
control: !slicerIntegration,
newPrinterProfile: !slicerIntegration
}}
objectData={objectFormState.objectData}
/>
@ -318,11 +322,26 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
/>
</Space>
</Space>
<ObjectTableNavigationButtons
disabled={objectFormState.loading}
_id={printerId}
objectType='printer'
/>
<Flex gap='small'>
<PrinterControlButtons
callAction={(action) =>
actionHandlerRef.current?.callAction(action)
}
objectData={objectFormState.objectData}
disabled={
!objectFormState.objectData?.online || objectFormState.loading
}
loading={objectFormState.loading}
/>
{!slicerIntegration && (
<ObjectTableNavigationButtons
showEndingDivider={true}
disabled={objectFormState.loading}
_id={printerId}
objectType='printer'
/>
)}
</Flex>
</Flex>
<AlertsDisplay

View File

@ -36,8 +36,6 @@ const FilePreview = ({ file, style = {} }) => {
}
}, [file._id, file?.type, fetchPreview, token])
console.log(file)
if (loading == true || !file?.type) {
return <LoadingPlaceholder message={'Loading file preview...'} />
}

View File

@ -1,6 +1,10 @@
import { useEffect, useRef, cloneElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Popover, Typography } from 'antd'
import loglevel from 'loglevel'
import config from '../../../config'
const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel)
const MODIFIER_ALIASES = {
cmd: 'meta',
@ -59,7 +63,9 @@ const KeyboardShortcut = ({
}
if (log) {
console.log('Key Down:', key, 'Pressed Keys:', [...pressedKeysRef.current])
logger.info('Key Down:', key, 'Pressed Keys:', [
...pressedKeysRef.current
])
}
if (

View File

@ -37,54 +37,95 @@ function filterActionsByVisibility(actions, visibleActions) {
})
}
function cleanDividers(items) {
if (!Array.isArray(items) || items.length === 0) return []
const cleaned = []
for (const item of items) {
if (!item) continue
if (item.type === 'divider') {
if (cleaned.length === 0) continue
if (cleaned[cleaned.length - 1]?.type === 'divider') continue
cleaned.push(item)
continue
}
if (item.children && Array.isArray(item.children)) {
const children = cleanDividers(item.children)
if (children.length === 0) continue
cleaned.push({ ...item, children })
continue
}
cleaned.push(item)
}
if (cleaned[cleaned.length - 1]?.type === 'divider') {
cleaned.pop()
}
return cleaned
}
// Recursively map actions to AntD Dropdown items
function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
return actions.map((action) => {
if (action.type === 'divider') {
return { type: 'divider' }
}
const actionUrl = action.url ? action.url(id) : undefined
var disabled = actionUrl && actionUrl === currentUrlWithActions
var visible = true
const { userProfile } = useContext(AuthContext)
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled = action.disabled({ ...objectData, _user: userProfile })
} else {
disabled = action.disabled
function mapActionsToMenuItems(
actions,
currentUrlWithActions,
id,
objectData,
userProfile
) {
return cleanDividers(
actions.map((action) => {
if (action.type === 'divider') {
return { type: 'divider' }
}
}
const actionUrl = action.url ? action.url(id) : undefined
if (action.visible) {
if (typeof action.visible === 'function') {
visible = action.visible(objectData)
} else {
visible = action.visible
var disabled = actionUrl && actionUrl === currentUrlWithActions
var visible = true
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled = action.disabled({ ...objectData, _user: userProfile })
} else {
disabled = action.disabled
}
}
}
const item = {
key: action.key || action.name,
label: action.label,
danger: action?.danger || false,
icon: action.icon ? createElement(action.icon) : undefined,
disabled
}
if (action.children && Array.isArray(action.children)) {
item.children = mapActionsToMenuItems(
action.children,
currentUrlWithActions,
id,
objectData
)
}
if (visible == true) {
if (action.visible) {
if (typeof action.visible === 'function') {
visible = action.visible(objectData)
} else {
visible = action.visible
}
}
if (visible != true) {
return null
}
const item = {
key: action.key || action.name,
label: action.label,
danger: action?.danger || false,
icon: action.icon ? createElement(action.icon) : undefined,
disabled
}
if (action.children && Array.isArray(action.children)) {
item.children = mapActionsToMenuItems(
action.children,
currentUrlWithActions,
id,
objectData,
userProfile
)
}
return item
}
})
})
)
}
const stripActionParam = (pathname, search) => {
@ -108,6 +149,7 @@ const ObjectActions = ({
const navigate = useNavigate()
const location = useLocation()
const { showActionsModal } = useActionsModal()
const { userProfile } = useContext(AuthContext)
// Get current url without 'action' param
const currentUrlWithoutActions = stripActionParam(
location.pathname,
@ -120,10 +162,13 @@ const ObjectActions = ({
visibleActions
)
const filteredActions = visibilityFilteredActions.filter(
(action) =>
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
const filteredActions = cleanDividers(
visibilityFilteredActions.filter(
(action) =>
action.type === 'divider' ||
typeof action.url !== 'function' ||
action.url(id) !== currentUrlWithoutActions
)
)
const currentUrlWithActions = location.pathname + location.search
@ -134,7 +179,8 @@ const ObjectActions = ({
filteredActions,
currentUrlWithActions,
id,
objectData
objectData,
userProfile
),
onClick: (info) => {
// Find the action by key

View File

@ -127,8 +127,6 @@ const ObjectForm = forwardRef(
searchParams.delete(actionParam)
const newSearch = searchParams.toString()
console.log('newSearch', newSearch)
console.log('location.pathname', location.pathname)
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
},
@ -537,7 +535,7 @@ const ObjectForm = forwardRef(
}
const activityUnsubscribe = subscribeToObjectActivity(
id,
id?.toLowerCase(),
type,
activityHandler
)
@ -565,7 +563,7 @@ const ObjectForm = forwardRef(
}
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
id,
id?.toLowerCase(),
type,
objectUpdateHandler
)

View File

@ -693,7 +693,7 @@ const ObjectTable = forwardRef(
// Subscribe to new items only
newItemIds.forEach((itemId) => {
const unsubscribe = subscribeToObjectUpdates(
itemId,
itemId?.toLowerCase(),
type,
(updateData) => {
updateEventHandlerRef.current(itemId, updateData)

View File

@ -0,0 +1,89 @@
import { createElement, useContext } from 'react'
import { Button, Space } from 'antd'
import PropTypes from 'prop-types'
import { getModelByName } from '../../../database/ObjectModels'
import { AuthContext } from '../context/AuthContext'
import PlayCircleIcon from '../../Icons/PlayCircleIcon'
import PauseCircleIcon from '../../Icons/PauseCircleIcon'
import StopCircleIcon from '../../Icons/StopCircleIcon'
const findAction = (actions, name) => {
for (const action of actions) {
if (action.name === name) return action
if (action.children) {
const found = findAction(action.children, name)
if (found) return found
}
}
return null
}
const isActionDisabled = (action, objectData, userProfile, disabled) => {
if (disabled) return true
if (!action?.disabled) return false
if (typeof action.disabled === 'function') {
return action.disabled({ ...objectData, _user: userProfile })
}
return action.disabled
}
const PrinterControlButtons = ({ callAction, objectData, disabled }) => {
const { userProfile } = useContext(AuthContext)
const model = getModelByName('printer')
const startQueueAction = findAction(model.actions, 'startQueue')
const pauseJobAction = findAction(model.actions, 'pauseJob')
const resumeJobAction = findAction(model.actions, 'resumeJob')
const cancelJobAction = findAction(model.actions, 'cancelJob')
const isPaused = objectData?.state?.type === 'paused'
const startAction = isPaused ? resumeJobAction : startQueueAction
const startActionName = isPaused ? 'resumeJob' : 'startQueue'
return (
<Space size='small'>
<Button
icon={createElement(PlayCircleIcon)}
onClick={() => callAction(startActionName)}
disabled={isActionDisabled(
startAction,
objectData,
userProfile,
disabled
)}
title={startAction?.label}
/>
<Button
icon={createElement(PauseCircleIcon)}
onClick={() => callAction('pauseJob')}
disabled={isActionDisabled(
pauseJobAction,
objectData,
userProfile,
disabled
)}
title={pauseJobAction?.label}
/>
<Button
icon={createElement(StopCircleIcon)}
onClick={() => callAction('cancelJob')}
disabled={isActionDisabled(
cancelJobAction,
objectData,
userProfile,
disabled
)}
title={cancelJobAction?.label}
danger
/>
</Space>
)
}
PrinterControlButtons.propTypes = {
callAction: PropTypes.func.isRequired,
objectData: PropTypes.object.isRequired,
disabled: PropTypes.bool,
loading: PropTypes.bool
}
export default PrinterControlButtons

View File

@ -7,7 +7,12 @@ import merge from 'lodash/merge'
const { Text } = Typography
const PrinterMiscPanel = ({ id, showControls = true, disabled = false }) => {
const PrinterMiscPanel = ({
id,
showControls = true,
disabled = false,
offline = true
}) => {
const { subscribeToObjectEvent, connected, sendObjectAction } =
useContext(ApiServerContext)
const controlsDisabled = disabled || !connected
@ -35,10 +40,22 @@ const PrinterMiscPanel = ({ id, showControls = true, disabled = false }) => {
const [lcdBrightness, setLcdBrightness] = useState(0)
const [beeperValue, setBeeperValue] = useState(0)
useEffect(() => {
if (offline) {
setMiscData({
fan: { speed: 0, target: 0 },
lcdBacklight: { brightness: 0 },
beeper: { value: 0 },
filamentSensor: { enabled: false, filamentDetected: false },
coolingFan: { speed: 0 }
})
}
}, [offline])
useEffect(() => {
if (id && connected == true) {
const miscEventUnsubscribe = subscribeToObjectEvent(
id,
id?.toLowerCase(),
'printer',
'misc',
(event) => {
@ -193,7 +210,8 @@ const PrinterMiscPanel = ({ id, showControls = true, disabled = false }) => {
PrinterMiscPanel.propTypes = {
id: PropTypes.string.isRequired,
showControls: PropTypes.bool,
disabled: PropTypes.bool
disabled: PropTypes.bool,
offline: PropTypes.bool
}
export default PrinterMiscPanel

View File

@ -30,11 +30,23 @@ const CustomCollapse = styled(Collapse)`
}
`
const controlLabelStyle = {
whiteSpace: 'nowrap',
flexShrink: 0,
minWidth: '120px'
}
const controlInputStyle = {
flex: 1,
minWidth: 0
}
const PrinterPositionPanel = ({
id,
showControls = true,
showMoreInfo = true,
disabled = false
disabled = false,
offline = true
}) => {
const { subscribeToObjectEvent, connected, sendObjectAction } =
useContext(ApiServerContext)
@ -55,10 +67,30 @@ const PrinterPositionPanel = ({
minCruiseRatio: 0
})
useEffect(() => {
if (offline) {
setPositionData({
speedFactor: 1.0,
speed: 100,
extrudeFactor: 1.0,
absoluteCoordinates: true,
absoluteExtrude: false,
homingOrigin: [0.0, 0.0, 0.0, 0.0],
toolheadPosition: [0.0, 0.0, 0.0, 0.0],
gcodePosition: [0.0, 0.0, 0.0, 0.0],
livePosition: [0.0, 0.0, 0.0, 0.0],
maxVelocity: 1000,
maxAcceleration: 1000,
squareCornerVelocity: 100,
minCruiseRatio: 0
})
}
}, [offline])
useEffect(() => {
if (id && connected == true) {
const motionEventUnsubscribe = subscribeToObjectEvent(
id,
id?.toLowerCase(),
'printer',
'motion',
(event) => {
@ -73,6 +105,7 @@ const PrinterPositionPanel = ({
}
}
}, [id, connected, subscribeToObjectEvent])
const [speedFactor, setSpeedFactor] = useState(positionData.speedFactor)
const [extrudeFactor, setExtrudeFactor] = useState(positionData.extrudeFactor)
const [maxVelocity, setMaxVelocity] = useState(positionData.maxVelocity)
@ -212,16 +245,22 @@ const PrinterPositionPanel = ({
</Descriptions>
{showControls && (
<>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Max Velocity:</Text>
<Space.Compact block size='small'>
<Flex vertical gap='small' style={{ width: '100%' }}>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '91px' }}>
Max Velocity:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(maxVelocity, 2)}
min={1}
max={10000}
step={5}
style={{ width: '125px' }}
style={controlInputStyle}
suffix='mm/s'
onChange={(value) => setMaxVelocity(value)}
onPressEnter={handleSetMaxVelocity}
@ -237,92 +276,104 @@ const PrinterPositionPanel = ({
Set
</Button>
</Space.Compact>
</Space>
</Flex>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Max Acceleration:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(maxAcceleration, 2)}
min={1}
max={10000}
step={5}
style={{ width: '125px' }}
suffix='mm/s²'
onChange={(value) => setMaxAcceleration(value)}
onPressEnter={handleSetMaxAcceleration}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMaxAcceleration}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '120px' }}>
Max Acceleration:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(maxAcceleration, 2)}
min={1}
max={10000}
step={5}
style={controlInputStyle}
suffix='mm/s²'
onChange={(value) => setMaxAcceleration(value)}
onPressEnter={handleSetMaxAcceleration}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMaxAcceleration}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Flex>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Sqr Corner Vel:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(squareCornerVelocity, 2) || 0}
min={0.1}
max={1000}
step={0.1}
suffix='mm/s'
style={{ width: '125px' }}
onChange={(value) => setSquareCornerVelocity(value)}
onPressEnter={handleSetSquareCornerVelocity}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetSquareCornerVelocity}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '102px' }}>
Sqr Corner Vel:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(squareCornerVelocity, 2) || 0}
min={0.1}
max={1000}
step={0.1}
style={controlInputStyle}
suffix='mm/s'
onChange={(value) => setSquareCornerVelocity(value)}
onPressEnter={handleSetSquareCornerVelocity}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetSquareCornerVelocity}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Flex>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Min Cruise Ratio:</Text>
<Space.Compact block size='small'>
<InputNumber
value={round(minCruiseRatio * 100, 2) || 0}
min={0}
max={100}
step={1}
suffix='%'
style={{ width: '125px' }}
onChange={(value) => setMinCruiseRatio(value / 100)}
onPressEnter={handleSetMinCruiseRatio}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMinCruiseRatio}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Space>
</Space>
</Space>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '114px' }}>
Min Cruise Ratio:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(minCruiseRatio * 100, 2) || 0}
min={0}
max={100}
step={1}
suffix='%'
style={controlInputStyle}
onChange={(value) => setMinCruiseRatio(value / 100)}
onPressEnter={handleSetMinCruiseRatio}
size='small'
disabled={controlsDisabled}
/>
<Button
type='default'
style={{ width: 40 }}
onClick={handleSetMinCruiseRatio}
disabled={controlsDisabled}
>
Set
</Button>
</Space.Compact>
</Flex>
</Flex>
</>
)}
<Text>Homing Origin:</Text>
@ -398,16 +449,22 @@ const PrinterPositionPanel = ({
</Descriptions>
{showControls && (
<>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='horizontal'>
<Text>Speed Factor:</Text>
<Space.Compact block size='small'>
<Flex vertical gap='small' style={{ width: '100%' }}>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '96px' }}>
Speed Factor:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(speedFactor, 2)}
min={0.1}
max={2}
step={0.1}
style={{ width: '125px' }}
style={controlInputStyle}
suffix='%'
onChange={(value) => setSpeedFactor(value)}
onPressEnter={handleSetSpeedFactor}
@ -423,17 +480,23 @@ const PrinterPositionPanel = ({
Set
</Button>
</Space.Compact>
</Space>
</Flex>
<Space direction='horizontal'>
<Text>Extrude Factor:</Text>
<Space.Compact block size='small'>
<Flex align='center' style={{ width: '100%' }}>
<Text style={{ ...controlLabelStyle, minWidth: '105px' }}>
Extrude Factor:
</Text>
<Space.Compact
block
size='small'
style={{ flex: 1, minWidth: 0 }}
>
<InputNumber
value={round(extrudeFactor, 2)}
min={0.1}
max={2}
step={0.1}
style={{ width: '125px' }}
style={controlInputStyle}
suffix='%'
onChange={(value) => setExtrudeFactor(value)}
onPressEnter={handleSetExtrudeFactor}
@ -449,8 +512,8 @@ const PrinterPositionPanel = ({
Set
</Button>
</Space.Compact>
</Space>
</Space>
</Flex>
</Flex>
</>
)}
<Descriptions
@ -509,7 +572,8 @@ PrinterPositionPanel.propTypes = {
id: PropTypes.string.isRequired,
showControls: PropTypes.bool,
showMoreInfo: PropTypes.bool,
disabled: PropTypes.bool
disabled: PropTypes.bool,
offline: PropTypes.bool
}
export default PrinterPositionPanel

View File

@ -38,7 +38,8 @@ const PrinterTemperaturePanel = ({
showExtruder = true,
showBed = true,
showMoreInfo = true,
disabled = false
disabled = false,
offline = true
}) => {
const [temperatureData, setTemperatureData] = useState({
extruder: {
@ -55,6 +56,17 @@ const PrinterTemperaturePanel = ({
ambiant: 0
})
useEffect(() => {
if (offline) {
setTemperatureData({
extruder: { current: 0, target: 0, power: 0 },
bed: { current: 0, target: 0, power: 0 },
pinda: 0,
ambiant: 0
})
}
}, [offline])
const { subscribeToObjectEvent, connected, sendObjectAction } =
useContext(ApiServerContext)
const controlsDisabled = disabled || !connected
@ -72,9 +84,9 @@ const PrinterTemperaturePanel = ({
}, [temperatureData.bed?.target])
useEffect(() => {
if (id && connected == true) {
if (id && connected == true && offline == false) {
const temperatureEventUnsubscribe = subscribeToObjectEvent(
id,
id?.toLowerCase(),
'printer',
'temperature',
(event) => {
@ -88,7 +100,7 @@ const PrinterTemperaturePanel = ({
if (temperatureEventUnsubscribe) temperatureEventUnsubscribe()
}
}
}, [id, connected, subscribeToObjectEvent])
}, [id, connected, subscribeToObjectEvent, offline])
const [extruderTarget, setExtruderTarget] = useState(0)
const [bedTarget, setBedTarget] = useState(0)
@ -177,7 +189,7 @@ const PrinterTemperaturePanel = ({
value={extruderTarget}
min={0}
max={300}
style={{ width: '120px' }}
style={{ width: '140px' }}
addonAfter='°C'
onChange={(value) => setExtruderTarget(value || 0)}
onPressEnter={() =>
@ -241,7 +253,7 @@ const PrinterTemperaturePanel = ({
value={bedTarget}
min={0}
max={300}
style={{ width: '120px' }}
style={{ width: '140px' }}
addonAfter='°C'
onChange={(value) => setBedTarget(value || 0)}
onPressEnter={() =>
@ -309,7 +321,8 @@ PrinterTemperaturePanel.propTypes = {
showBed: PropTypes.bool,
showMoreInfo: PropTypes.bool,
shouldUnsubscribe: PropTypes.bool,
disabled: PropTypes.bool
disabled: PropTypes.bool,
offline: PropTypes.bool
}
export default PrinterTemperaturePanel

View File

@ -42,7 +42,6 @@ const Thumbnail = function Thumbnail({
borderRadius: '5px',
width: size,
height: size,
border: 'none',
...style
}
@ -151,7 +150,7 @@ const Thumbnail = function Thumbnail({
<Card
className={className}
style={cardStyle}
styles={{ body: { padding: 0, height: '100%' } }}
styles={{ body: { padding: 0, height: '100%', width: style?.width } }}
>
<Flex justify='center' align='center' style={{ height: '100%' }}>
{fallback || (

View File

@ -1510,7 +1510,7 @@ const ApiServerProvider = ({ children }) => {
}
}
const fetchFileThumbnail = async (file, size) => {
const fetchFileThumbnail = async (file, size, show404Error = false) => {
try {
const response = await axios.get(
`${config.backendUrl}/files/${file._id}/thumbnail`,
@ -1528,6 +1528,9 @@ const ApiServerProvider = ({ children }) => {
})
return window.URL.createObjectURL(blob)
} catch (err) {
if (err.response.status === 404 && show404Error == false) {
return null
}
console.error(err)
showError(err, () => {
fetchFileThumbnail(file, size)

View File

@ -164,6 +164,7 @@ export const GCodeFile = {
showPreview: false,
showHyperlink: true,
masterFilter: ['.gcode', '.g'],
thumbnail: true,
columnWidth: 200
},
{