mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-14 21:22:20 +00:00
Machine properties and periodic ping (#10381)
* add machine properties * remove non working polyfill * add periodic task * add tests and docs * fix ping task * add int and float type * Update api_version.py --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added `order_queryset` report helper function in [#10439](https://github.com/inventree/InvenTree/pull/10439)
|
||||
- Added much more detailed status information for machines to the API endpoint (including backend and frontend changes) in [#10381](https://github.com/inventree/InvenTree/pull/10381)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@@ -107,6 +107,7 @@ The machine type class gets instantiated for each machine on server startup and
|
||||
- check_setting
|
||||
- set_status
|
||||
- set_status_text
|
||||
- set_properties
|
||||
|
||||
### Drivers
|
||||
|
||||
@@ -152,6 +153,7 @@ class MyXyzAbcDriverPlugin(MachineDriverMixin, InvenTreePlugin):
|
||||
- init_machine
|
||||
- update_machine
|
||||
- restart_machine
|
||||
- ping_machines
|
||||
- get_machines
|
||||
- handle_error
|
||||
|
||||
@@ -224,3 +226,24 @@ class MyXYZDriver(ABCBaseDriver):
|
||||
# ... do some init stuff here
|
||||
machine.set_status_text("Paper missing")
|
||||
```
|
||||
|
||||
### Machine Properties
|
||||
|
||||
Machine properties such as the device model, firmware version, and total pages printed can be displayed in the machine detail drawer to provide users with relevant device information.
|
||||
|
||||
To achieve this, use the `machine.set_properties` function to set the desired properties. This can be combined with a periodic task, such as `ping_machines`, to keep the information up to date.
|
||||
|
||||
```py
|
||||
from plugin.machine import MachineProperty
|
||||
|
||||
class MyXYZDriver(ABCBaseDriver):
|
||||
# ...
|
||||
def ping_machines(self):
|
||||
for machine in self.get_machines():
|
||||
# ... fetch machine info
|
||||
props: list[MachineProperty] = [
|
||||
{ 'key': 'Model', 'value': 'ABC' },
|
||||
]
|
||||
machine.set_properties(props)
|
||||
|
||||
```
|
||||
|
@@ -243,6 +243,12 @@ Refer to the [return order settings](../sales/return_order.md#return-order-setti
|
||||
{{ globalsetting("ENABLE_PLUGINS_INTERFACE") }}
|
||||
{{ globalsetting("ENABLE_PLUGINS_MAILS") }}
|
||||
|
||||
### Machine Settings
|
||||
|
||||
| Name | Description | Default | Units |
|
||||
| ---- | ----------- | ------- | ----- |
|
||||
{{ globalsetting("MACHINE_PING_ENABLED") }}
|
||||
|
||||
### Project Codes
|
||||
|
||||
Refer to the [project code settings](../concepts/project_codes.md).
|
||||
|
@@ -1,12 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 400
|
||||
INVENTREE_API_VERSION = 401
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v401 -> 2025-10-04 : https://github.com/inventree/InvenTree/pull/10381
|
||||
- Adds machine properties to machine API endpoints
|
||||
|
||||
v400 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10486
|
||||
- Adds return datatypes for admin/config and flags entpoints
|
||||
|
||||
|
@@ -91,6 +91,13 @@ def get_cache_config(global_cache: bool) -> dict:
|
||||
else:
|
||||
redis_url = f'redis://{cache_host()}:{cache_port()}/0'
|
||||
|
||||
keepalive_options = {
|
||||
'TCP_KEEPCNT': cache_setting('keepalive_count', 5, typecast=int),
|
||||
'TCP_KEEPIDLE': cache_setting('keepalive_idle', 1, typecast=int),
|
||||
'TCP_KEEPINTVL': cache_setting('keepalive_interval', 1, typecast=int),
|
||||
'TCP_USER_TIMEOUT': cache_setting('user_timeout', 1000, typecast=int),
|
||||
}
|
||||
|
||||
return {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': redis_url,
|
||||
@@ -105,18 +112,11 @@ def get_cache_config(global_cache: bool) -> dict:
|
||||
'tcp_keepalive', True, typecast=bool
|
||||
),
|
||||
'socket_keepalive_options': {
|
||||
socket.TCP_KEEPCNT: cache_setting(
|
||||
'keepalive_count', 5, typecast=int
|
||||
),
|
||||
socket.TCP_KEEPIDLE: cache_setting(
|
||||
'keepalive_idle', 1, typecast=int
|
||||
),
|
||||
socket.TCP_KEEPINTVL: cache_setting(
|
||||
'keepalive_interval', 1, typecast=int
|
||||
),
|
||||
socket.TCP_USER_TIMEOUT: cache_setting(
|
||||
'user_timeout', 1000, typecast=int
|
||||
),
|
||||
# Only include options which are available on this platform
|
||||
# e.g. MacOS does not have TCP_KEEPIDLE and TCP_USER_TIMEOUT
|
||||
getattr(socket, key): value
|
||||
for key, value in keepalive_options.items()
|
||||
if hasattr(socket, key)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -1130,4 +1130,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
},
|
||||
'MACHINE_PING_ENABLED': {
|
||||
'name': _('Enable Machine Ping'),
|
||||
'description': _(
|
||||
'Enable periodic ping task of registered machines to check their status'
|
||||
),
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
}
|
||||
|
@@ -253,7 +253,7 @@ class ApiTests(InvenTreeAPITestCase):
|
||||
# MachineStatus model
|
||||
machine_status = response.data['LabelPrinterStatus']
|
||||
self.assertEqual(machine_status['status_class'], 'LabelPrinterStatus')
|
||||
self.assertEqual(len(machine_status['values']), 6)
|
||||
self.assertEqual(len(machine_status['values']), 8)
|
||||
connected = machine_status['values']['CONNECTED']
|
||||
self.assertEqual(connected['key'], 100)
|
||||
self.assertEqual(connected['name'], 'CONNECTED')
|
||||
|
@@ -1,4 +1,15 @@
|
||||
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
|
||||
from machine.machine_type import (
|
||||
BaseDriver,
|
||||
BaseMachineType,
|
||||
MachineProperty,
|
||||
MachineStatus,
|
||||
)
|
||||
from machine.registry import registry
|
||||
|
||||
__all__ = ['BaseDriver', 'BaseMachineType', 'MachineStatus', 'registry']
|
||||
__all__ = [
|
||||
'BaseDriver',
|
||||
'BaseMachineType',
|
||||
'MachineProperty',
|
||||
'MachineStatus',
|
||||
'registry',
|
||||
]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Base machine type/base driver."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union
|
||||
|
||||
from generic.states import StatusCode
|
||||
from InvenTree.helpers_mixin import (
|
||||
@@ -48,6 +48,24 @@ class MachineStatus(StatusCode):
|
||||
"""
|
||||
|
||||
|
||||
class MachineProperty(TypedDict, total=False):
|
||||
"""Type definition for machine properties.
|
||||
|
||||
Attributes:
|
||||
key: Key of the property (required)
|
||||
value: Value of the property (required)
|
||||
group: Grouping of the property
|
||||
type: Type of the property (one of 'str', 'bool', 'progress', 'int', 'float') default = 'str'
|
||||
max_progress: Maximum value for progress type (required if type is 'progress')
|
||||
"""
|
||||
|
||||
key: str
|
||||
value: Union[str, bool, int, float]
|
||||
group: str
|
||||
type: Literal['str', 'bool', 'progress', 'int', 'float']
|
||||
max_progress: Union[int, None]
|
||||
|
||||
|
||||
class BaseDriver(
|
||||
ClassValidationMixin,
|
||||
ClassProviderMixin,
|
||||
@@ -117,6 +135,12 @@ class BaseDriver(
|
||||
machine: Machine instance
|
||||
"""
|
||||
|
||||
def ping_machines(self):
|
||||
"""Ping all machines using this driver to check if they are online.
|
||||
|
||||
This is called periodically by a background task if the setting 'MACHINE_PING_ENABLED' is active.
|
||||
"""
|
||||
|
||||
def get_machines(self, **kwargs):
|
||||
"""Return all machines using this driver (By default only initialized machines).
|
||||
|
||||
@@ -139,7 +163,8 @@ class BaseDriver(
|
||||
Arguments:
|
||||
error: Exception or string
|
||||
"""
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
if error not in self.errors:
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
|
||||
# --- state getters/setters
|
||||
@property
|
||||
@@ -321,7 +346,8 @@ class BaseMachineType(
|
||||
Arguments:
|
||||
error: Exception or string
|
||||
"""
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
if error not in self.errors:
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
|
||||
def reset_errors(self):
|
||||
"""Helper function for resetting the error list for a machine."""
|
||||
@@ -406,6 +432,26 @@ class BaseMachineType(
|
||||
"""
|
||||
self.set_shared_state('status_text', status_text)
|
||||
|
||||
def set_properties(self, properties: list[MachineProperty]):
|
||||
"""Set the machine properties. This can be any arbitrary dict with model information, etc.
|
||||
|
||||
Arguments:
|
||||
properties: The new properties dict to set
|
||||
"""
|
||||
for p in properties:
|
||||
if 'type' not in p:
|
||||
p['type'] = 'str'
|
||||
|
||||
if 'group' not in p:
|
||||
p['group'] = ''
|
||||
|
||||
if p['type'] == 'progress' and 'max_progress' not in p:
|
||||
p['max_progress'] = 100
|
||||
if 'max_progress' not in p:
|
||||
p['max_progress'] = None
|
||||
|
||||
self.set_shared_state('properties', properties)
|
||||
|
||||
# --- state getters/setters
|
||||
@property
|
||||
def initialized(self) -> bool:
|
||||
@@ -440,3 +486,13 @@ class BaseMachineType(
|
||||
def status_text(self) -> str:
|
||||
"""Machine status text."""
|
||||
return self.get_shared_state('status_text', '')
|
||||
|
||||
@property
|
||||
def properties(self) -> list[MachineProperty]:
|
||||
"""Return a dict of all relevant machine properties."""
|
||||
return self.get_shared_state('properties', [])
|
||||
|
||||
@property
|
||||
def properties_dict(self) -> dict[str, MachineProperty]:
|
||||
"""Return a dict of all machine properties with key as dict key."""
|
||||
return {prop['key']: prop for prop in self.properties}
|
||||
|
@@ -221,16 +221,21 @@ class LabelPrinterStatus(MachineStatus):
|
||||
CONNECTED: The printer is connected and ready to print
|
||||
UNKNOWN: The printer status is unknown (e.g. there is no active connection to the printer)
|
||||
PRINTING: The printer is currently printing a label
|
||||
WARNING: The printer is in an unknown warning condition
|
||||
NO_MEDIA: The printer is out of media (e.g. the label spool is empty)
|
||||
PAPER_JAM: The printer has a paper jam
|
||||
DISCONNECTED: The driver cannot establish a connection to the printer
|
||||
ERROR: The printer is in an unknown error condition
|
||||
"""
|
||||
|
||||
CONNECTED = 100, _('Connected'), ColorEnum.success
|
||||
UNKNOWN = 101, _('Unknown'), ColorEnum.secondary
|
||||
PRINTING = 110, _('Printing'), ColorEnum.primary
|
||||
WARNING = 200, _('Warning'), ColorEnum.warning
|
||||
NO_MEDIA = 301, _('No media'), ColorEnum.warning
|
||||
PAPER_JAM = 302, _('Paper jam'), ColorEnum.warning
|
||||
DISCONNECTED = 400, _('Disconnected'), ColorEnum.danger
|
||||
ERROR = 500, _('Error'), ColorEnum.danger
|
||||
|
||||
|
||||
class LabelPrinterMachine(BaseMachineType):
|
||||
@@ -250,7 +255,7 @@ class LabelPrinterMachine(BaseMachineType):
|
||||
}
|
||||
}
|
||||
|
||||
MACHINE_STATUS = LabelPrinterStatus
|
||||
MACHINE_STATUS: type[LabelPrinterStatus] = LabelPrinterStatus
|
||||
|
||||
default_machine_status = LabelPrinterStatus.UNKNOWN
|
||||
|
||||
|
@@ -125,7 +125,8 @@ class MachineRegistry(
|
||||
|
||||
def handle_error(self, error: Union[Exception, str]):
|
||||
"""Helper function for capturing errors with the machine registry."""
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
if error not in self.errors:
|
||||
self.set_shared_state('errors', [*self.errors, error])
|
||||
|
||||
@machine_registry_entrypoint(check_reload=False, check_ready=False)
|
||||
def initialize(self, main: bool = False):
|
||||
|
@@ -10,6 +10,22 @@ from machine import registry
|
||||
from machine.models import MachineConfig, MachineSetting
|
||||
|
||||
|
||||
class MachinePropertySerializer(serializers.Serializer):
|
||||
"""Serializer for a MachineProperty."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for serializer."""
|
||||
|
||||
fields = ['key', 'value', 'group', 'type', 'max_progress']
|
||||
read_only_fields = fields
|
||||
|
||||
key = serializers.CharField()
|
||||
value = serializers.CharField()
|
||||
group = serializers.CharField()
|
||||
type = serializers.CharField()
|
||||
max_progress = serializers.IntegerField()
|
||||
|
||||
|
||||
class MachineConfigSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for a MachineConfig."""
|
||||
|
||||
@@ -30,9 +46,10 @@ class MachineConfigSerializer(serializers.ModelSerializer):
|
||||
'machine_errors',
|
||||
'is_driver_available',
|
||||
'restart_required',
|
||||
'properties',
|
||||
]
|
||||
|
||||
read_only_fields = ['machine_type', 'driver']
|
||||
read_only_fields = ['machine_type', 'driver', 'properties']
|
||||
|
||||
initialized = serializers.SerializerMethodField('get_initialized')
|
||||
status = serializers.SerializerMethodField('get_status')
|
||||
@@ -41,6 +58,12 @@ class MachineConfigSerializer(serializers.ModelSerializer):
|
||||
machine_errors = serializers.SerializerMethodField('get_errors')
|
||||
is_driver_available = serializers.SerializerMethodField('get_is_driver_available')
|
||||
restart_required = serializers.SerializerMethodField('get_restart_required')
|
||||
properties = serializers.ListField(
|
||||
child=MachinePropertySerializer(),
|
||||
source='machine.properties',
|
||||
read_only=True,
|
||||
default=[],
|
||||
)
|
||||
|
||||
def get_initialized(self, obj: MachineConfig) -> bool:
|
||||
"""Serializer method for the initialized field."""
|
||||
|
23
src/backend/InvenTree/machine/tasks.py
Normal file
23
src/backend/InvenTree/machine/tasks.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Background task definitions for the 'machine' app."""
|
||||
|
||||
import structlog
|
||||
from opentelemetry import trace
|
||||
|
||||
from common.settings import get_global_setting
|
||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||
from machine import registry
|
||||
|
||||
tracer = trace.get_tracer(__name__)
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
@tracer.start_as_current_span('ping_machines')
|
||||
@scheduled_task(ScheduledTask.MINUTES, 5)
|
||||
def ping_machines():
|
||||
"""Periodically ping all configured machines to check if they are online."""
|
||||
if not get_global_setting('MACHINE_PING_ENABLED', True):
|
||||
return
|
||||
|
||||
for driver in registry.get_drivers():
|
||||
logger.debug("Pinging machines for driver '%s'", driver.SLUG)
|
||||
driver.ping_machines()
|
@@ -139,6 +139,27 @@ class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
|
||||
# Test machine restart hook
|
||||
registry.restart_machine(machine.machine)
|
||||
|
||||
# Assert properties have been set and correctly initialized with default values
|
||||
self.assertDictEqual(
|
||||
machine.machine.properties_dict,
|
||||
{
|
||||
'Model': {
|
||||
'key': 'Model',
|
||||
'value': 'Sample Printer 3000',
|
||||
'type': 'str',
|
||||
'group': '',
|
||||
'max_progress': None,
|
||||
},
|
||||
'Battery': {
|
||||
'key': 'Battery',
|
||||
'value': 42,
|
||||
'type': 'progress',
|
||||
'group': '',
|
||||
'max_progress': 100,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Test remove machine
|
||||
self.assertEqual(len(registry.get_machines()), 2)
|
||||
registry.remove_machine(machine)
|
||||
|
@@ -1,9 +1,16 @@
|
||||
from machine import BaseDriver, BaseMachineType, MachineStatus, registry
|
||||
from machine import (
|
||||
BaseDriver,
|
||||
BaseMachineType,
|
||||
MachineProperty,
|
||||
MachineStatus,
|
||||
registry,
|
||||
)
|
||||
from machine.registry import call_machine_function
|
||||
|
||||
__all__ = [
|
||||
'BaseDriver',
|
||||
'BaseMachineType',
|
||||
'MachineProperty',
|
||||
'MachineStatus',
|
||||
'call_machine_function',
|
||||
'registry',
|
||||
|
@@ -31,6 +31,10 @@ class SamplePrinterDriver(LabelPrinterBaseDriver):
|
||||
|
||||
def init_machine(self, machine: BaseMachineType) -> None:
|
||||
"""Machine initialization hook."""
|
||||
machine.set_properties([
|
||||
{'key': 'Model', 'value': 'Sample Printer 3000'},
|
||||
{'key': 'Battery', 'value': 42, 'type': 'progress'},
|
||||
])
|
||||
|
||||
def print_label(
|
||||
self,
|
||||
|
@@ -17,6 +17,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { api } from '../../../../App';
|
||||
import { StylishText } from '../../../../components/items/StylishText';
|
||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||
import { MachineListTable } from '../../../../tables/machine/MachineListTable';
|
||||
import {
|
||||
MachineDriverTable,
|
||||
@@ -116,6 +117,14 @@ export default function MachineManagementPanel() {
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value='settings'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<GlobalSettingList keys={['MACHINE_PING_ENABLED']} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
@@ -4,14 +4,16 @@ import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Code,
|
||||
Flex,
|
||||
Group,
|
||||
Indicator,
|
||||
List,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Progress,
|
||||
Stack,
|
||||
Table,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
@@ -24,7 +26,7 @@ import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { RowDeleteAction, RowEditAction } from '@lib/index';
|
||||
import { RowDeleteAction, RowEditAction, formatDecimal } from '@lib/index';
|
||||
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
||||
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
@@ -70,6 +72,13 @@ interface MachineI {
|
||||
machine_errors: string[];
|
||||
is_driver_available: boolean;
|
||||
restart_required: boolean;
|
||||
properties: {
|
||||
key: string;
|
||||
value: string;
|
||||
group: string;
|
||||
type: 'str' | 'progress' | 'bool' | 'int' | 'float';
|
||||
max_progress: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
function MachineStatusIndicator({ machine }: Readonly<{ machine: MachineI }>) {
|
||||
@@ -183,10 +192,11 @@ function MachineDrawer({
|
||||
const {
|
||||
data: machine,
|
||||
refetch,
|
||||
isFetching: isMachineFetching
|
||||
isLoading: isMachineFetching
|
||||
} = useQuery<MachineI>({
|
||||
enabled: true,
|
||||
queryKey: ['machine-detail', machinePk],
|
||||
refetchInterval: 5 * 1000,
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(apiUrl(ApiEndpoints.machine_list, machinePk))
|
||||
@@ -251,6 +261,19 @@ function MachineDrawer({
|
||||
}
|
||||
});
|
||||
|
||||
const groupedProperties = useMemo(() => {
|
||||
if (!machine?.properties) return [];
|
||||
const groups: string[] = []; // track ordered list of groups
|
||||
const groupMap: { [key: string]: typeof machine.properties } = {};
|
||||
for (const prop of machine.properties) {
|
||||
if (!groups.includes(prop.group)) groups.push(prop.group);
|
||||
if (!groupMap[prop.group]) groupMap[prop.group] = [];
|
||||
groupMap[prop.group].push(prop);
|
||||
}
|
||||
|
||||
return groups.map((g) => ({ group: g, properties: groupMap[g] }));
|
||||
}, [machine?.properties]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap='xs'>
|
||||
@@ -305,17 +328,22 @@ function MachineDrawer({
|
||||
|
||||
<Accordion
|
||||
multiple
|
||||
defaultValue={['machine-info', 'machine-settings', 'driver-settings']}
|
||||
defaultValue={[
|
||||
'machine-info',
|
||||
'machine-properties',
|
||||
'machine-settings',
|
||||
'driver-settings'
|
||||
]}
|
||||
>
|
||||
<Accordion.Item
|
||||
key={`machine-info-${machinePk}`}
|
||||
value='machine-info'
|
||||
>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Machine Information`}</StylishText>
|
||||
<StylishText size='lg'>{t`General`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Card withBorder>
|
||||
<Paper withBorder p='md'>
|
||||
<Stack gap='md'>
|
||||
<Stack pos='relative' gap='xs'>
|
||||
<LoadingOverlay
|
||||
@@ -363,7 +391,7 @@ function MachineDrawer({
|
||||
) : (
|
||||
StatusRenderer({
|
||||
status: `${machine?.status || -1}`,
|
||||
type: `MachineStatus__${machine?.status_model}` as any
|
||||
type: `${machine?.status_model}` as any
|
||||
})
|
||||
)}
|
||||
<Text fz='sm'>{machine?.status_text}</Text>
|
||||
@@ -392,7 +420,77 @@ function MachineDrawer({
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Paper>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item
|
||||
key={`machine-properties-${machinePk}`}
|
||||
value='machine-properties'
|
||||
>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Properties`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Paper withBorder p='md'>
|
||||
<Stack gap='sm'>
|
||||
{groupedProperties.map(({ group, properties }) => (
|
||||
<Stack key={group} gap={0}>
|
||||
{group && (
|
||||
<Text fz='sm' fw={700} mb={2}>
|
||||
{group}
|
||||
</Text>
|
||||
)}
|
||||
<Table
|
||||
variant='vertical'
|
||||
withTableBorder
|
||||
verticalSpacing={4}
|
||||
>
|
||||
<Table.Tbody>
|
||||
{properties.map((prop) => (
|
||||
<Table.Tr key={prop.key}>
|
||||
<Table.Th w={250}>{prop.key}</Table.Th>
|
||||
<Table.Td>
|
||||
{prop.type === 'bool' ? (
|
||||
<YesNoButton
|
||||
value={
|
||||
`${prop.value}`.toLowerCase() === 'true'
|
||||
}
|
||||
/>
|
||||
) : prop.type === 'progress' ? (
|
||||
<Group>
|
||||
<Progress
|
||||
value={
|
||||
(Number.parseInt(prop.value) /
|
||||
prop.max_progress) *
|
||||
100
|
||||
}
|
||||
flex={1}
|
||||
/>
|
||||
<Text>
|
||||
{prop.value} / {prop.max_progress}
|
||||
</Text>
|
||||
</Group>
|
||||
) : prop.type === 'int' ? (
|
||||
<Text size='sm'>{prop.value}</Text>
|
||||
) : prop.type === 'float' ? (
|
||||
<Text size='sm'>
|
||||
{formatDecimal(
|
||||
Number.parseFloat(prop.value),
|
||||
{ digits: 4 }
|
||||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size='sm'>{prop.value}</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
{machine?.is_driver_available && (
|
||||
@@ -404,13 +502,13 @@ function MachineDrawer({
|
||||
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Card withBorder>
|
||||
<Paper withBorder p='xs'>
|
||||
<MachineSettingList
|
||||
machinePk={machinePk}
|
||||
configType='M'
|
||||
onChange={refreshAll}
|
||||
/>
|
||||
</Card>
|
||||
</Paper>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
@@ -423,13 +521,13 @@ function MachineDrawer({
|
||||
<StylishText size='lg'>{t`Driver Settings`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Card withBorder>
|
||||
<Paper withBorder p='xs'>
|
||||
<MachineSettingList
|
||||
machinePk={machinePk}
|
||||
configType='D'
|
||||
onChange={refreshAll}
|
||||
/>
|
||||
</Card>
|
||||
</Paper>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
@@ -518,6 +616,15 @@ export function MachineListTable({
|
||||
return renderer(record);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'status_text',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
accessor: 'machine_errors',
|
||||
sortable: false,
|
||||
render: (record) => record.machine_errors.join(', ')
|
||||
}
|
||||
],
|
||||
[machineTypes]
|
||||
|
Reference in New Issue
Block a user