diff --git a/bec_widgets/widgets/monitor/config_dialog.py b/bec_widgets/widgets/monitor/config_dialog.py
index e04ac736..1641c11c 100644
--- a/bec_widgets/widgets/monitor/config_dialog.py
+++ b/bec_widgets/widgets/monitor/config_dialog.py
@@ -13,6 +13,11 @@ from qtpy.QtWidgets import (
)
from bec_widgets.utils.yaml_dialog import load_yaml, save_yaml
+from bec_widgets.validation import MonitorConfigValidator
+from pydantic import ValidationError
+from qtpy.QtWidgets import QApplication, QMessageBox
+from bec_widgets.utils.bec_dispatcher import BECDispatcher
+
current_path = os.path.dirname(__file__)
Ui_Form, BaseClass = uic.loadUiType(os.path.join(current_path, "config_dialog.ui"))
@@ -168,10 +173,25 @@ CONFIG_SCAN_MODE = {
class ConfigDialog(QWidget, Ui_Form):
config_updated = pyqtSignal(dict)
- def __init__(self, default_config=None):
+ def __init__(
+ self,
+ client=None,
+ default_config=None,
+ skip_validation: bool = False,
+ ):
super(ConfigDialog, self).__init__()
self.setupUi(self)
+ # Client
+ bec_dispatcher = BECDispatcher()
+ self.client = bec_dispatcher.client if client is None else client
+ self.dev = self.client.device_manager.devices
+
+ # Init validator
+ self.skip_validation = skip_validation
+ if self.skip_validation is False:
+ self.validator = MonitorConfigValidator(self.dev)
+
# Connect the Ok/Apply/Cancel buttons
self.pushButton_ok.clicked.connect(self.apply_and_close)
self.pushButton_apply.clicked.connect(self.apply_config)
@@ -522,8 +542,45 @@ class ConfigDialog(QWidget, Ui_Form):
def apply_and_close(self):
new_config = self.apply_config()
- self.config_updated.emit(new_config)
- self.close()
+ if self.skip_validation is True:
+ self.config_updated.emit(new_config)
+ self.close()
+ else:
+ try:
+ validated_config = self.validator.validate_monitor_config(new_config)
+ approved_config = validated_config.model_dump()
+ self.config_updated.emit(approved_config)
+ self.close()
+ except ValidationError as e:
+ error_str = str(e)
+ formatted_error_message = ConfigDialog.format_validation_error(error_str)
+
+ # Display the formatted error message in a popup
+ QMessageBox.critical(self, "Configuration Error", formatted_error_message)
+
+ @staticmethod
+ def format_validation_error(error_str: str) -> str:
+ """
+ Format the validation error string to be displayed in a popup.
+ Args:
+ error_str(str): Error string from the validation error.
+ """
+ error_lines = error_str.split("\n")
+ # The first line contains the number of errors.
+ error_header = f"
{error_lines[0]}
"
+
+ formatted_error_message = error_header
+ # Skip the first line as it's the header.
+ error_details = error_lines[1:]
+
+ # Iterate through pairs of lines (each error's two lines).
+ for i in range(0, len(error_details), 2):
+ location = error_details[i]
+ message = error_details[i + 1] if i + 1 < len(error_details) else ""
+
+ formatted_error_message += f"{location}
{message}
"
+
+ return formatted_error_message
if __name__ == "__main__": # pragma: no cover
diff --git a/bec_widgets/widgets/monitor/monitor.py b/bec_widgets/widgets/monitor/monitor.py
index 4b7d70fd..840f0dd7 100644
--- a/bec_widgets/widgets/monitor/monitor.py
+++ b/bec_widgets/widgets/monitor/monitor.py
@@ -309,7 +309,7 @@ class BECMonitor(pg.GraphicsLayoutWidget):
self.enable_crosshair = enable_crosshair
# Displayed Data
- self.database = {}
+ self.database = None
self.crosshairs = None
self.plots = None
@@ -352,6 +352,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
# Initialize the UI
self._init_ui(self.plot_settings["num_columns"])
+ if self.scanID is not None:
+ self.replot_last_scan()
+
def _init_database(self, plot_data_config: dict, source_type_to_init=None) -> dict:
"""
Initializes or updates the database for the PlotApp.
@@ -601,7 +604,9 @@ class BECMonitor(pg.GraphicsLayoutWidget):
"""Show the configuration dialog."""
from bec_widgets.widgets import ConfigDialog
- dialog = ConfigDialog(default_config=self.config)
+ dialog = ConfigDialog(
+ client=self.client, default_config=self.config, skip_validation=self.skip_validation
+ )
dialog.config_updated.connect(self.on_config_update)
dialog.show()
@@ -772,6 +777,13 @@ class BECMonitor(pg.GraphicsLayoutWidget):
else:
print(f"No data found for {device_name} {entry}")
+ def replot_last_scan(self):
+ """
+ Replot the last scan.
+ """
+ self.scan_segment_update()
+ self.update_plot(source_type="scan_segment")
+
@pyqtSlot(dict)
def on_data_from_redis(self, msg) -> None:
"""
diff --git a/tests/test_config_dialog.py b/tests/test_config_dialog.py
index 6d9bc9c9..48db677a 100644
--- a/tests/test_config_dialog.py
+++ b/tests/test_config_dialog.py
@@ -1,4 +1,6 @@
import os
+from unittest.mock import MagicMock
+
import yaml
import pytest
@@ -15,9 +17,73 @@ def load_test_config(config_name):
return config
+class FakeDevice:
+ """Fake minimal positioner class for testing."""
+
+ def __init__(self, name, enabled=True):
+ self.name = name
+ self.enabled = enabled
+ self.signals = {self.name: {"value": 1.0}}
+ self.description = {self.name: {"source": self.name}}
+
+ def __contains__(self, item):
+ return item == self.name
+
+ @property
+ def _hints(self):
+ return [self.name]
+
+ def set_value(self, fake_value: float = 1.0) -> None:
+ """
+ Setup fake value for device readout
+ Args:
+ fake_value(float): Desired fake value
+ """
+ self.signals[self.name]["value"] = fake_value
+
+ def describe(self) -> dict:
+ """
+ Get the description of the device
+ Returns:
+ dict: Description of the device
+ """
+ return self.description
+
+
+def get_mocked_device(device_name: str):
+ """
+ Helper function to mock the devices
+ Args:
+ device_name(str): Name of the device to mock
+ """
+ return FakeDevice(name=device_name, enabled=True)
+
+
@pytest.fixture(scope="function")
-def config_dialog(qtbot):
- widget = ConfigDialog()
+def mocked_client():
+ # Create a dictionary of mocked devices
+ device_names = ["samx", "gauss_bpm", "gauss_adc1", "gauss_adc2", "gauss_adc3", "bpm4i"]
+ mocked_devices = {name: get_mocked_device(name) for name in device_names}
+
+ # Create a MagicMock object
+ client = MagicMock()
+
+ # Mock the device_manager.devices attribute
+ client.device_manager.devices = MagicMock()
+ client.device_manager.devices.__getitem__.side_effect = lambda x: mocked_devices.get(x)
+ client.device_manager.devices.__contains__.side_effect = lambda x: x in mocked_devices
+
+ # Set each device as an attribute of the mock
+ for name, device in mocked_devices.items():
+ setattr(client.device_manager.devices, name, device)
+
+ return client
+
+
+@pytest.fixture(scope="function")
+def config_dialog(qtbot, mocked_client):
+ client = mocked_client
+ widget = ConfigDialog(client=client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget