mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-11 17:15:43 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f13fa75e25 | |||
| 0cf84cd1d8 | |||
| 3e77f54034 | |||
| f7616102d8 | |||
| 5a497c3598 | |||
| 23e3644619 | |||
| a5db2dc340 | |||
| 2e8f43fcac | |||
| 09bb1121d8 |
@@ -1,6 +1,41 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
|
||||||
|
## v3.7.0 (2026-04-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Move companion app to applications
|
||||||
|
([`0cf84cd`](https://github.com/bec-project/bec_widgets/commit/0cf84cd1d839ac4a39ffb5fb9ba57d432e04348a))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- Cleanup of imports
|
||||||
|
([`3e77f54`](https://github.com/bec-project/bec_widgets/commit/3e77f540345f56b9f184a332fcdd50d4d4c8c621))
|
||||||
|
|
||||||
|
|
||||||
|
## v3.6.0 (2026-04-21)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Change resize mode to interactive
|
||||||
|
([`a5db2dc`](https://github.com/bec-project/bec_widgets/commit/a5db2dc340f3386e68b300fd4528a44f87cbbf97))
|
||||||
|
|
||||||
|
- Small usability changes
|
||||||
|
([`5a497c3`](https://github.com/bec-project/bec_widgets/commit/5a497c3598c2d8f27916d91d53c646d5d6d3a4a7))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add button/slot to pause/unpause logs
|
||||||
|
([`23e3644`](https://github.com/bec-project/bec_widgets/commit/23e3644619de958bcfdb8a0b2ee1f7c2ce05b235))
|
||||||
|
|
||||||
|
- Add logpanel to menu
|
||||||
|
([`2e8f43f`](https://github.com/bec-project/bec_widgets/commit/2e8f43fcac581cd1c227308198565d142a1bf276))
|
||||||
|
|
||||||
|
- Migrate logpanel to table model/view
|
||||||
|
([`09bb112`](https://github.com/bec-project/bec_widgets/commit/09bb1121d83bac1f6e4827daa476fbe7cd5b3a80))
|
||||||
|
|
||||||
|
|
||||||
## v3.5.1 (2026-04-20)
|
## v3.5.1 (2026-04-20)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+12
-18
@@ -1,19 +1,13 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
|
||||||
|
|
||||||
if sys.platform.startswith("linux"):
|
|
||||||
qt_platform = os.environ.get("QT_QPA_PLATFORM", "")
|
|
||||||
if qt_platform != "offscreen":
|
|
||||||
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
|
||||||
|
|
||||||
# Default QtAds configuration
|
|
||||||
QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True)
|
|
||||||
QtAds.CDockManager.setConfigFlag(
|
|
||||||
QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = ["BECWidget", "SafeSlot", "SafeProperty"]
|
__all__ = ["BECWidget", "SafeSlot", "SafeProperty"]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
if name == "BECWidget":
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
|
||||||
|
return BECWidget
|
||||||
|
if name in {"SafeSlot", "SafeProperty"}:
|
||||||
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
|
||||||
|
return {"SafeSlot": SafeSlot, "SafeProperty": SafeProperty}[name]
|
||||||
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
qt_platform = os.environ.get("QT_QPA_PLATFORM", "")
|
||||||
|
if qt_platform != "offscreen":
|
||||||
|
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
||||||
|
|
||||||
|
# Default QtAds configuration
|
||||||
|
QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True)
|
||||||
|
QtAds.CDockManager.setConfigFlag(
|
||||||
|
QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ from qtpy.QtWidgets import QApplication
|
|||||||
|
|
||||||
import bec_widgets
|
import bec_widgets
|
||||||
from bec_widgets.applications.launch_window import LaunchWindow
|
from bec_widgets.applications.launch_window import LaunchWindow
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -20,13 +20,13 @@ from qtpy.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import bec_widgets
|
import bec_widgets
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||||
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
||||||
from bec_widgets.utils.round_frame import RoundedFrame
|
from bec_widgets.utils.round_frame import RoundedFrame
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app
|
from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app
|
||||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
from bec_widgets.utils.ui_loader import UILoader
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from bec_widgets.cli.rpc import rpc_base
|
||||||
|
|||||||
+10
-10
@@ -3190,26 +3190,26 @@ class LaunchWindow(RPCBase):
|
|||||||
|
|
||||||
|
|
||||||
class LogPanel(RPCBase):
|
class LogPanel(RPCBase):
|
||||||
"""Displays a log panel"""
|
"""Live display of the BEC logs in a table view."""
|
||||||
|
|
||||||
_IMPORT_MODULE = "bec_widgets.widgets.utility.logpanel.logpanel"
|
_IMPORT_MODULE = "bec_widgets.widgets.utility.logpanel.logpanel"
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_plain_text(self, text: str) -> None:
|
def remove(self):
|
||||||
"""
|
"""
|
||||||
Set the plain text of the widget.
|
Cleanup the BECConnector
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): The text to set.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_html_text(self, text: str) -> None:
|
def attach(self):
|
||||||
|
"""
|
||||||
|
None
|
||||||
"""
|
"""
|
||||||
Set the HTML text of the widget.
|
|
||||||
|
|
||||||
Args:
|
@rpc_call
|
||||||
text (str): The text to set.
|
def detach(self):
|
||||||
|
"""
|
||||||
|
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import apply_theme
|
||||||
|
from bec_widgets.utils.rpc_widget_handler import widget_handler
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy as wh
|
from bec_widgets.utils.widget_io import WidgetHierarchy as wh
|
||||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1 @@
|
|||||||
from qtpy.QtWebEngineWidgets import QWebEngineView
|
|
||||||
|
|
||||||
from .bec_connector import BECConnector, ConnectionConfig
|
|
||||||
from .bec_dispatcher import BECDispatcher
|
|
||||||
from .bec_table import BECTable
|
|
||||||
from .colors import Colors
|
|
||||||
from .container_utils import WidgetContainerUtils
|
|
||||||
from .crosshair import Crosshair
|
|
||||||
from .entry_validator import EntryValidator
|
|
||||||
from .layout_manager import GridLayoutManager
|
|
||||||
from .rpc_decorator import register_rpc_methods, rpc_public
|
|
||||||
from .ui_loader import UILoader
|
|
||||||
from .validator_delegate import DoubleValidationDelegate
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from pydantic import BaseModel, Field, field_validator
|
|||||||
from qtpy.QtCore import Property, QObject, QRunnable, QThreadPool, Signal
|
from qtpy.QtCore import Property, QObject, QRunnable, QThreadPool, Signal
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
|
from bec_widgets.utils.error_popups import ErrorPopupUtility, SafeSlot
|
||||||
from bec_widgets.utils.name_utils import sanitize_namespace
|
from bec_widgets.utils.name_utils import sanitize_namespace
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ from qtpy.QtGui import QFont, QPixmap
|
|||||||
from qtpy.QtWidgets import QApplication, QFileDialog, QLabel, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QApplication, QFileDialog, QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
from bec_widgets.utils.busy_loader import install_busy_loader
|
from bec_widgets.utils.busy_loader import install_busy_loader
|
||||||
from bec_widgets.utils.error_popups import SafeConnect, SafeSlot
|
from bec_widgets.utils.error_popups import SafeConnect, SafeSlot
|
||||||
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||||
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Iterable
|
|||||||
from bec_lib.plugin_helper import _get_available_plugins
|
from bec_lib.plugin_helper import _get_available_plugins
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ from qtpy.QtCore import Qt, QTimer
|
|||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils import BECDispatcher
|
|
||||||
from bec_widgets.utils.bec_connector import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.screen_utils import apply_window_geometry
|
from bec_widgets.utils.screen_utils import apply_window_geometry
|
||||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
||||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
|
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from qtpy.QtWidgets import (
|
|||||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ class WidgetHierarchy:
|
|||||||
only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
|
only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
|
||||||
show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
|
show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
|
||||||
"""
|
"""
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||||
|
|
||||||
for node in WidgetHierarchy.iter_widget_tree(
|
for node in WidgetHierarchy.iter_widget_tree(
|
||||||
@@ -468,7 +468,7 @@ class WidgetHierarchy:
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
|
|
||||||
# 1) Gather ALL QWidget-based BECConnector objects
|
# 1) Gather ALL QWidget-based BECConnector objects
|
||||||
@@ -534,7 +534,7 @@ class WidgetHierarchy:
|
|||||||
Returns:
|
Returns:
|
||||||
The nearest ancestor that is a BECConnector, or None if not found.
|
The nearest ancestor that is a BECConnector, or None if not found.
|
||||||
"""
|
"""
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
|
|
||||||
# Guard against deleted/invalid Qt wrappers
|
# Guard against deleted/invalid Qt wrappers
|
||||||
if not shb.isValid(widget):
|
if not shb.isValid(widget):
|
||||||
@@ -636,7 +636,7 @@ class WidgetHierarchy:
|
|||||||
Return all BECConnector instances whose closest BECConnector ancestor is the given widget,
|
Return all BECConnector instances whose closest BECConnector ancestor is the given widget,
|
||||||
including the widget itself if it is a BECConnector.
|
including the widget itself if it is a BECConnector.
|
||||||
"""
|
"""
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
|
|
||||||
connectors: list[BECConnector] = []
|
connectors: list[BECConnector] = []
|
||||||
if isinstance(widget, BECConnector):
|
if isinstance(widget, BECConnector):
|
||||||
@@ -664,7 +664,7 @@ class WidgetHierarchy:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from bec_widgets.utils import BECConnector # local import to avoid cycles
|
from bec_widgets.utils.bec_connector import BECConnector # local import to avoid cycles
|
||||||
|
|
||||||
is_bec_target = False
|
is_bec_target = False
|
||||||
if isinstance(ancestor_class, str):
|
if isinstance(ancestor_class, str):
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from shiboken6 import isValid
|
|||||||
|
|
||||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||||
from bec_widgets import BECWidget, SafeSlot
|
from bec_widgets import BECWidget, SafeSlot
|
||||||
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
|
||||||
from bec_widgets.utils.bec_connector import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.property_editor import PropertyEditor
|
from bec_widgets.utils.property_editor import PropertyEditor
|
||||||
|
from bec_widgets.utils.rpc_widget_handler import widget_handler
|
||||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
from bec_widgets.widgets.containers.qt_ads import (
|
from bec_widgets.widgets.containers.qt_ads import (
|
||||||
CDockAreaWidget,
|
CDockAreaWidget,
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ from qtpy.QtWidgets import (
|
|||||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||||
from bec_widgets.applications.views.view import ViewTourSteps
|
from bec_widgets.applications.views.view import ViewTourSteps
|
||||||
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||||
from bec_widgets.utils import BECDispatcher
|
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import apply_theme
|
||||||
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
from bec_widgets.utils.rpc_decorator import rpc_timeout
|
||||||
|
from bec_widgets.utils.rpc_widget_handler import widget_handler
|
||||||
from bec_widgets.utils.toolbars.actions import (
|
from bec_widgets.utils.toolbars.actions import (
|
||||||
ExpandableMenuAction,
|
ExpandableMenuAction,
|
||||||
MaterialIconAction,
|
MaterialIconAction,
|
||||||
@@ -79,7 +79,7 @@ from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
|||||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||||||
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
||||||
from bec_widgets.widgets.utility.logpanel import LogPanel
|
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
|
||||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -376,6 +376,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
"bec_shell": (BECShell.ICON_NAME, "Add BEC Shell", "BECShell"),
|
"bec_shell": (BECShell.ICON_NAME, "Add BEC Shell", "BECShell"),
|
||||||
"log_panel": (LogPanel.ICON_NAME, "Add LogPanel - Disabled", "LogPanel"),
|
"log_panel": (LogPanel.ICON_NAME, "Add LogPanel - Disabled", "LogPanel"),
|
||||||
"sbb_monitor": ("train", "Add SBB Monitor", "SBBMonitor"),
|
"sbb_monitor": ("train", "Add SBB Monitor", "SBBMonitor"),
|
||||||
|
"log_panel": (LogPanel.ICON_NAME, "Add LogPanel", "LogPanel"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create expandable menu actions (original behavior)
|
# Create expandable menu actions (original behavior)
|
||||||
@@ -487,9 +488,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
# first two items not needed for this part
|
# first two items not needed for this part
|
||||||
for key, (_, _, widget_type) in mapping.items():
|
for key, (_, _, widget_type) in mapping.items():
|
||||||
act = menu.actions[key].action
|
act = menu.actions[key].action
|
||||||
if widget_type == "LogPanel":
|
if key == "terminal":
|
||||||
act.setEnabled(False) # keep disabled per issue #644
|
|
||||||
elif key == "terminal":
|
|
||||||
act.triggered.connect(
|
act.triggered.connect(
|
||||||
lambda _, t=widget_type: self.new(widget=t, closable=True, startup_cmd=None)
|
lambda _, t=widget_type: self.new(widget=t, closable=True, startup_cmd=None)
|
||||||
)
|
)
|
||||||
@@ -510,10 +509,7 @@ class BECDockArea(DockAreaWidget):
|
|||||||
for action_id, (_, _, widget_type) in mapping.items():
|
for action_id, (_, _, widget_type) in mapping.items():
|
||||||
flat_action_id = f"flat_{action_id}"
|
flat_action_id = f"flat_{action_id}"
|
||||||
flat_action = self.toolbar.components.get_action(flat_action_id).action
|
flat_action = self.toolbar.components.get_action(flat_action_id).action
|
||||||
if widget_type == "LogPanel":
|
flat_action.triggered.connect(lambda _, t=widget_type: self.new(t))
|
||||||
flat_action.setEnabled(False) # keep disabled per issue #644
|
|
||||||
else:
|
|
||||||
flat_action.triggered.connect(lambda _, t=widget_type: self.new(t))
|
|
||||||
|
|
||||||
_connect_flat_actions(self._ACTION_MAPPINGS["menu_plots"])
|
_connect_flat_actions(self._ACTION_MAPPINGS["menu_plots"])
|
||||||
_connect_flat_actions(self._ACTION_MAPPINGS["menu_devices"])
|
_connect_flat_actions(self._ACTION_MAPPINGS["menu_devices"])
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from qtpy.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from typeguard import typechecked
|
from typeguard import typechecked
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
from bec_widgets.utils.rpc_widget_handler import widget_handler
|
||||||
|
|
||||||
|
|
||||||
class LayoutManagerWidget(QWidget):
|
class LayoutManagerWidget(QWidget):
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ from qtpy.QtCore import QObject, QTimer
|
|||||||
from qtpy.QtWidgets import QApplication, QFrame, QMainWindow, QScrollArea, QWidget
|
from qtpy.QtWidgets import QApplication, QFrame, QMainWindow, QScrollArea, QWidget
|
||||||
|
|
||||||
from bec_widgets import SafeProperty, SafeSlot
|
from bec_widgets import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import apply_theme
|
||||||
from bec_widgets.utils.widget_io import WidgetIO
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ from qtpy.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import bec_widgets
|
import bec_widgets
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
|
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
|
||||||
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
|
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
|
||||||
BECNotificationBroker,
|
BECNotificationBroker,
|
||||||
|
|||||||
+1
-1
@@ -11,9 +11,9 @@ from qtpy.QtCore import Qt, Signal
|
|||||||
from qtpy.QtGui import QDoubleValidator
|
from qtpy.QtGui import QDoubleValidator
|
||||||
from qtpy.QtWidgets import QDoubleSpinBox
|
from qtpy.QtWidgets import QDoubleSpinBox
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
|
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
|
||||||
DeviceUpdateUIComponents,
|
DeviceUpdateUIComponents,
|
||||||
PositionerBoxBase,
|
PositionerBoxBase,
|
||||||
|
|||||||
+1
-1
@@ -12,9 +12,9 @@ from qtpy.QtCore import Signal
|
|||||||
from qtpy.QtGui import QDoubleValidator
|
from qtpy.QtGui import QDoubleValidator
|
||||||
from qtpy.QtWidgets import QDoubleSpinBox
|
from qtpy.QtWidgets import QDoubleSpinBox
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
|
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
|
||||||
DeviceUpdateUIComponents,
|
DeviceUpdateUIComponents,
|
||||||
PositionerBoxBase,
|
PositionerBoxBase,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from bec_lib.device import Signal as BECSignal
|
|||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.filter_io import FilterIO
|
from bec_widgets.utils.filter_io import FilterIO
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from bec_lib.device import Signal
|
|||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from qtpy.QtCore import Property
|
from qtpy.QtCore import Property
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.filter_io import FilterIO
|
from bec_widgets.utils.filter_io import FilterIO
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
from bec_widgets.utils.colors import apply_theme, get_accent_colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from bec_lib.logger import bec_logger
|
|||||||
from qtpy.QtCore import Signal
|
from qtpy.QtCore import Signal
|
||||||
from qtpy.QtWidgets import QPushButton, QSizePolicy, QTreeWidgetItem, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QPushButton, QSizePolicy, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import get_accent_colors
|
from bec_widgets.utils.colors import get_accent_colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ from scipy.interpolate import (
|
|||||||
from scipy.spatial import cKDTree
|
from scipy.spatial import cKDTree
|
||||||
from toolz import partition
|
from toolz import partition
|
||||||
|
|
||||||
from bec_widgets.utils import Colors
|
|
||||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import os
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
|
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
|
|
||||||
|
|
||||||
class HeatmapSettings(SettingWidget):
|
class HeatmapSettings(SettingWidget):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, field_validator
|
|||||||
from qtpy.QtCore import QTimer
|
from qtpy.QtCore import QTimer
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.colors import Colors, apply_theme
|
from bec_widgets.utils.colors import Colors, apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|||||||
from qtpy.QtCore import QPointF, Signal, SignalInstance
|
from qtpy.QtCore import QPointF, Signal, SignalInstance
|
||||||
from qtpy.QtWidgets import QDialog, QVBoxLayout
|
from qtpy.QtWidgets import QDialog, QVBoxLayout
|
||||||
|
|
||||||
from bec_widgets.utils import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.side_panel import SidePanel
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from pydantic import Field, ValidationError, field_validator
|
|||||||
from qtpy.QtCore import Signal
|
from qtpy.QtCore import Signal
|
||||||
from qtpy.QtGui import QTransform
|
from qtpy.QtGui import QTransform
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.widgets.plots.image.image_processor import (
|
from bec_widgets.widgets.plots.image.image_processor import (
|
||||||
ImageProcessor,
|
ImageProcessor,
|
||||||
ImageStats,
|
ImageStats,
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ from qtpy.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets import BECWidget
|
from bec_widgets import BECWidget
|
||||||
from bec_widgets.utils import BECDispatcher, ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||||
from bec_widgets.utils.toolbars.actions import WidgetAction
|
from bec_widgets.utils.toolbars.actions import WidgetAction
|
||||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ from qtpy.QtCore import Signal
|
|||||||
from qtpy.QtGui import QColor
|
from qtpy.QtGui import QColor
|
||||||
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import Colors, apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import os
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.utils.widget_io import WidgetIO
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ from pydantic import Field, ValidationError, field_validator
|
|||||||
from qtpy.QtCore import Signal
|
from qtpy.QtCore import Signal
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.side_panel import SidePanel
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import os
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.utils.widget_io import WidgetIO
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from bec_lib import bec_logger
|
|||||||
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
|
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
|
||||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.crosshair import Crosshair
|
||||||
|
from bec_widgets.utils.entry_validator import EntryValidator
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.fps_counter import FPSCounter
|
from bec_widgets.utils.fps_counter import FPSCounter
|
||||||
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from qtpy import QtCore
|
|||||||
from qtpy.QtCore import QObject, Signal
|
from qtpy.QtCore import QObject, Signal
|
||||||
|
|
||||||
from bec_widgets import SafeProperty
|
from bec_widgets import SafeProperty
|
||||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
from bec_widgets.utils.colors import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from bec_lib import bec_logger
|
|||||||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||||||
from qtpy import QtCore
|
from qtpy import QtCore
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
|
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from pydantic import Field, ValidationError, field_validator
|
|||||||
from qtpy.QtCore import QTimer, Signal
|
from qtpy.QtCore import QTimer, Signal
|
||||||
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import os
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
|
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
|
|
||||||
|
|
||||||
class ScatterCurveSettings(SettingWidget):
|
class ScatterCurveSettings(SettingWidget):
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import os
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.utils.widget_io import WidgetIO
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from bec_lib import bec_logger
|
|||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from qtpy import QtCore
|
from qtpy import QtCore
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ from qtpy.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets import SafeSlot
|
from bec_widgets import SafeSlot
|
||||||
from bec_widgets.utils import ConnectionConfig, EntryValidator
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
|
from bec_widgets.utils.entry_validator import EntryValidator
|
||||||
from bec_widgets.utils.toolbars.actions import WidgetAction
|
from bec_widgets.utils.toolbars.actions import WidgetAction
|
||||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||||
from bec_widgets.utils.colors import Colors, apply_theme
|
from bec_widgets.utils.colors import Colors, apply_theme
|
||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from bec_lib.logger import bec_logger
|
|||||||
from qtpy.QtCore import QPointF, QSize, Qt
|
from qtpy.QtCore import QPointF, QSize, Qt
|
||||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty
|
from bec_widgets.utils.error_popups import SafeProperty
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.utils import UILoader
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring
|
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring
|
||||||
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ from pyqtgraph import SignalProxy
|
|||||||
from qtpy.QtCore import QThreadPool, Signal
|
from qtpy.QtCore import QThreadPool, Signal
|
||||||
from qtpy.QtWidgets import QFileDialog, QListWidget, QToolButton, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QFileDialog, QListWidget, QToolButton, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.list_of_expandable_frames import ListOfExpandableFrames
|
from bec_widgets.utils.list_of_expandable_frames import ListOfExpandableFrames
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
from bec_widgets.utils.ui_loader import UILoader
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.services.device_browser.device_item import DeviceItem
|
from bec_widgets.widgets.services.device_browser.device_item import DeviceItem
|
||||||
from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import (
|
from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import (
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
"""Utilities for filtering and formatting in the LogPanel"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from collections import deque
|
|
||||||
from typing import Callable, Iterator
|
|
||||||
|
|
||||||
from bec_lib.logger import LogLevel
|
|
||||||
from bec_lib.messages import LogMessage
|
|
||||||
from qtpy.QtCore import QDateTime
|
|
||||||
|
|
||||||
LinesHtmlFormatter = Callable[[deque[LogMessage]], Iterator[str]]
|
|
||||||
LineFormatter = Callable[[LogMessage], str]
|
|
||||||
LineFilter = Callable[[LogMessage], bool] | None
|
|
||||||
|
|
||||||
ANSI_ESCAPE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
||||||
|
|
||||||
|
|
||||||
def replace_escapes(s: str):
|
|
||||||
s = ANSI_ESCAPE_REGEX.sub("", s)
|
|
||||||
return s.replace(" ", " ").replace("\n", "<br />").replace("\t", " ")
|
|
||||||
|
|
||||||
|
|
||||||
def level_filter(msg: LogMessage, thresh: int):
|
|
||||||
return LogLevel[msg.content["log_type"].upper()].value >= thresh
|
|
||||||
|
|
||||||
|
|
||||||
def noop_format(line: LogMessage):
|
|
||||||
_textline = line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
|
|
||||||
return replace_escapes(_textline.strip()) + "<br />"
|
|
||||||
|
|
||||||
|
|
||||||
def simple_color_format(line: LogMessage, colors: dict[LogLevel, str]):
|
|
||||||
color = colors.get(LogLevel[line.content["log_type"].upper()]) or colors[LogLevel.INFO]
|
|
||||||
return f'<font color="{color}">{noop_format(line)}</font>'
|
|
||||||
|
|
||||||
|
|
||||||
def create_formatter(line_format: LineFormatter, line_filter: LineFilter) -> LinesHtmlFormatter:
|
|
||||||
def _formatter(data: deque[LogMessage]):
|
|
||||||
if line_filter is not None:
|
|
||||||
return (line_format(line) for line in data if line_filter(line))
|
|
||||||
else:
|
|
||||||
return (line_format(line) for line in data)
|
|
||||||
|
|
||||||
return _formatter
|
|
||||||
|
|
||||||
|
|
||||||
def log_txt(line):
|
|
||||||
return line.log_msg if isinstance(line.log_msg, str) else line.log_msg["text"]
|
|
||||||
|
|
||||||
|
|
||||||
def log_time(line):
|
|
||||||
return QDateTime.fromMSecsSinceEpoch(int(line.log_msg["record"]["time"]["timestamp"] * 1000))
|
|
||||||
|
|
||||||
|
|
||||||
def log_svc(line):
|
|
||||||
return line.log_msg["service_name"]
|
|
||||||
@@ -30,7 +30,7 @@ class LogPanelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return DOM_XML
|
return DOM_XML
|
||||||
|
|
||||||
def group(self):
|
def group(self):
|
||||||
return "BEC Services"
|
return ""
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(LogPanel.ICON_NAME)
|
return designer_material_icon(LogPanel.ICON_NAME)
|
||||||
@@ -51,7 +51,7 @@ class LogPanelPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
|||||||
return "LogPanel"
|
return "LogPanel"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "Displays a log panel"
|
return "LogPanel"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
|
|||||||
@@ -2,21 +2,31 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import operator
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import partial, reduce
|
from dataclasses import dataclass
|
||||||
from re import Pattern
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import Iterable, Literal
|
||||||
|
|
||||||
from bec_lib.client import BECClient
|
from bec_lib.client import BECClient
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.logger import LogLevel, bec_logger
|
from bec_lib.logger import LogLevel, bec_logger
|
||||||
from bec_lib.messages import LogMessage, StatusMessage
|
from bec_lib.messages import LogMessage, StatusMessage
|
||||||
from pyqtgraph import SignalProxy
|
from bec_qthemes import material_icon
|
||||||
from qtpy.QtCore import QDateTime, QObject, Qt, Signal
|
from qtpy.QtCore import Signal # type: ignore
|
||||||
from qtpy.QtGui import QFont
|
from qtpy.QtCore import (
|
||||||
|
QAbstractTableModel,
|
||||||
|
QCoreApplication,
|
||||||
|
QDateTime,
|
||||||
|
QModelIndex,
|
||||||
|
QObject,
|
||||||
|
QPersistentModelIndex,
|
||||||
|
QSize,
|
||||||
|
QSortFilterProxyModel,
|
||||||
|
Qt,
|
||||||
|
QTimer,
|
||||||
|
)
|
||||||
|
from qtpy.QtGui import QColor
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
@@ -25,204 +35,414 @@ from qtpy.QtWidgets import (
|
|||||||
QDialog,
|
QDialog,
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
|
QHeaderView,
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QScrollArea,
|
QSizePolicy,
|
||||||
QTextEdit,
|
QTableView,
|
||||||
|
QToolButton,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
from bec_widgets.utils.bec_connector import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.colors import apply_theme, get_theme_palette
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.widgets.editors.text_box.text_box import TextBox
|
|
||||||
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECServiceStatusMixin
|
|
||||||
from bec_widgets.widgets.utility.logpanel._util import (
|
|
||||||
LineFilter,
|
|
||||||
LineFormatter,
|
|
||||||
LinesHtmlFormatter,
|
|
||||||
create_formatter,
|
|
||||||
level_filter,
|
|
||||||
log_svc,
|
|
||||||
log_time,
|
|
||||||
log_txt,
|
|
||||||
noop_format,
|
|
||||||
simple_color_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
|
||||||
from qtpy.QtCore import SignalInstance
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
# TODO: improve log color handling
|
_DEFAULT_LOG_COLORS = {
|
||||||
DEFAULT_LOG_COLORS = {
|
LogLevel.INFO.name: QColor("#FFFFFF"),
|
||||||
LogLevel.INFO: "#FFFFFF",
|
LogLevel.SUCCESS.name: QColor("#00FF00"),
|
||||||
LogLevel.SUCCESS: "#00FF00",
|
LogLevel.WARNING.name: QColor("#FFCC00"),
|
||||||
LogLevel.WARNING: "#FFCC00",
|
LogLevel.ERROR.name: QColor("#FF0000"),
|
||||||
LogLevel.ERROR: "#FF0000",
|
LogLevel.DEBUG.name: QColor("#0000CC"),
|
||||||
LogLevel.DEBUG: "#0000CC",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _Constants:
|
||||||
|
FUZZ_THRESHOLD = 80
|
||||||
|
UPDATE_INTERVAL_MS = 200
|
||||||
|
headers = ["level", "timestamp", "service_name", "message", "function"]
|
||||||
|
|
||||||
|
|
||||||
|
_CONST = _Constants()
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampUpdate:
|
||||||
|
def __init__(self, value: QDateTime | None, update_type: Literal["start", "end"]) -> None:
|
||||||
|
self.value = value
|
||||||
|
self.update_type = update_type
|
||||||
|
|
||||||
|
|
||||||
class BecLogsQueue(BECConnector, QObject):
|
class BecLogsQueue(BECConnector, QObject):
|
||||||
"""Manages getting logs from BEC Redis and formatting them for display"""
|
"""Manages getting logs from BEC Redis and formatting them for display"""
|
||||||
|
|
||||||
RPC = False
|
RPC = False
|
||||||
new_message = Signal()
|
new_messages = Signal()
|
||||||
|
paused = Signal(bool)
|
||||||
|
_instance: BecLogsQueue | None = None
|
||||||
|
|
||||||
def __init__(
|
@classmethod
|
||||||
self,
|
def instance(cls):
|
||||||
parent: QObject | None,
|
if cls._instance is None:
|
||||||
maxlen: int = 1000,
|
cls._instance = cls(QCoreApplication.instance())
|
||||||
line_formatter: LineFormatter = noop_format,
|
return cls._instance
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
def __init__(self, parent: QObject | None, maxlen: int = 2500, **kwargs) -> None:
|
||||||
|
if BecLogsQueue._instance:
|
||||||
|
raise RuntimeError("Create no more than one BecLogsQueue - use BecLogsQueue.instance()")
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
self._timestamp_start: QDateTime | None = None
|
|
||||||
self._timestamp_end: QDateTime | None = None
|
|
||||||
self._max_length = maxlen
|
self._max_length = maxlen
|
||||||
self._data: deque[LogMessage] = deque([], self._max_length)
|
self._paused = False
|
||||||
self._display_queue: deque[str] = deque([], self._max_length)
|
self._data = deque(
|
||||||
self._log_level: str | None = None
|
(
|
||||||
self._search_query: Pattern | str | None = None
|
item["data"]
|
||||||
self._selected_services: set[str] | None = None
|
for item in self.bec_dispatcher.client.connector.xread(
|
||||||
self._set_formatter_and_update_filter(line_formatter)
|
MessageEndpoints.log(), count=self._max_length, id="0"
|
||||||
# instance attribute still accessible after c++ object is deleted, so the callback can be unregistered
|
)
|
||||||
|
),
|
||||||
|
maxlen=self._max_length,
|
||||||
|
)
|
||||||
|
self._incoming: deque[LogMessage] = deque([], maxlen=self._max_length)
|
||||||
self.bec_dispatcher.connect_slot(self._process_incoming_log_msg, MessageEndpoints.log())
|
self.bec_dispatcher.connect_slot(self._process_incoming_log_msg, MessageEndpoints.log())
|
||||||
|
|
||||||
|
self._update_timer = QTimer(self, interval=_CONST.UPDATE_INTERVAL_MS)
|
||||||
|
self._update_timer.timeout.connect(self._proc_update)
|
||||||
|
QCoreApplication.instance().aboutToQuit.connect(self.cleanup) # type: ignore
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def toggle_pause(self):
|
||||||
|
self._paused = not self._paused
|
||||||
|
self.paused.emit(self._paused)
|
||||||
|
|
||||||
|
def row_data(self, index: int) -> LogMessage | None:
|
||||||
|
if index < 0 or index > (len(self._data) - 1):
|
||||||
|
return None
|
||||||
|
return self._data[index]
|
||||||
|
|
||||||
|
def cell_data(self, row: int, key: str):
|
||||||
|
if key == "level":
|
||||||
|
return self._data[row].log_type.upper()
|
||||||
|
|
||||||
|
msg_item = self._data[row].log_msg
|
||||||
|
if isinstance(msg_item, str):
|
||||||
|
return msg_item
|
||||||
|
if key == "service_name":
|
||||||
|
return msg_item.get(key)
|
||||||
|
elif key in ["service_name", "function", "message"]:
|
||||||
|
return msg_item.get("record", {}).get(key)
|
||||||
|
elif key == "timestamp":
|
||||||
|
return msg_item.get("record", {}).get("time", {}).get("repr")
|
||||||
|
|
||||||
|
def log_timestamp(self, row: int) -> float:
|
||||||
|
msg_item = self._data[row].log_msg
|
||||||
|
if isinstance(msg_item, str):
|
||||||
|
return 0
|
||||||
|
return msg_item.get("record", {}).get("time", {}).get("timestamp")
|
||||||
|
|
||||||
def cleanup(self, *_):
|
def cleanup(self, *_):
|
||||||
"""Stop listening to the Redis log stream"""
|
"""Stop listening to the Redis log stream"""
|
||||||
self.bec_dispatcher.disconnect_slot(
|
self.bec_dispatcher.disconnect_slot(
|
||||||
self._process_incoming_log_msg, [MessageEndpoints.log()]
|
self._process_incoming_log_msg, [MessageEndpoints.log()]
|
||||||
)
|
)
|
||||||
|
self._update_timer.stop()
|
||||||
|
BecLogsQueue._instance = None
|
||||||
|
|
||||||
@SafeSlot(verify_sender=True)
|
@SafeSlot(verify_sender=True)
|
||||||
def _process_incoming_log_msg(self, msg: dict, _metadata: dict):
|
def _process_incoming_log_msg(self, msg: dict, _metadata: dict):
|
||||||
try:
|
try:
|
||||||
_msg = LogMessage(**msg)
|
_msg = LogMessage(**msg)
|
||||||
self._data.append(_msg)
|
self._incoming.append(_msg)
|
||||||
if self.filter is None or self.filter(_msg):
|
|
||||||
self._display_queue.append(self._line_formatter(_msg))
|
|
||||||
self.new_message.emit()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if "Internal C++ object (BecLogsQueue) already deleted." in e.args:
|
if "Internal C++ object (BecLogsQueue) already deleted." in e.args:
|
||||||
return
|
return
|
||||||
logger.warning(f"Error in LogPanel incoming message callback: {e}")
|
logger.warning(f"Error in LogPanel incoming message callback: {e}")
|
||||||
|
|
||||||
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
|
@SafeSlot(verify_sender=True)
|
||||||
self._line_formatter: LineFormatter = line_formatter
|
def _proc_update(self):
|
||||||
self._queue_formatter: LinesHtmlFormatter = create_formatter(
|
if self._paused or len(self._incoming) == 0:
|
||||||
self._line_formatter, self.filter
|
|
||||||
)
|
|
||||||
|
|
||||||
def _combine_filters(self, *args: LineFilter):
|
|
||||||
return lambda msg: reduce(operator.and_, [filt(msg) for filt in args if filt is not None])
|
|
||||||
|
|
||||||
def _create_re_filter(self) -> LineFilter:
|
|
||||||
if self._search_query is None:
|
|
||||||
return None
|
|
||||||
elif isinstance(self._search_query, str):
|
|
||||||
return lambda line: self._search_query in log_txt(line)
|
|
||||||
return lambda line: self._search_query.match(log_txt(line)) is not None
|
|
||||||
|
|
||||||
def _create_service_filter(self):
|
|
||||||
return (
|
|
||||||
lambda line: self._selected_services is None or log_svc(line) in self._selected_services
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_timestamp_filter(self) -> LineFilter:
|
|
||||||
s, e = self._timestamp_start, self._timestamp_end
|
|
||||||
if s is e is None:
|
|
||||||
return lambda msg: True
|
|
||||||
|
|
||||||
def _time_filter(msg):
|
|
||||||
msg_time = log_time(msg)
|
|
||||||
if s is None:
|
|
||||||
return msg_time <= e
|
|
||||||
if e is None:
|
|
||||||
return s <= msg_time
|
|
||||||
return s <= msg_time <= e
|
|
||||||
|
|
||||||
return _time_filter
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filter(self) -> LineFilter:
|
|
||||||
"""A function which filters a log message based on all applied criteria"""
|
|
||||||
thresh = LogLevel[self._log_level].value if self._log_level is not None else 0
|
|
||||||
return self._combine_filters(
|
|
||||||
partial(level_filter, thresh=thresh),
|
|
||||||
self._create_re_filter(),
|
|
||||||
self._create_timestamp_filter(),
|
|
||||||
self._create_service_filter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_level_filter(self, level: str):
|
|
||||||
"""Change the log-level of the level filter"""
|
|
||||||
if level not in [l.name for l in LogLevel]:
|
|
||||||
logger.error(f"Logging level {level} unrecognized for filter!")
|
|
||||||
return
|
return
|
||||||
self._log_level = level
|
self._data.extend(self._incoming)
|
||||||
self._set_formatter_and_update_filter(self._line_formatter)
|
self._incoming.clear()
|
||||||
|
self.new_messages.emit()
|
||||||
|
|
||||||
def update_search_filter(self, search_query: Pattern | str | None = None):
|
|
||||||
"""Change the string or regex to filter against"""
|
|
||||||
self._search_query = search_query
|
|
||||||
self._set_formatter_and_update_filter(self._line_formatter)
|
|
||||||
|
|
||||||
def update_time_filter(self, start: QDateTime | None, end: QDateTime | None):
|
class BecLogsTableModel(QAbstractTableModel):
|
||||||
"""Change the start and/or end times to filter against"""
|
def __init__(self, parent: QWidget | None = None):
|
||||||
self._timestamp_start = start
|
super().__init__(parent)
|
||||||
self._timestamp_end = end
|
self.log_queue = BecLogsQueue.instance()
|
||||||
self._set_formatter_and_update_filter(self._line_formatter)
|
self.log_queue.new_messages.connect(self.handle_new_messages)
|
||||||
|
self._headers = _CONST.headers
|
||||||
|
|
||||||
def update_service_filter(self, services: set[str]):
|
def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
|
||||||
"""Change the selected services to display"""
|
return len(self.log_queue)
|
||||||
self._selected_services = services
|
|
||||||
self._set_formatter_and_update_filter(self._line_formatter)
|
|
||||||
|
|
||||||
def update_line_formatter(self, line_formatter: LineFormatter):
|
def columnCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int:
|
||||||
"""Update the formatter"""
|
return len(self._headers)
|
||||||
self._set_formatter_and_update_filter(line_formatter)
|
|
||||||
|
|
||||||
def display_all(self) -> str:
|
def headerData(self, section, orientation, role=int(Qt.ItemDataRole.DisplayRole)):
|
||||||
"""Return formatted output for all log messages"""
|
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
||||||
return "\n".join(self._queue_formatter(self._data.copy()))
|
return self._headers[section]
|
||||||
|
return None
|
||||||
|
|
||||||
def format_new(self):
|
def get_row_data(self, index: QModelIndex) -> LogMessage | None:
|
||||||
"""Return formatted output for the display queue"""
|
"""Return the row data for the given index."""
|
||||||
res = "\n".join(self._display_queue)
|
if not index.isValid():
|
||||||
self._display_queue = deque([], self._max_length)
|
return None
|
||||||
return res
|
return self.log_queue.row_data(index.row())
|
||||||
|
|
||||||
def clear_logs(self):
|
def timestamp(self, row: int):
|
||||||
"""Clear the cache and display queue"""
|
return QDateTime.fromMSecsSinceEpoch(int(self.log_queue.log_timestamp(row) * 1000))
|
||||||
self._data = deque([])
|
|
||||||
self._display_queue = deque([])
|
|
||||||
|
|
||||||
def fetch_history(self):
|
def data(self, index, role=int(Qt.ItemDataRole.DisplayRole)):
|
||||||
"""Fetch all available messages from Redis"""
|
"""Return data for the given index and role."""
|
||||||
self._data = deque(
|
if not index.isValid():
|
||||||
item["data"]
|
return
|
||||||
for item in self.bec_dispatcher.client.connector.xread(
|
if role in [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.ToolTipRole]:
|
||||||
MessageEndpoints.log().endpoint, from_start=True, count=self._max_length
|
return self.log_queue.cell_data(index.row(), self._headers[index.column()])
|
||||||
)
|
if role in [Qt.ItemDataRole.ForegroundRole]:
|
||||||
|
return self._map_log_level_color(self.log_queue.cell_data(index.row(), "level"))
|
||||||
|
|
||||||
|
def _map_log_level_color(self, data):
|
||||||
|
return _DEFAULT_LOG_COLORS.get(data)
|
||||||
|
|
||||||
|
def handle_new_messages(self):
|
||||||
|
self.dataChanged.emit(
|
||||||
|
self.index(0, 0), self.index(self.rowCount() - 1, self.columnCount() - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
def unique_service_names_from_history(self) -> set[str]:
|
|
||||||
"""Go through the log history to determine active service names"""
|
class LogMsgProxyModel(QSortFilterProxyModel):
|
||||||
return set(msg.log_msg["service_name"] for msg in self._data)
|
show_service_column = Signal(bool)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
service_filter: set[str] | None = None,
|
||||||
|
level_filter: LogLevel | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._service_filter = service_filter or set()
|
||||||
|
self._level_filter: LogLevel | None = level_filter
|
||||||
|
self._filter_text: str = ""
|
||||||
|
self._fuzzy_search: bool = False
|
||||||
|
self._time_filter_start: QDateTime | None = None
|
||||||
|
self._time_filter_end: QDateTime | None = None
|
||||||
|
|
||||||
|
def get_row_data(self, rows: Iterable[QModelIndex]) -> Iterable[LogMessage | None]:
|
||||||
|
return (self.sourceModel().get_row_data(self.mapToSource(idx)) for idx in rows)
|
||||||
|
|
||||||
|
def sourceModel(self) -> BecLogsTableModel:
|
||||||
|
return super().sourceModel() # type: ignore
|
||||||
|
|
||||||
|
@SafeSlot(int, int)
|
||||||
|
def refresh(self, *_):
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
@SafeSlot(None)
|
||||||
|
@SafeSlot(set)
|
||||||
|
def update_service_filter(self, filter: set[str]):
|
||||||
|
"""Filter to the selected services (show any service in the provided set)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter (set[str] | None): set of services for which to show logs"""
|
||||||
|
self._service_filter = filter
|
||||||
|
self.show_service_column.emit(len(filter) != 1)
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
@SafeSlot(None)
|
||||||
|
@SafeSlot(LogLevel)
|
||||||
|
def update_level_filter(self, filter: LogLevel | None):
|
||||||
|
"""Filter to the selected log level
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter (str | None): lowest log level to show"""
|
||||||
|
self._level_filter = filter
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def update_filter_text(self, filter: str):
|
||||||
|
"""Filter messages based on text
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter (str | None): set of services for which to show logs"""
|
||||||
|
self._filter_text = filter
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def update_fuzzy(self, state: bool):
|
||||||
|
"""Set text filter to fuzzy search or not
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state (bool): fuzzy search on"""
|
||||||
|
self._fuzzy_search = state
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
@SafeSlot(TimestampUpdate)
|
||||||
|
def update_timestamp(self, update: TimestampUpdate):
|
||||||
|
if update.update_type == "start":
|
||||||
|
self._time_filter_start = update.value
|
||||||
|
else:
|
||||||
|
self._time_filter_end = update.value
|
||||||
|
self.invalidateRowsFilter()
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, source_row: int, source_parent) -> bool:
|
||||||
|
# No service filter, and no filter text, display everything
|
||||||
|
possible_filters = [
|
||||||
|
self._service_filter,
|
||||||
|
self._level_filter,
|
||||||
|
self._filter_text,
|
||||||
|
self._time_filter_start,
|
||||||
|
self._time_filter_end,
|
||||||
|
]
|
||||||
|
if not any(map(bool, possible_filters)):
|
||||||
|
return True
|
||||||
|
model = self.sourceModel()
|
||||||
|
# Filter out services
|
||||||
|
if self._service_filter:
|
||||||
|
col = _CONST.headers.index("service_name")
|
||||||
|
if model.data(model.index(source_row, col, source_parent)) not in self._service_filter:
|
||||||
|
return False
|
||||||
|
# Filter out levels
|
||||||
|
if self._level_filter:
|
||||||
|
col = _CONST.headers.index("level")
|
||||||
|
level: str = model.data(model.index(source_row, col, source_parent)) # type: ignore
|
||||||
|
if LogLevel[level] < self._level_filter:
|
||||||
|
return False
|
||||||
|
# Filter time
|
||||||
|
if self._time_filter_start:
|
||||||
|
if model.timestamp(source_row) < self._time_filter_start:
|
||||||
|
return False
|
||||||
|
if self._time_filter_end:
|
||||||
|
if model.timestamp(source_row) > self._time_filter_end:
|
||||||
|
return False
|
||||||
|
# Filter message text - must go last because this can return True
|
||||||
|
if self._filter_text:
|
||||||
|
col = _CONST.headers.index("message")
|
||||||
|
msg: str = model.data(model.index(source_row, col, source_parent)).lower() # type: ignore
|
||||||
|
if self._fuzzy_search:
|
||||||
|
return fuzz.partial_ratio(self._filter_text.lower(), msg) >= _CONST.FUZZ_THRESHOLD
|
||||||
|
else:
|
||||||
|
return self._filter_text.lower() in msg.lower()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class BecLogTableView(QTableView):
|
||||||
|
def __init__(self, *args, max_message_width: int = 1000, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
header = QHeaderView(Qt.Orientation.Horizontal, parent=self)
|
||||||
|
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||||
|
header.setStretchLastSection(True)
|
||||||
|
header.setMaximumSectionSize(max_message_width)
|
||||||
|
self.setHorizontalHeader(header)
|
||||||
|
|
||||||
|
def model(self) -> LogMsgProxyModel:
|
||||||
|
return super().model() # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class LogPanel(BECWidget, QWidget):
|
||||||
|
"""Live display of the BEC logs in a table view."""
|
||||||
|
|
||||||
|
PLUGIN = True
|
||||||
|
ICON_NAME = "browse_activity"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
max_message_width: int = 1000,
|
||||||
|
show_toolbar: bool = True,
|
||||||
|
service_filter: set[str] | None = None,
|
||||||
|
level_filter: LogLevel | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self._setup_models(service_filter=service_filter, level_filter=level_filter)
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
if show_toolbar:
|
||||||
|
self._setup_toolbar(client=self.client)
|
||||||
|
self._setup_table_view(max_message_width=max_message_width)
|
||||||
|
self._update_service_filter(service_filter or set())
|
||||||
|
if show_toolbar:
|
||||||
|
self._connect_toolbar()
|
||||||
|
self._proxy.show_service_column.connect(self._show_service_column)
|
||||||
|
colors = QApplication.instance().theme.accent_colors # type: ignore
|
||||||
|
dict_colors = QApplication.instance().theme.colors # type: ignore
|
||||||
|
_DEFAULT_LOG_COLORS.update(
|
||||||
|
{
|
||||||
|
LogLevel.INFO.name: dict_colors["FG"],
|
||||||
|
LogLevel.SUCCESS.name: colors.success,
|
||||||
|
LogLevel.WARNING.name: colors.warning,
|
||||||
|
LogLevel.ERROR.name: colors.emergency,
|
||||||
|
LogLevel.DEBUG.name: dict_colors["BORDER"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self._table.scrollToBottom()
|
||||||
|
|
||||||
|
def _setup_models(self, service_filter: set[str] | None, level_filter: LogLevel | None):
|
||||||
|
self._model = BecLogsTableModel(parent=self)
|
||||||
|
self._proxy = LogMsgProxyModel(
|
||||||
|
parent=self, service_filter=service_filter, level_filter=level_filter
|
||||||
|
)
|
||||||
|
self._proxy.setSourceModel(self._model)
|
||||||
|
self._model.log_queue.new_messages.connect(self._proxy.refresh)
|
||||||
|
|
||||||
|
def _setup_table_view(self, max_message_width: int) -> None:
|
||||||
|
"""Setup the table view."""
|
||||||
|
self._table = BecLogTableView(self, max_message_width=max_message_width)
|
||||||
|
self._table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
self._layout.addWidget(self._table)
|
||||||
|
self._table.setModel(self._proxy)
|
||||||
|
self._table.setHorizontalScrollMode(QTableView.ScrollMode.ScrollPerPixel)
|
||||||
|
self._table.setTextElideMode(Qt.TextElideMode.ElideRight)
|
||||||
|
self._table.resizeColumnsToContents()
|
||||||
|
|
||||||
|
def _setup_toolbar(self, client: BECClient):
|
||||||
|
self._toolbar = LogPanelToolbar(self, client)
|
||||||
|
self._layout.addWidget(self._toolbar)
|
||||||
|
|
||||||
|
def _connect_toolbar(self):
|
||||||
|
self._toolbar.services_selected.connect(self._proxy.update_service_filter)
|
||||||
|
self._toolbar.search_textbox.textChanged.connect(self._proxy.update_filter_text)
|
||||||
|
self._toolbar.level_changed.connect(self._proxy.update_level_filter)
|
||||||
|
self._toolbar.fuzzy_changed.connect(self._proxy.update_fuzzy)
|
||||||
|
self._toolbar.timestamp_update.connect(self._proxy.update_timestamp)
|
||||||
|
self._toolbar.pause_button.clicked.connect(self._model.log_queue.toggle_pause)
|
||||||
|
self._model.log_queue.paused.connect(self._toolbar._update_pause_button_icon)
|
||||||
|
|
||||||
|
def _update_service_filter(self, filter: set[str]):
|
||||||
|
self._service_filter = filter
|
||||||
|
self._proxy.update_service_filter(filter)
|
||||||
|
self._table.setColumnHidden(
|
||||||
|
_CONST.headers.index("service_name"), len(self._service_filter) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def _show_service_column(self, show: bool):
|
||||||
|
self._table.setColumnHidden(_CONST.headers.index("service_name"), not show)
|
||||||
|
|
||||||
|
def sizeHint(self) -> QSize:
|
||||||
|
return QSize(600, 300)
|
||||||
|
|
||||||
|
|
||||||
class LogPanelToolbar(QWidget):
|
class LogPanelToolbar(QWidget):
|
||||||
|
services_selected = Signal(set)
|
||||||
|
level_changed = Signal(LogLevel)
|
||||||
|
fuzzy_changed = Signal(bool)
|
||||||
|
timestamp_update = Signal(TimestampUpdate)
|
||||||
|
|
||||||
services_selected: SignalInstance = Signal(set)
|
def __init__(self, parent: QWidget | None = None, client: BECClient | None = None) -> None:
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
|
||||||
"""A toolbar for the logpanel, mainly used for managing the states of filters"""
|
"""A toolbar for the logpanel, mainly used for managing the states of filters"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@@ -231,51 +451,69 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._timestamp_end: QDateTime | None = None
|
self._timestamp_end: QDateTime | None = None
|
||||||
|
|
||||||
self._unique_service_names: set[str] = set()
|
self._unique_service_names: set[str] = set()
|
||||||
self._services_selected: set[str] | None = None
|
self._services_selected: set[str] = set()
|
||||||
|
|
||||||
self.layout = QHBoxLayout(self) # type: ignore
|
self._layout = QHBoxLayout(self)
|
||||||
|
|
||||||
self.service_choice_button = QPushButton("Select services", self)
|
if client is not None:
|
||||||
self.layout.addWidget(self.service_choice_button)
|
self.client = client
|
||||||
self.service_choice_button.clicked.connect(self._open_service_filter_dialog)
|
self.service_choice_button = QPushButton("Select services", self)
|
||||||
|
self._layout.addWidget(self.service_choice_button)
|
||||||
|
self.service_choice_button.clicked.connect(self._open_service_filter_dialog)
|
||||||
|
self.service_list_update(self.client.service_status)
|
||||||
|
self._services_selected = self._unique_service_names
|
||||||
|
|
||||||
self.filter_level_dropdown = self._log_level_box()
|
self.filter_level_dropdown = self._log_level_box()
|
||||||
self.layout.addWidget(self.filter_level_dropdown)
|
self._layout.addWidget(self.filter_level_dropdown)
|
||||||
|
self.filter_level_dropdown.currentTextChanged.connect(self._emit_level)
|
||||||
self.clear_button = QPushButton("Clear all", self)
|
|
||||||
self.layout.addWidget(self.clear_button)
|
|
||||||
self.fetch_button = QPushButton("Fetch history", self)
|
|
||||||
self.layout.addWidget(self.fetch_button)
|
|
||||||
|
|
||||||
self._string_search_box()
|
self._string_search_box()
|
||||||
|
|
||||||
self.timerange_button = QPushButton("Set time range", self)
|
self.timerange_button = QPushButton("Set time range", self)
|
||||||
self.layout.addWidget(self.timerange_button)
|
self._layout.addWidget(self.timerange_button)
|
||||||
|
self.timerange_button.clicked.connect(self._open_datetime_dialog)
|
||||||
|
|
||||||
@property
|
self.pause_button = QToolButton()
|
||||||
def time_start(self):
|
self.pause_button.setIcon(material_icon("pause", size=(20, 20), convert_to_pixmap=False))
|
||||||
return self._timestamp_start
|
self._PLAYING_TOOLTIP = "Pause live log updates."
|
||||||
|
self._PAUSED_TOOLTIP = "Continue live log updates."
|
||||||
|
self.pause_button.setToolTip(self._PLAYING_TOOLTIP)
|
||||||
|
self._layout.addWidget(self.pause_button)
|
||||||
|
|
||||||
@property
|
@SafeSlot(bool)
|
||||||
def time_end(self):
|
def _update_pause_button_icon(self, paused):
|
||||||
return self._timestamp_end
|
if paused:
|
||||||
|
icon = "play_arrow"
|
||||||
|
tooltip = self._PAUSED_TOOLTIP
|
||||||
|
else:
|
||||||
|
icon = "pause"
|
||||||
|
tooltip = self._PLAYING_TOOLTIP
|
||||||
|
self.pause_button.setIcon(material_icon(icon, size=(20, 20), convert_to_pixmap=False))
|
||||||
|
self.pause_button.setToolTip(tooltip)
|
||||||
|
|
||||||
def _string_search_box(self):
|
def _string_search_box(self):
|
||||||
self.layout.addWidget(QLabel("Search: "))
|
self._layout.addWidget(QLabel("Search: "))
|
||||||
self.search_textbox = QLineEdit()
|
self.search_textbox = QLineEdit()
|
||||||
self.layout.addWidget(self.search_textbox)
|
self._layout.addWidget(self.search_textbox)
|
||||||
self.layout.addWidget(QLabel("Use regex: "))
|
self._layout.addWidget(QLabel("Fuzzy: "))
|
||||||
self.regex_enabled = QCheckBox()
|
self.fuzzy = QCheckBox()
|
||||||
self.layout.addWidget(self.regex_enabled)
|
self._layout.addWidget(self.fuzzy)
|
||||||
self.update_re_button = QPushButton("Update search", self)
|
self.fuzzy.checkStateChanged.connect(self._emit_fuzzy)
|
||||||
self.layout.addWidget(self.update_re_button)
|
|
||||||
|
|
||||||
def _log_level_box(self):
|
def _log_level_box(self):
|
||||||
box = QComboBox()
|
box = QComboBox()
|
||||||
box.setToolTip("Display logs with equal or greater significance to the selected level.")
|
box.setToolTip("Display logs with equal or greater significance to the selected level.")
|
||||||
[box.addItem(l.name) for l in LogLevel]
|
[box.addItem(level.name) for level in LogLevel]
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def _emit_level(self, level: str):
|
||||||
|
self.level_changed.emit(LogLevel[level])
|
||||||
|
|
||||||
|
@SafeSlot(Qt.CheckState)
|
||||||
|
def _emit_fuzzy(self, state: Qt.CheckState):
|
||||||
|
self.fuzzy_changed.emit(state == Qt.CheckState.Checked)
|
||||||
|
|
||||||
def _current_ts(self, selection_type: Literal["start", "end"]):
|
def _current_ts(self, selection_type: Literal["start", "end"]):
|
||||||
if selection_type == "start":
|
if selection_type == "start":
|
||||||
return self._timestamp_start
|
return self._timestamp_start
|
||||||
@@ -284,6 +522,7 @@ class LogPanelToolbar(QWidget):
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"timestamps can only be for the start or end, not {selection_type}")
|
raise ValueError(f"timestamps can only be for the start or end, not {selection_type}")
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
def _open_datetime_dialog(self):
|
def _open_datetime_dialog(self):
|
||||||
"""Open dialog window for timestamp filter selection"""
|
"""Open dialog window for timestamp filter selection"""
|
||||||
self._dt_dialog = QDialog(self)
|
self._dt_dialog = QDialog(self)
|
||||||
@@ -312,8 +551,8 @@ class LogPanelToolbar(QWidget):
|
|||||||
)
|
)
|
||||||
_layout.addWidget(date_clear_button)
|
_layout.addWidget(date_clear_button)
|
||||||
|
|
||||||
for v in [("start", label_start), ("end", label_end)]:
|
date_button_set("start", label_start)
|
||||||
date_button_set(*v)
|
date_button_set("end", label_end)
|
||||||
|
|
||||||
close_button = QPushButton("Close", parent=self._dt_dialog)
|
close_button = QPushButton("Close", parent=self._dt_dialog)
|
||||||
close_button.clicked.connect(self._dt_dialog.accept)
|
close_button.clicked.connect(self._dt_dialog.accept)
|
||||||
@@ -352,27 +591,23 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._timestamp_start = dt
|
self._timestamp_start = dt
|
||||||
else:
|
else:
|
||||||
self._timestamp_end = dt
|
self._timestamp_end = dt
|
||||||
|
self.timestamp_update.emit(TimestampUpdate(value=dt, update_type=selection_type))
|
||||||
|
|
||||||
@SafeSlot(dict, set)
|
def service_list_update(self, services_info: dict[str, StatusMessage]):
|
||||||
def service_list_update(
|
|
||||||
self, services_info: dict[str, StatusMessage], services_from_history: set[str], *_, **__
|
|
||||||
):
|
|
||||||
"""Change the list of services which can be selected"""
|
"""Change the list of services which can be selected"""
|
||||||
self._unique_service_names = set([s.split("/")[0] for s in services_info.keys()])
|
self._unique_service_names = set([s.split("/")[0] for s in services_info.keys()])
|
||||||
self._unique_service_names |= services_from_history
|
|
||||||
if self._services_selected is None:
|
|
||||||
self._services_selected = self._unique_service_names
|
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def _open_service_filter_dialog(self):
|
def _open_service_filter_dialog(self):
|
||||||
|
self.service_list_update(self.client.service_status)
|
||||||
if len(self._unique_service_names) == 0 or self._services_selected is None:
|
if len(self._unique_service_names) == 0 or self._services_selected is None:
|
||||||
return
|
return
|
||||||
self._svc_dialog = QDialog(self)
|
self._svc_dialog = QDialog(self)
|
||||||
self._svc_dialog.setWindowTitle(f"Select services to show logs from")
|
self._svc_dialog.setWindowTitle("Select services to show logs from")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self._svc_dialog.setLayout(layout)
|
self._svc_dialog.setLayout(layout)
|
||||||
|
|
||||||
service_cb_grid = QGridLayout(parent=self._svc_dialog)
|
service_cb_grid = QGridLayout()
|
||||||
layout.addLayout(service_cb_grid)
|
layout.addLayout(service_cb_grid)
|
||||||
|
|
||||||
def check_box(name: str, checked: Qt.CheckState):
|
def check_box(name: str, checked: Qt.CheckState):
|
||||||
@@ -398,146 +633,6 @@ class LogPanelToolbar(QWidget):
|
|||||||
self._svc_dialog.deleteLater()
|
self._svc_dialog.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
class LogPanel(TextBox):
|
|
||||||
"""Displays a log panel"""
|
|
||||||
|
|
||||||
ICON_NAME = "terminal"
|
|
||||||
service_list_update = Signal(dict, set)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
parent=None,
|
|
||||||
client: BECClient | None = None,
|
|
||||||
service_status: BECServiceStatusMixin | None = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Initialize the LogPanel widget."""
|
|
||||||
super().__init__(parent=parent, client=client, config={"text": ""}, **kwargs)
|
|
||||||
self._update_colors()
|
|
||||||
self._service_status = service_status or BECServiceStatusMixin(self, client=self.client) # type: ignore
|
|
||||||
self._log_manager = BecLogsQueue(
|
|
||||||
parent=self, line_formatter=partial(simple_color_format, colors=self._colors)
|
|
||||||
)
|
|
||||||
self._proxy_update = SignalProxy(
|
|
||||||
self._log_manager.new_message, rateLimit=1, slot=self._on_append
|
|
||||||
)
|
|
||||||
|
|
||||||
self.toolbar = LogPanelToolbar(parent=self)
|
|
||||||
self.toolbar_area = QScrollArea()
|
|
||||||
self.toolbar_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
||||||
self.toolbar_area.setSizeAdjustPolicy(QScrollArea.SizeAdjustPolicy.AdjustToContents)
|
|
||||||
self.toolbar_area.setFixedHeight(int(self.toolbar.clear_button.height() * 2))
|
|
||||||
self.toolbar_area.setWidget(self.toolbar)
|
|
||||||
|
|
||||||
self.layout.addWidget(self.toolbar_area)
|
|
||||||
self.toolbar.clear_button.clicked.connect(self._on_clear)
|
|
||||||
self.toolbar.fetch_button.clicked.connect(self._on_fetch)
|
|
||||||
self.toolbar.update_re_button.clicked.connect(self._on_re_update)
|
|
||||||
self.toolbar.search_textbox.returnPressed.connect(self._on_re_update)
|
|
||||||
self.toolbar.regex_enabled.checkStateChanged.connect(self._on_re_update)
|
|
||||||
self.toolbar.filter_level_dropdown.currentTextChanged.connect(self._set_level_filter)
|
|
||||||
|
|
||||||
self.toolbar.timerange_button.clicked.connect(self._choose_datetime)
|
|
||||||
self._service_status.services_update.connect(self._update_service_list)
|
|
||||||
self.service_list_update.connect(self.toolbar.service_list_update)
|
|
||||||
self.toolbar.services_selected.connect(self._update_service_filter)
|
|
||||||
|
|
||||||
self.text_box_text_edit.setFont(QFont("monospace", 12))
|
|
||||||
self.text_box_text_edit.setHtml("")
|
|
||||||
self.text_box_text_edit.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
|
||||||
|
|
||||||
self._connect_to_theme_change()
|
|
||||||
|
|
||||||
@SafeSlot(set)
|
|
||||||
def _update_service_filter(self, services: set[str]):
|
|
||||||
self._log_manager.update_service_filter(services)
|
|
||||||
self._on_redraw()
|
|
||||||
|
|
||||||
@SafeSlot(dict, dict)
|
|
||||||
def _update_service_list(self, services_info: dict[str, StatusMessage], *_, **__):
|
|
||||||
self.service_list_update.emit(
|
|
||||||
services_info, self._log_manager.unique_service_names_from_history()
|
|
||||||
)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def _choose_datetime(self):
|
|
||||||
self.toolbar._open_datetime_dialog()
|
|
||||||
self._set_time_filter()
|
|
||||||
|
|
||||||
def _connect_to_theme_change(self):
|
|
||||||
"""Connect to the theme change signal."""
|
|
||||||
qapp = QApplication.instance()
|
|
||||||
if hasattr(qapp, "theme_signal"):
|
|
||||||
qapp.theme_signal.theme_updated.connect(self._on_redraw) # type: ignore
|
|
||||||
|
|
||||||
def _update_colors(self):
|
|
||||||
self._colors = DEFAULT_LOG_COLORS.copy()
|
|
||||||
self._colors.update({LogLevel.INFO: get_theme_palette().text().color().name()})
|
|
||||||
|
|
||||||
def _cursor_to_end(self):
|
|
||||||
c = self.text_box_text_edit.textCursor()
|
|
||||||
c.movePosition(c.MoveOperation.End)
|
|
||||||
self.text_box_text_edit.setTextCursor(c)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
@SafeSlot(str)
|
|
||||||
def _on_redraw(self, *_):
|
|
||||||
self._update_colors()
|
|
||||||
self._log_manager.update_line_formatter(partial(simple_color_format, colors=self._colors))
|
|
||||||
self.set_html_text(self._log_manager.display_all())
|
|
||||||
self._cursor_to_end()
|
|
||||||
|
|
||||||
@SafeSlot(verify_sender=True)
|
|
||||||
def _on_append(self, *_):
|
|
||||||
self.text_box_text_edit.insertHtml(self._log_manager.format_new())
|
|
||||||
self._cursor_to_end()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def _on_clear(self):
|
|
||||||
self._log_manager.clear_logs()
|
|
||||||
self.set_html_text(self._log_manager.display_all())
|
|
||||||
self._cursor_to_end()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
@SafeSlot(Qt.CheckState)
|
|
||||||
def _on_re_update(self, *_):
|
|
||||||
if self.toolbar.regex_enabled.isChecked():
|
|
||||||
try:
|
|
||||||
search_query = re.compile(self.toolbar.search_textbox.text())
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to compile search regex with error {e}")
|
|
||||||
search_query = None
|
|
||||||
logger.info(f"Setting LogPanel search regex to {search_query}")
|
|
||||||
else:
|
|
||||||
search_query = self.toolbar.search_textbox.text()
|
|
||||||
logger.info(f'Setting LogPanel search string to "{search_query}"')
|
|
||||||
self._log_manager.update_search_filter(search_query)
|
|
||||||
self.set_html_text(self._log_manager.display_all())
|
|
||||||
self._cursor_to_end()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def _on_fetch(self):
|
|
||||||
self._log_manager.fetch_history()
|
|
||||||
self.set_html_text(self._log_manager.display_all())
|
|
||||||
self._cursor_to_end()
|
|
||||||
|
|
||||||
@SafeSlot(str)
|
|
||||||
def _set_level_filter(self, level: str):
|
|
||||||
self._log_manager.update_level_filter(level)
|
|
||||||
self._on_redraw()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def _set_time_filter(self):
|
|
||||||
self._log_manager.update_time_filter(self.toolbar.time_start, self.toolbar.time_end)
|
|
||||||
self._on_redraw()
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self._service_status.cleanup()
|
|
||||||
self._log_manager.cleanup()
|
|
||||||
self._log_manager.deleteLater()
|
|
||||||
super().cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -545,7 +640,15 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
apply_theme("dark")
|
apply_theme("dark")
|
||||||
widget = LogPanel()
|
panel = QWidget()
|
||||||
|
queue = BecLogsQueue(panel)
|
||||||
|
layout = QVBoxLayout(panel)
|
||||||
|
layout.addWidget(QLabel("All logs, no filters:"))
|
||||||
|
layout.addWidget(LogPanel())
|
||||||
|
layout.addWidget(QLabel("All services, level filter WARNING preapplied:"))
|
||||||
|
layout.addWidget(LogPanel(level_filter=LogLevel.WARNING))
|
||||||
|
layout.addWidget(QLabel('All services, service filter {"DeviceServer"} preapplied:'))
|
||||||
|
layout.addWidget(LogPanel(service_filter={"DeviceServer"}))
|
||||||
|
|
||||||
widget.show()
|
panel.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ from qtpy import QtCore, QtGui
|
|||||||
from qtpy.QtCore import Property, Signal, Slot
|
from qtpy.QtCore import Property, Signal, Slot
|
||||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import Colors
|
||||||
|
|
||||||
|
|
||||||
class RoundedColorMapButton(ColorMapButton):
|
class RoundedColorMapButton(ColorMapButton):
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.widget_highlighter import WidgetHighlighter
|
from bec_widgets.utils.widget_highlighter import WidgetHighlighter
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "bec_widgets"
|
name = "bec_widgets"
|
||||||
version = "3.5.1"
|
version = "3.7.0"
|
||||||
description = "BEC Widgets"
|
description = "BEC Widgets"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -12,19 +12,19 @@ dependencies = [
|
|||||||
"PyJWT~=2.9",
|
"PyJWT~=2.9",
|
||||||
"PySide6==6.9.0",
|
"PySide6==6.9.0",
|
||||||
"PySide6-QtAds==4.4.0",
|
"PySide6-QtAds==4.4.0",
|
||||||
"bec_ipython_client~=3.107,>=3.107.2", # needed for jupyter console
|
"bec_ipython_client~=3.107,>=3.107.2", # needed for jupyter console
|
||||||
"bec_lib~=3.107,>=3.107.2",
|
"bec_lib~=3.107,>=3.107.2",
|
||||||
"bec_qthemes~=1.0, >=1.3.4",
|
"bec_qthemes~=1.0, >=1.3.4",
|
||||||
"black>=26,<27", # needed for bw-generate-cli
|
"black>=26,<27", # needed for bw-generate-cli
|
||||||
"copier~=9.7",
|
"copier~=9.7",
|
||||||
"darkdetect~=0.8",
|
"darkdetect~=0.8",
|
||||||
"isort>=5.13, <9.0", # needed for bw-generate-cli
|
"isort>=5.13, <9.0", # needed for bw-generate-cli
|
||||||
"markdown~=3.9",
|
"markdown~=3.9",
|
||||||
"ophyd_devices~=1.29, >=1.29.1",
|
"ophyd_devices~=1.29, >=1.29.1",
|
||||||
"pydantic~=2.0",
|
"pydantic~=2.0",
|
||||||
"pylsp-bec~=1.2",
|
"pylsp-bec~=1.2",
|
||||||
"pyqtgraph==0.13.7",
|
"pyqtgraph==0.13.7",
|
||||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||||
"qtmonaco~=0.8, >=0.8.1",
|
"qtmonaco~=0.8, >=0.8.1",
|
||||||
"qtpy~=2.4",
|
"qtpy~=2.4",
|
||||||
"thefuzz~=0.22",
|
"thefuzz~=0.22",
|
||||||
@@ -38,8 +38,8 @@ Homepage = "https://gitlab.psi.ch/bec/bec_widgets"
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
bec-app = "bec_widgets.applications.main_app:main"
|
bec-app = "bec_widgets.applications.main_app:main"
|
||||||
bec-designer = "bec_widgets.utils.bec_designer:main"
|
bec-designer = "bec_widgets.utils.bec_designer:main"
|
||||||
bec-gui-server = "bec_widgets.cli.server:main"
|
bec-gui-server = "bec_widgets.applications.companion_app:main"
|
||||||
bw-generate-cli = "bec_widgets.cli.generate_cli:main"
|
bw-generate-cli = "bec_widgets.utils.generate_cli:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -55,9 +55,7 @@ dev = [
|
|||||||
"watchdog~=6.0",
|
"watchdog~=6.0",
|
||||||
"pre_commit~=4.2",
|
"pre_commit~=4.2",
|
||||||
]
|
]
|
||||||
qtermwidget = [
|
qtermwidget = ["pyside6_qtermwidget"]
|
||||||
"pyside6_qtermwidget",
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
@@ -68,7 +66,7 @@ line-length = 100
|
|||||||
skip-magic-trailing-comma = true
|
skip-magic-trailing-comma = true
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
skip_empty = true # exclude empty *files*, e.g. __init__.py, from the report
|
skip_empty = true # exclude empty *files*, e.g. __init__.py, from the report
|
||||||
exclude_lines = [
|
exclude_lines = [
|
||||||
"pragma: no cover",
|
"pragma: no cover",
|
||||||
"if TYPE_CHECKING:",
|
"if TYPE_CHECKING:",
|
||||||
|
|||||||
+18
-5
@@ -1,3 +1,5 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import qtpy.QtCore
|
import qtpy.QtCore
|
||||||
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
|
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
|
||||||
@@ -5,12 +7,14 @@ from qtpy.QtCore import QTimer
|
|||||||
|
|
||||||
|
|
||||||
class TestableQTimer(QTimer):
|
class TestableQTimer(QTimer):
|
||||||
_instances: list[tuple[QTimer, str]] = []
|
_instances: list[tuple[QTimer, str, str]] = []
|
||||||
_current_test_name: str = ""
|
_current_test_name: str = ""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
TestableQTimer._instances.append((self, TestableQTimer._current_test_name))
|
tb = traceback.format_stack()
|
||||||
|
init_line = list(filter(lambda msg: "QTimer(" in msg, tb))[-1]
|
||||||
|
TestableQTimer._instances.append((self, TestableQTimer._current_test_name, init_line))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_all_stopped(cls, qtbot):
|
def check_all_stopped(cls, qtbot):
|
||||||
@@ -20,12 +24,21 @@ class TestableQTimer(QTimer):
|
|||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
return "already deleted" in e.args[0]
|
return "already deleted" in e.args[0]
|
||||||
|
|
||||||
|
def _format_timers(timers: list[tuple[QTimer, str, str]]):
|
||||||
|
return "\n".join(
|
||||||
|
f"Timer: {t[0]}\n in test: {t[1]}\n created at:{t[2]}" for t in timers
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qtbot.waitUntil(lambda: all(_is_done_or_deleted(timer) for timer, _ in cls._instances))
|
qtbot.waitUntil(
|
||||||
|
lambda: all(_is_done_or_deleted(timer) for timer, _, _ in cls._instances)
|
||||||
|
)
|
||||||
except QtBotTimeoutError as exc:
|
except QtBotTimeoutError as exc:
|
||||||
active_timers = list(filter(lambda t: t[0].isActive(), cls._instances))
|
active_timers = list(filter(lambda t: t[0].isActive(), cls._instances))
|
||||||
(t.stop() for t, _ in cls._instances)
|
(t.stop() for t, _, _ in cls._instances)
|
||||||
raise TimeoutError(f"Failed to stop all timers: {active_timers}") from exc
|
raise TimeoutError(
|
||||||
|
f"Failed to stop all timers:\n{_format_timers(active_timers)}"
|
||||||
|
) from exc
|
||||||
cls._instances = []
|
cls._instances = []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -260,22 +260,6 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
|
|||||||
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
# TODO re-enable when issue is resolved #560
|
|
||||||
# @pytest.mark.timeout(PYTEST_TIMEOUT)
|
|
||||||
# def test_widgets_e2e_log_panel(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
|
||||||
# """Test the LogPanel widget."""
|
|
||||||
# gui = connected_client_gui_obj
|
|
||||||
# bec = gui._client
|
|
||||||
# # Create dock_area and widget
|
|
||||||
# widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
|
|
||||||
# widget: client.LogPanel
|
|
||||||
|
|
||||||
# # No rpc calls to check so far
|
|
||||||
|
|
||||||
# # Test removing the widget, or leaving it open for the next test
|
|
||||||
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
"""Test the MineSweeper widget."""
|
"""Test the MineSweeper widget."""
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
|
|||||||
from qtpy.QtCore import QEvent, QEventLoop
|
from qtpy.QtCore import QEvent, QEventLoop
|
||||||
from qtpy.QtWidgets import QApplication, QMessageBox
|
from qtpy.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
|
||||||
from bec_widgets.tests.utils import DEVICES, DMMock
|
from bec_widgets.tests.utils import DEVICES, DMMock
|
||||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||||
from bec_widgets.utils import error_popups
|
from bec_widgets.utils import error_popups
|
||||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||||
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
|
|
||||||
# Patch to set default RAISE_ERROR_DEFAULT to True for tests
|
# Patch to set default RAISE_ERROR_DEFAULT to True for tests
|
||||||
# This means that by default, error popups will raise exceptions during tests
|
# This means that by default, error popups will raise exceptions during tests
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pytest
|
|||||||
from qtpy.QtCore import QObject
|
from qtpy.QtCore import QObject
|
||||||
from qtpy.QtWidgets import QApplication, QWidget
|
from qtpy.QtWidgets import QApplication, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.error_popups import SafeProperty
|
from bec_widgets.utils.error_popups import SafeProperty
|
||||||
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from pydantic import ValidationError
|
|||||||
from qtpy.QtGui import QColor
|
from qtpy.QtGui import QColor
|
||||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import Colors, ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import apply_theme
|
from bec_widgets.utils.colors import Colors, apply_theme
|
||||||
from bec_widgets.widgets.plots.waveform.curve import CurveConfig
|
from bec_widgets.widgets.plots.waveform.curve import CurveConfig
|
||||||
from tests.unit_tests.client_mocks import mocked_client
|
from tests.unit_tests.client_mocks import mocked_client
|
||||||
from tests.unit_tests.conftest import create_widget
|
from tests.unit_tests.conftest import create_widget
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pytest
|
|||||||
from qtpy.QtCore import QPointF, Qt
|
from qtpy.QtCore import QPointF, Qt
|
||||||
from qtpy.QtGui import QTransform
|
from qtpy.QtGui import QTransform
|
||||||
|
|
||||||
from bec_widgets.utils import Crosshair
|
from bec_widgets.utils.crosshair import Crosshair
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||||
from tests.unit_tests.client_mocks import mocked_client
|
from tests.unit_tests.client_mocks import mocked_client
|
||||||
|
|||||||
@@ -2229,7 +2229,6 @@ class TestFlatToolbarActions:
|
|||||||
"flat_progress_bar",
|
"flat_progress_bar",
|
||||||
"flat_terminal",
|
"flat_terminal",
|
||||||
"flat_bec_shell",
|
"flat_bec_shell",
|
||||||
"flat_log_panel",
|
|
||||||
"flat_sbb_monitor",
|
"flat_sbb_monitor",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2289,11 +2288,6 @@ class TestFlatToolbarActions:
|
|||||||
action.trigger()
|
action.trigger()
|
||||||
mock_new.assert_called_once_with(widget_type)
|
mock_new.assert_called_once_with(widget_type)
|
||||||
|
|
||||||
def test_flat_log_panel_action_disabled(self, advanced_dock_area):
|
|
||||||
"""Test that flat log panel action is disabled."""
|
|
||||||
action = advanced_dock_area.toolbar.components.get_action("flat_log_panel").action
|
|
||||||
assert not action.isEnabled()
|
|
||||||
|
|
||||||
|
|
||||||
class TestModeTransitions:
|
class TestModeTransitions:
|
||||||
"""Test mode transitions and state consistency."""
|
"""Test mode transitions and state consistency."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import black
|
|||||||
import isort
|
import isort
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bec_widgets.cli.generate_cli import ClientGenerator
|
from bec_widgets.utils.generate_cli import ClientGenerator
|
||||||
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
|
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
|
||||||
|
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
|||||||
+106
-146
@@ -7,163 +7,123 @@ from collections import deque
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from bec_lib.logger import LogLevel
|
||||||
from bec_lib.messages import LogMessage
|
from bec_lib.messages import LogMessage
|
||||||
from bec_lib.redis_connector import StreamMessage
|
|
||||||
from qtpy.QtCore import QDateTime
|
from qtpy.QtCore import QDateTime
|
||||||
|
|
||||||
from bec_widgets.widgets.utility.logpanel._util import (
|
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel, TimestampUpdate
|
||||||
log_time,
|
|
||||||
replace_escapes,
|
|
||||||
simple_color_format,
|
|
||||||
)
|
|
||||||
from bec_widgets.widgets.utility.logpanel.logpanel import DEFAULT_LOG_COLORS, LogPanel
|
|
||||||
|
|
||||||
from .client_mocks import mocked_client
|
from .client_mocks import mocked_client
|
||||||
|
|
||||||
TEST_TABLE_STRING = "2025-01-15 15:57:18 | bec_server.scan_server.scan_queue | [INFO] | \n \x1b[3m primary queue / ScanQueueStatus.RUNNING \x1b[0m\n┏━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┓\n┃\x1b[1m \x1b[0m\x1b[1m queue_id \x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mscan_id\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mis_scan\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mtype\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mscan_numb…\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mIQ status\x1b[0m\x1b[1m \x1b[0m┃\n┡━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━┩\n│ bbe50c82-6… │ None │ False │ mv │ None │ PENDING │\n└─────────────┴─────────┴─────────┴──────┴────────────┴───────────┘\n\n"
|
|
||||||
|
|
||||||
TEST_LOG_MESSAGES = [
|
TEST_LOG_MESSAGES = [
|
||||||
LogMessage(
|
{"data": msg}
|
||||||
metadata={},
|
for msg in [
|
||||||
log_type="debug",
|
LogMessage(
|
||||||
log_msg={
|
|
||||||
"text": "datetime | debug | test log message",
|
|
||||||
"record": {"time": {"timestamp": 123456789.000}},
|
|
||||||
"service_name": "ScanServer",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
LogMessage(
|
|
||||||
metadata={},
|
|
||||||
log_type="info",
|
|
||||||
log_msg={
|
|
||||||
"text": "datetime | info | test log message",
|
|
||||||
"record": {"time": {"timestamp": 123456789.007}},
|
|
||||||
"service_name": "ScanServer",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
LogMessage(
|
|
||||||
metadata={},
|
|
||||||
log_type="success",
|
|
||||||
log_msg={
|
|
||||||
"text": "datetime | success | test log message",
|
|
||||||
"record": {"time": {"timestamp": 123456789.012}},
|
|
||||||
"service_name": "ScanServer",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
TEST_COMBINED_PLAINTEXT = "datetime | debug | test log message\ndatetime | info | test log message\ndatetime | success | test log message\n"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def raw_queue():
|
|
||||||
yield deque(TEST_LOG_MESSAGES, maxlen=100)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def log_panel(qtbot, mocked_client: MagicMock):
|
|
||||||
widget = LogPanel(client=mocked_client, service_status=MagicMock())
|
|
||||||
qtbot.addWidget(widget)
|
|
||||||
qtbot.waitExposed(widget)
|
|
||||||
yield widget
|
|
||||||
|
|
||||||
|
|
||||||
def test_log_panel_init(log_panel: LogPanel):
|
|
||||||
assert log_panel.plain_text == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_table_string_processing():
|
|
||||||
assert "\x1b" in TEST_TABLE_STRING
|
|
||||||
sanitized = replace_escapes(TEST_TABLE_STRING)
|
|
||||||
assert "\x1b" not in sanitized
|
|
||||||
assert " " not in sanitized
|
|
||||||
assert "\n" not in sanitized
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
["msg", "color"], zip(TEST_LOG_MESSAGES, ["#0000CC", "#FFFFFF", "#00FF00"])
|
|
||||||
)
|
|
||||||
def test_color_format(msg: LogMessage, color: str):
|
|
||||||
assert color in simple_color_format(msg, DEFAULT_LOG_COLORS)
|
|
||||||
|
|
||||||
|
|
||||||
def test_logpanel_output(qtbot, log_panel: LogPanel):
|
|
||||||
log_panel._log_manager._data = deque(TEST_LOG_MESSAGES)
|
|
||||||
log_panel._on_redraw()
|
|
||||||
assert log_panel.plain_text == TEST_COMBINED_PLAINTEXT
|
|
||||||
|
|
||||||
def display_queue_empty():
|
|
||||||
print(log_panel._log_manager._display_queue)
|
|
||||||
return len(log_panel._log_manager._display_queue) == 0
|
|
||||||
|
|
||||||
next_text = "datetime | error | test log message"
|
|
||||||
msg = LogMessage(
|
|
||||||
metadata={},
|
|
||||||
log_type="error",
|
|
||||||
log_msg={"text": next_text, "record": {}, "service_name": "ScanServer"},
|
|
||||||
)
|
|
||||||
log_panel._log_manager._process_incoming_log_msg(
|
|
||||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
|
||||||
)
|
|
||||||
|
|
||||||
qtbot.waitUntil(display_queue_empty, timeout=5000)
|
|
||||||
assert log_panel.plain_text == TEST_COMBINED_PLAINTEXT + next_text + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def test_level_filter(log_panel: LogPanel):
|
|
||||||
log_panel._log_manager._data = deque(TEST_LOG_MESSAGES)
|
|
||||||
log_panel._log_manager.update_level_filter("INFO")
|
|
||||||
log_panel._on_redraw()
|
|
||||||
assert (
|
|
||||||
log_panel.plain_text
|
|
||||||
== "datetime | info | test log message\ndatetime | success | test log message\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_button(log_panel: LogPanel):
|
|
||||||
log_panel._log_manager._data = deque(TEST_LOG_MESSAGES)
|
|
||||||
log_panel.toolbar.clear_button.click()
|
|
||||||
assert log_panel._log_manager._data == deque([])
|
|
||||||
|
|
||||||
|
|
||||||
def test_timestamp_filter(log_panel: LogPanel):
|
|
||||||
log_panel._log_manager._timestamp_start = QDateTime(1973, 11, 29, 21, 33, 9, 5, 1)
|
|
||||||
pytest.approx(log_panel._log_manager._timestamp_start.toMSecsSinceEpoch() / 1000, 123456789.005)
|
|
||||||
log_panel._log_manager._timestamp_end = QDateTime(1973, 11, 29, 21, 33, 9, 10, 1)
|
|
||||||
pytest.approx(log_panel._log_manager._timestamp_end.toMSecsSinceEpoch() / 1000, 123456789.010)
|
|
||||||
filter_ = log_panel._log_manager._create_timestamp_filter()
|
|
||||||
assert not filter_(TEST_LOG_MESSAGES[0])
|
|
||||||
assert filter_(TEST_LOG_MESSAGES[1])
|
|
||||||
assert not filter_(TEST_LOG_MESSAGES[2])
|
|
||||||
|
|
||||||
|
|
||||||
def test_error_handling_in_callback(log_panel: LogPanel):
|
|
||||||
log_panel._log_manager.new_message = MagicMock()
|
|
||||||
|
|
||||||
with patch("bec_widgets.widgets.utility.logpanel.logpanel.logger") as logger:
|
|
||||||
# generally errors should be logged
|
|
||||||
log_panel._log_manager.new_message.emit = MagicMock(
|
|
||||||
side_effect=ValueError("Something went wrong")
|
|
||||||
)
|
|
||||||
msg = LogMessage(
|
|
||||||
metadata={},
|
metadata={},
|
||||||
log_type="debug",
|
log_type="debug",
|
||||||
log_msg={
|
log_msg={
|
||||||
"text": "datetime | debug | test log message",
|
"text": "datetime | debug | test log message",
|
||||||
"record": {"time": {"timestamp": 123456789.000}},
|
"record": {
|
||||||
|
"time": {"timestamp": 123456789.000, "repr": "2025-01-01 00:00:01"},
|
||||||
|
"message": "test debug message abcd",
|
||||||
|
"function": "_debug",
|
||||||
|
},
|
||||||
|
"service_name": "ScanServer",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
LogMessage(
|
||||||
|
metadata={},
|
||||||
|
log_type="info",
|
||||||
|
log_msg={
|
||||||
|
"text": "datetime | info | test info log message",
|
||||||
|
"record": {
|
||||||
|
"time": {"timestamp": 123456789.007, "repr": "2025-01-01 00:00:02"},
|
||||||
|
"message": "test info message efgh",
|
||||||
|
"function": "_info",
|
||||||
|
},
|
||||||
|
"service_name": "DeviceServer",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
LogMessage(
|
||||||
|
metadata={},
|
||||||
|
log_type="success",
|
||||||
|
log_msg={
|
||||||
|
"text": "datetime | success | test log message",
|
||||||
|
"record": {
|
||||||
|
"time": {"timestamp": 123456789.012, "repr": "2025-01-01 00:00:03"},
|
||||||
|
"message": "test success message ijkl",
|
||||||
|
"function": "_success",
|
||||||
|
},
|
||||||
|
"service_name": "ScanServer",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log_panel(qtbot, mocked_client):
|
||||||
|
mocked_client.connector.xread = lambda *_, **__: TEST_LOG_MESSAGES
|
||||||
|
widget = LogPanel()
|
||||||
|
qtbot.addWidget(widget)
|
||||||
|
qtbot.waitExposed(widget)
|
||||||
|
yield widget
|
||||||
|
widget._model.log_queue.cleanup()
|
||||||
|
widget.close()
|
||||||
|
widget.deleteLater()
|
||||||
|
qtbot.wait(100)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_panel_init(qtbot, log_panel: LogPanel):
|
||||||
|
assert log_panel
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_panel_filters(qtbot, log_panel: LogPanel):
|
||||||
|
assert log_panel._proxy.rowCount() == 3
|
||||||
|
# Service filter
|
||||||
|
log_panel._update_service_filter({"DeviceServer"})
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||||
|
log_panel._update_service_filter(set())
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||||
|
# Text filter
|
||||||
|
log_panel._proxy.update_filter_text("efgh")
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||||
|
log_panel._proxy.update_filter_text("")
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||||
|
# Time filter
|
||||||
|
log_panel._proxy.update_timestamp(
|
||||||
|
TimestampUpdate(value=QDateTime.fromMSecsSinceEpoch(123456789004), update_type="start")
|
||||||
|
)
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 2, timeout=200)
|
||||||
|
log_panel._proxy.update_timestamp(
|
||||||
|
TimestampUpdate(value=QDateTime.fromMSecsSinceEpoch(123456789009), update_type="end")
|
||||||
|
)
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||||
|
log_panel._proxy.update_timestamp(TimestampUpdate(value=None, update_type="start"))
|
||||||
|
log_panel._proxy.update_timestamp(TimestampUpdate(value=None, update_type="end"))
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||||
|
# Level filter
|
||||||
|
log_panel._proxy.update_level_filter(LogLevel.SUCCESS)
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||||
|
log_panel._proxy.update_level_filter(None)
|
||||||
|
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_panel_update(qtbot, log_panel: LogPanel):
|
||||||
|
log_panel._model.log_queue._incoming.append(
|
||||||
|
LogMessage(
|
||||||
|
metadata={},
|
||||||
|
log_type="error",
|
||||||
|
log_msg={
|
||||||
|
"text": "datetime | error | test log message",
|
||||||
|
"record": {
|
||||||
|
"time": {"timestamp": 123456789.015, "repr": "2025-01-01 00:00:03"},
|
||||||
|
"message": "test error message xyz",
|
||||||
|
"function": "_error",
|
||||||
|
},
|
||||||
"service_name": "ScanServer",
|
"service_name": "ScanServer",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
log_panel._log_manager._process_incoming_log_msg(
|
)
|
||||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
log_panel._model.log_queue._proc_update()
|
||||||
)
|
qtbot.waitUntil(lambda: log_panel._model.rowCount() == 4, timeout=500)
|
||||||
logger.warning.assert_called_once()
|
|
||||||
|
|
||||||
# this specific error should be ignored and not relogged
|
|
||||||
log_panel._log_manager.new_message.emit = MagicMock(
|
|
||||||
side_effect=RuntimeError("Internal C++ object (BecLogsQueue) already deleted.")
|
|
||||||
)
|
|
||||||
log_panel._log_manager._process_incoming_log_msg(
|
|
||||||
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
|
|
||||||
)
|
|
||||||
logger.warning.assert_called_once()
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
|
|||||||
from qtpy.QtGui import QColor, QMouseEvent
|
from qtpy.QtGui import QColor, QMouseEvent
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
from bec_widgets.utils import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
||||||
RingProgressBar,
|
RingProgressBar,
|
||||||
RingProgressContainerWidget,
|
RingProgressContainerWidget,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
from bec_widgets.utils.rpc_register import RPCRegister
|
||||||
|
|
||||||
|
|
||||||
class FakeObject:
|
class FakeObject:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pytest
|
|||||||
from bec_lib.service_config import ServiceConfig
|
from bec_lib.service_config import ServiceConfig
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.cli.server import GUIServer
|
from bec_widgets.applications.companion_app import GUIServer
|
||||||
from bec_widgets.utils.bec_connector import BECConnector
|
from bec_widgets.utils.bec_connector import BECConnector
|
||||||
from bec_widgets.utils.rpc_server import RegistryNotReadyError, RPCServer, SingleshotRPCRepeat
|
from bec_widgets.utils.rpc_server import RegistryNotReadyError, RPCServer, SingleshotRPCRepeat
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_widget_handler import RPCWidgetHandler
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
|
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
|
||||||
|
from bec_widgets.utils.rpc_widget_handler import RPCWidgetHandler
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_widget_handler():
|
def test_rpc_widget_handler():
|
||||||
@@ -16,7 +16,7 @@ class _TestPluginWidget(BECWidget): ...
|
|||||||
|
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"bec_widgets.cli.rpc.rpc_widget_handler.get_all_plugin_widgets",
|
"bec_widgets.utils.rpc_widget_handler.get_all_plugin_widgets",
|
||||||
return_value=BECClassContainer(
|
return_value=BECClassContainer(
|
||||||
[
|
[
|
||||||
BECClassInfo(name="DeviceComboBox", obj=_TestPluginWidget, module="", file=""),
|
BECClassInfo(name="DeviceComboBox", obj=_TestPluginWidget, module="", file=""),
|
||||||
|
|||||||
Reference in New Issue
Block a user