From a46c557652520acb44e9b3663f051d9c2ffed704 Mon Sep 17 00:00:00 2001 From: David Perl Date: Wed, 10 Sep 2025 09:24:57 +0200 Subject: [PATCH] feat: allow setting config in redis --- .../device_manager_view.py | 56 ++++++++++++++----- .../components/device_table_view.py | 11 +++- .../components/dm_ophyd_test.py | 3 + .../device_item/config_communicator.py | 9 ++- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/bec_widgets/examples/device_manager_view/device_manager_view.py b/bec_widgets/examples/device_manager_view/device_manager_view.py index dd4a2937..5cdea710 100644 --- a/bec_widgets/examples/device_manager_view/device_manager_view.py +++ b/bec_widgets/examples/device_manager_view/device_manager_view.py @@ -1,17 +1,19 @@ from __future__ import annotations import os +from functools import partial from typing import TYPE_CHECKING, List import PySide6QtAds as QtAds import yaml +from bec_lib import config_helper from bec_lib.bec_yaml_loader import yaml_load from bec_lib.file_utils import DeviceConfigWriter from bec_lib.logger import bec_logger from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path from bec_qthemes import apply_theme from PySide6QtAds import CDockManager, CDockWidget -from qtpy.QtCore import Qt, QTimer +from qtpy.QtCore import Qt, QThreadPool, QTimer from qtpy.QtWidgets import QFileDialog, QMessageBox, QSplitter, QVBoxLayout, QWidget from bec_widgets import BECWidget @@ -29,13 +31,21 @@ from bec_widgets.widgets.control.device_manager.components._util import SharedSe from bec_widgets.widgets.control.device_manager.components.available_device_resources.available_device_resources import ( AvailableDeviceResources, ) +from bec_widgets.widgets.services.device_browser.device_item.config_communicator import ( + CommunicateConfigAction, +) from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import ( - DeviceConfigDialog, PresetClassDeviceConfigDialog, ) logger = bec_logger.logger +_yes_no_question = partial( + QMessageBox.question, + buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + defaultButton=QMessageBox.StandardButton.No, +) + def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None: """ @@ -78,6 +88,7 @@ class DeviceManagerView(BECWidget, QWidget): def __init__(self, parent=None, *args, **kwargs): super().__init__(parent=parent, client=None, *args, **kwargs) + self._config_helper = config_helper.ConfigHelper(self.client.connector) self._shared_selection = SharedSelectionSignal() # Top-level layout hosting a toolbar and the dock manager @@ -326,12 +337,10 @@ class DeviceManagerView(BECWidget, QWidget): @SafeSlot() def _load_redis_action(self): """Action for the 'load_redis' action to load the current config from Redis for the io_bundle of the toolbar.""" - reply = QMessageBox.question( + reply = _yes_no_question( self, "Load currently active config", "Do you really want to discard the current config and reload?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes and self.client.device_manager is not None: self.device_table_view.set_device_config( @@ -340,6 +349,32 @@ class DeviceManagerView(BECWidget, QWidget): else: return + @SafeSlot() + def _update_redis_action(self): + """Action to push the current composition to Redis""" + reply = _yes_no_question( + self, + "Push composition to Redis", + "Do you really want to replace the active configuration in the BEC server with the current composition? ", + ) + if reply != QMessageBox.StandardButton.Yes: + return + if self.device_table_view.table.contains_invalid_devices(): + return QMessageBox.warning( + self, "Validation has errors!", "Please resolve before proceeding." + ) + if self.ophyd_test_view.validation_running(): + return QMessageBox.warning( + self, "Validation has not completed.", "Please wait for the validation to finish." + ) + self._push_compositiion_to_redis() + + def _push_compositiion_to_redis(self): + config = {cfg.pop("name"): cfg for cfg in self.device_table_view.table.all_configs()} + threadpool = QThreadPool.globalInstance() + comm = CommunicateConfigAction(self._config_helper, None, config, "set") + threadpool.start(comm) + @SafeSlot() def _save_to_disk_action(self): """Action for the 'safe_to_disk' action to save the current config to disk.""" @@ -360,24 +395,15 @@ class DeviceManagerView(BECWidget, QWidget): with open(file_path, "w") as file: file.write(yaml.dump(config)) - # TODO add here logic, should be asyncronous, but probably block UI, and show a loading spinner. If failed, it should report.. - @SafeSlot() - def _update_redis_action(self): - """Action for the 'update_redis' action to update the current config in Redis.""" - config = self.device_table_view.get_device_config() - reply = self._coming_soon() - # Table actions @SafeSlot() def _reset_composed_view(self): """Action for the 'reset_composed_view' action to reset the composed view.""" - reply = QMessageBox.question( + reply = _yes_no_question( self, "Clear View", "You are about to clear the current composed config view, please confirm...", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: self.device_table_view.clear_device_configs() diff --git a/bec_widgets/widgets/control/device_manager/components/device_table_view.py b/bec_widgets/widgets/control/device_manager/components/device_table_view.py index dd922416..221c835b 100644 --- a/bec_widgets/widgets/control/device_manager/components/device_table_view.py +++ b/bec_widgets/widgets/control/device_manager/components/device_table_view.py @@ -313,7 +313,7 @@ class DeviceTableModel(QtCore.QAbstractTableModel): def get_device_config(self) -> list[dict[str, Any]]: """Method to get the device configuration.""" - return self._device_config + return copy.deepcopy(self._device_config) def device_names(self, configs: _DeviceCfgIter | None = None) -> set[str]: _configs = self._device_config if configs is None else configs @@ -431,6 +431,9 @@ class DeviceTableModel(QtCore.QAbstractTableModel): index = self.index(row, 0) self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole]) + def validation_statuses(self): + return copy.deepcopy(self._validation_status) + class BECTableView(QtWidgets.QTableView): """Table View with custom keyPressEvent to delete rows with backspace or delete key""" @@ -455,6 +458,12 @@ class BECTableView(QtWidgets.QTableView): return self.delete_selected() return super().keyPressEvent(event) + def contains_invalid_devices(self): + return ValidationStatus.FAILED in self.model().sourceModel().validation_statuses().values() + + def all_configs(self): + return self.model().sourceModel().get_device_config() + def selected_configs(self): return self.model().get_row_data(self.selectionModel().selectedRows()) diff --git a/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py b/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py index 71ef1da5..2093eb6c 100644 --- a/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py +++ b/bec_widgets/widgets/control/device_manager/components/dm_ophyd_test.py @@ -373,6 +373,9 @@ class DMOphydTest(BECWidget, QtWidgets.QWidget): ) return html + def validation_running(self): + return self._device_list_items != {} + @SafeSlot() def clear_list(self): """Clear the device list.""" diff --git a/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py b/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py index 4a469dbb..ca1d66f7 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py +++ b/bec_widgets/widgets/services/device_browser/device_item/config_communicator.py @@ -34,7 +34,11 @@ class CommunicateConfigAction(QRunnable): @SafeSlot() def run(self): try: - if self.action in ["add", "update", "remove"]: + if self.action == "set": + self._process( + {"action": self.action, "config": self.config, "wait_for_response": False} + ) + elif self.action in ["add", "update", "remove"]: if (dev_name := self.device or self.config.get("name")) is None: raise ValueError( "Must be updating a device or be supplied a name for a new device" @@ -57,6 +61,9 @@ class CommunicateConfigAction(QRunnable): "config": {dev_name: self.config}, "wait_for_response": False, } + self._process(req_args) + + def _process(self, req_args: dict): timeout = ( self.config_helper.suggested_timeout_s(self.config) if self.config is not None else 20 )