bec-widgets 0.66.0__py3-none-any.whl → 0.67.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +28 -24
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +82 -3
- bec_widgets/utils/bec_dispatcher.py +10 -3
- bec_widgets/widgets/bec_status_box/__init__.py +0 -0
- bec_widgets/widgets/bec_status_box/bec_status_box.py +352 -0
- bec_widgets/widgets/bec_status_box/status_item.py +171 -0
- {bec_widgets-0.66.0.dist-info → bec_widgets-0.67.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.66.0.dist-info → bec_widgets-0.67.0.dist-info}/RECORD +21 -15
- docs/developer/widgets/widgets.md +0 -1
- docs/requirements.txt +1 -0
- docs/user/getting_started/installation.md +2 -2
- docs/user/widgets/bec_status_box.gif +0 -0
- docs/user/widgets/bec_status_box.md +30 -0
- docs/user/widgets/buttons.md +0 -1
- docs/user/widgets/widgets.md +2 -0
- pyproject.toml +1 -1
- tests/unit_tests/test_bec_status_box.py +152 -0
- {bec_widgets-0.66.0.dist-info → bec_widgets-0.67.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.66.0.dist-info → bec_widgets-0.67.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-0.66.0.dist-info → bec_widgets-0.67.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## v0.67.0 (2024-06-21)
|
4
|
+
|
5
|
+
### Documentation
|
6
|
+
|
7
|
+
* docs: add widget to documentation ([`6fa1c06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fa1c06053131dabd084bb3cf13c853b5d3ce833))
|
8
|
+
|
9
|
+
### Feature
|
10
|
+
|
11
|
+
* feat: introduce BECStatusBox Widget ([`443b6c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/443b6c1d7b02c772fda02e2d1eefd5bd40249e0c))
|
12
|
+
|
13
|
+
### Refactor
|
14
|
+
|
15
|
+
* refactor: Change inheritance to QTreeWidget from QWidget ([`d2f2b20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2f2b206bb0eab60b8a9b0d0ac60a6b7887fa6fb))
|
16
|
+
|
17
|
+
### Test
|
18
|
+
|
19
|
+
* test: add test suite for bec_status_box and status_item ([`5d4ca81`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4ca816cdedec4c88aba9eb326f85392504ea1c))
|
20
|
+
|
21
|
+
### Unknown
|
22
|
+
|
23
|
+
* Update file requirements.txt ([`505a5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/505a5ec8334ff4422913b3a7b79d39bcb42ad535))
|
24
|
+
|
25
|
+
## v0.66.1 (2024-06-20)
|
26
|
+
|
27
|
+
### Fix
|
28
|
+
|
29
|
+
* fix: fixed shutdown for pyside ([`2718bc6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2718bc624731301756df524d0d5beef6cb1c1430))
|
30
|
+
|
3
31
|
## v0.66.0 (2024-06-20)
|
4
32
|
|
5
33
|
### Feature
|
@@ -147,30 +175,6 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
|
|
147
175
|
|
148
176
|
## v0.60.0 (2024-06-08)
|
149
177
|
|
150
|
-
### Ci
|
151
|
-
|
152
|
-
* ci: added git fetch for target branch ([`fc4f4f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4f4f81ad1be99cf5112f2188a46c5bed2679ee))
|
153
|
-
|
154
|
-
* ci: fixed pylint-check ([`6b1d582`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b1d5827d6599f06a3acd316060a8d25f0686d54))
|
155
|
-
|
156
|
-
### Feature
|
157
|
-
|
158
|
-
* feat: added isort to bw-generate-cli ([`f0391f5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0391f59c9eb0a51b693fccfe2e399e869d35dda))
|
159
|
-
|
160
178
|
### Fix
|
161
179
|
|
162
180
|
* fix: removed BECConnector from rpc client interface ([`6428e38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6428e38ab94c15a2c904e75cc6404bb6d0394e04))
|
163
|
-
|
164
|
-
* fix: added bec_ipython_client as dependency; needed for jupyter widget ([`006a089`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/006a0894b85cba3b2773737ed6fe3e92c81cdee0))
|
165
|
-
|
166
|
-
### Refactor
|
167
|
-
|
168
|
-
* refactor: minor cleanup ([`3adf6cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3adf6cfd586355c8b8ce7fdc9722f868e22287c5))
|
169
|
-
|
170
|
-
* refactor: disabled pylint for auto-gen client ([`b15816c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b15816ca9fd3e4ae87cca5fcfe029b4dfca570ca))
|
171
|
-
|
172
|
-
* refactor(isort): added bec_widgets as known first party package ([`9c5a471`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9c5a471234ed2928e4527b079436db2a807c5f6f))
|
173
|
-
|
174
|
-
### Test
|
175
|
-
|
176
|
-
* test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
|
PKG-INFO
CHANGED
bec_widgets/cli/client.py
CHANGED
@@ -13,6 +13,7 @@ class Widgets(str, enum.Enum):
|
|
13
13
|
Enum for the available widgets.
|
14
14
|
"""
|
15
15
|
|
16
|
+
BECStatusBox = "BECStatusBox"
|
16
17
|
BECDock = "BECDock"
|
17
18
|
BECDockArea = "BECDockArea"
|
18
19
|
BECFigure = "BECFigure"
|
@@ -1388,6 +1389,24 @@ class BECPlotBase(RPCBase):
|
|
1388
1389
|
"""
|
1389
1390
|
|
1390
1391
|
|
1392
|
+
class BECStatusBox(RPCBase):
|
1393
|
+
@property
|
1394
|
+
@rpc_call
|
1395
|
+
def config_dict(self) -> "dict":
|
1396
|
+
"""
|
1397
|
+
Get the configuration of the widget.
|
1398
|
+
|
1399
|
+
Returns:
|
1400
|
+
dict: The configuration of the widget.
|
1401
|
+
"""
|
1402
|
+
|
1403
|
+
@rpc_call
|
1404
|
+
def get_all_rpc(self) -> "dict":
|
1405
|
+
"""
|
1406
|
+
Get all registered RPC objects.
|
1407
|
+
"""
|
1408
|
+
|
1409
|
+
|
1391
1410
|
class BECWaveform(RPCBase):
|
1392
1411
|
@property
|
1393
1412
|
@rpc_call
|
@@ -1649,6 +1668,60 @@ class BECWaveform(RPCBase):
|
|
1649
1668
|
"""
|
1650
1669
|
|
1651
1670
|
|
1671
|
+
class DeviceComboBox(RPCBase):
|
1672
|
+
@property
|
1673
|
+
@rpc_call
|
1674
|
+
def config_dict(self) -> "dict":
|
1675
|
+
"""
|
1676
|
+
Get the configuration of the widget.
|
1677
|
+
|
1678
|
+
Returns:
|
1679
|
+
dict: The configuration of the widget.
|
1680
|
+
"""
|
1681
|
+
|
1682
|
+
@rpc_call
|
1683
|
+
def get_all_rpc(self) -> "dict":
|
1684
|
+
"""
|
1685
|
+
Get all registered RPC objects.
|
1686
|
+
"""
|
1687
|
+
|
1688
|
+
|
1689
|
+
class DeviceInputBase(RPCBase):
|
1690
|
+
@property
|
1691
|
+
@rpc_call
|
1692
|
+
def config_dict(self) -> "dict":
|
1693
|
+
"""
|
1694
|
+
Get the configuration of the widget.
|
1695
|
+
|
1696
|
+
Returns:
|
1697
|
+
dict: The configuration of the widget.
|
1698
|
+
"""
|
1699
|
+
|
1700
|
+
@rpc_call
|
1701
|
+
def get_all_rpc(self) -> "dict":
|
1702
|
+
"""
|
1703
|
+
Get all registered RPC objects.
|
1704
|
+
"""
|
1705
|
+
|
1706
|
+
|
1707
|
+
class DeviceLineEdit(RPCBase):
|
1708
|
+
@property
|
1709
|
+
@rpc_call
|
1710
|
+
def config_dict(self) -> "dict":
|
1711
|
+
"""
|
1712
|
+
Get the configuration of the widget.
|
1713
|
+
|
1714
|
+
Returns:
|
1715
|
+
dict: The configuration of the widget.
|
1716
|
+
"""
|
1717
|
+
|
1718
|
+
@rpc_call
|
1719
|
+
def get_all_rpc(self) -> "dict":
|
1720
|
+
"""
|
1721
|
+
Get all registered RPC objects.
|
1722
|
+
"""
|
1723
|
+
|
1724
|
+
|
1652
1725
|
class Ring(RPCBase):
|
1653
1726
|
@rpc_call
|
1654
1727
|
def get_all_rpc(self) -> "dict":
|
@@ -1950,7 +2023,7 @@ class TextBox(RPCBase):
|
|
1950
2023
|
@rpc_call
|
1951
2024
|
def set_color(self, background_color: str, font_color: str) -> None:
|
1952
2025
|
"""
|
1953
|
-
Set the background color of the
|
2026
|
+
Set the background color of the widget.
|
1954
2027
|
|
1955
2028
|
Args:
|
1956
2029
|
background_color (str): The color to set the background in HEX.
|
@@ -1960,13 +2033,19 @@ class TextBox(RPCBase):
|
|
1960
2033
|
@rpc_call
|
1961
2034
|
def set_text(self, text: str) -> None:
|
1962
2035
|
"""
|
1963
|
-
Set the text of the
|
2036
|
+
Set the text of the widget.
|
2037
|
+
|
2038
|
+
Args:
|
2039
|
+
text (str): The text to set.
|
1964
2040
|
"""
|
1965
2041
|
|
1966
2042
|
@rpc_call
|
1967
2043
|
def set_font_size(self, size: int) -> None:
|
1968
2044
|
"""
|
1969
|
-
Set the font size of the text in the
|
2045
|
+
Set the font size of the text in the widget.
|
2046
|
+
|
2047
|
+
Args:
|
2048
|
+
size (int): The font size to set.
|
1970
2049
|
"""
|
1971
2050
|
|
1972
2051
|
|
@@ -9,7 +9,7 @@ import redis
|
|
9
9
|
from bec_lib.client import BECClient
|
10
10
|
from bec_lib.redis_connector import MessageObject, RedisConnector
|
11
11
|
from bec_lib.service_config import ServiceConfig
|
12
|
-
from qtpy.QtCore import QCoreApplication, QObject
|
12
|
+
from qtpy.QtCore import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QCoreApplication, QObject
|
13
13
|
from qtpy.QtCore import Signal as pyqtSignal
|
14
14
|
|
15
15
|
if TYPE_CHECKING:
|
@@ -115,9 +115,16 @@ class BECDispatcher:
|
|
115
115
|
def reset_singleton(cls):
|
116
116
|
cls._instance = None
|
117
117
|
cls._initialized = False
|
118
|
-
|
118
|
+
|
119
|
+
if not cls.qapp:
|
120
|
+
return
|
121
|
+
|
122
|
+
# shutdown QCoreApp if it exists
|
123
|
+
if PYQT5 or PYQT6:
|
119
124
|
cls.qapp.exit()
|
120
|
-
|
125
|
+
elif PYSIDE2 or PYSIDE6:
|
126
|
+
cls.qapp.shutdown()
|
127
|
+
cls.qapp = None
|
121
128
|
|
122
129
|
def connect_slot(
|
123
130
|
self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]]
|
File without changes
|
@@ -0,0 +1,352 @@
|
|
1
|
+
"""This module contains the BECStatusBox widget, which displays the status of different BEC services in a collapsible tree widget.
|
2
|
+
The widget automatically updates the status of all running BEC services, and displays their status.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
import sys
|
8
|
+
from typing import TYPE_CHECKING
|
9
|
+
|
10
|
+
import qdarktheme
|
11
|
+
from bec_lib.utils.import_utils import lazy_import_from
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
13
|
+
from qtpy.QtCore import QObject, QTimer, Signal, Slot
|
14
|
+
from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
15
|
+
|
16
|
+
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
17
|
+
from bec_widgets.widgets.bec_status_box.status_item import StatusItem
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from bec_lib.client import BECClient
|
21
|
+
|
22
|
+
# TODO : Put normal imports back when Pydantic gets faster
|
23
|
+
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
|
24
|
+
|
25
|
+
|
26
|
+
class BECStatusBoxConfig(ConnectionConfig):
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
class BECServiceInfoContainer(BaseModel):
|
31
|
+
"""Container to store information about the BEC services."""
|
32
|
+
|
33
|
+
service_name: str
|
34
|
+
status: BECStatus | str = Field(
|
35
|
+
default="NOTCONNECTED",
|
36
|
+
description="The status of the service. Can be any of the BECStatus names, or NOTCONNECTED.",
|
37
|
+
)
|
38
|
+
info: dict
|
39
|
+
metrics: dict | None
|
40
|
+
model_config: dict = {"validate_assignment": True}
|
41
|
+
|
42
|
+
@field_validator("status")
|
43
|
+
@classmethod
|
44
|
+
def validate_status(cls, v):
|
45
|
+
"""Validate input for status. Accept BECStatus and NOTCONNECTED.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
v (BECStatus | str): The input value.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: The validated status.
|
52
|
+
"""
|
53
|
+
if v in list(BECStatus.__members__.values()):
|
54
|
+
return v.name
|
55
|
+
if v in list(BECStatus.__members__.keys()) or v == "NOTCONNECTED":
|
56
|
+
return v
|
57
|
+
raise ValueError(
|
58
|
+
f"Status must be one of {BECStatus.__members__.values()} or 'NOTCONNECTED'. Input {v}"
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
class BECServiceStatusMixin(QObject):
|
63
|
+
"""A mixin class to update the service status, and metrics.
|
64
|
+
It emits a signal 'services_update' when the service status is updated.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
client (BECClient): The client object to connect to the BEC server.
|
68
|
+
"""
|
69
|
+
|
70
|
+
services_update = Signal(dict, dict)
|
71
|
+
|
72
|
+
def __init__(self, client: BECClient):
|
73
|
+
super().__init__()
|
74
|
+
self.client = client
|
75
|
+
self._service_update_timer = QTimer()
|
76
|
+
self._service_update_timer.timeout.connect(self._get_service_status)
|
77
|
+
self._service_update_timer.start(1000)
|
78
|
+
|
79
|
+
def _get_service_status(self):
|
80
|
+
"""Pull latest service and metrics updates from REDIS for all services, and emit both via 'services_update' signal."""
|
81
|
+
# pylint: disable=protected-access
|
82
|
+
self.client._update_existing_services()
|
83
|
+
self.services_update.emit(self.client._services_info, self.client._services_metric)
|
84
|
+
|
85
|
+
|
86
|
+
class BECStatusBox(BECConnector, QTreeWidget):
|
87
|
+
"""A widget to display the status of different BEC services.
|
88
|
+
This widget automatically updates the status of all running BEC services, and displays their status.
|
89
|
+
Information about the individual services is collapsible, and double clicking on
|
90
|
+
the individual service will display the metrics about the service.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
parent Optional : The parent widget for the BECStatusBox. Defaults to None.
|
94
|
+
service_name Optional(str): The name of the top service label. Defaults to "BEC Server".
|
95
|
+
client Optional(BECClient): The client object to connect to the BEC server. Defaults to None
|
96
|
+
config Optional(BECStatusBoxConfig | dict): The configuration for the status box. Defaults to None.
|
97
|
+
gui_id Optional(str): The unique id for the widget. Defaults to None.
|
98
|
+
"""
|
99
|
+
|
100
|
+
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
|
101
|
+
|
102
|
+
service_update = Signal(dict)
|
103
|
+
bec_core_state = Signal(str)
|
104
|
+
|
105
|
+
def __init__(
|
106
|
+
self,
|
107
|
+
parent=None,
|
108
|
+
service_name: str = "BEC Server",
|
109
|
+
client: BECClient = None,
|
110
|
+
config: BECStatusBoxConfig | dict = None,
|
111
|
+
gui_id: str = None,
|
112
|
+
):
|
113
|
+
if config is None:
|
114
|
+
config = BECStatusBoxConfig(widget_class=self.__class__.__name__)
|
115
|
+
else:
|
116
|
+
if isinstance(config, dict):
|
117
|
+
config = BECStatusBoxConfig(**config)
|
118
|
+
super().__init__(client=client, config=config, gui_id=gui_id)
|
119
|
+
QTreeWidget.__init__(self, parent=parent)
|
120
|
+
|
121
|
+
self.service_name = service_name
|
122
|
+
self.config = config
|
123
|
+
|
124
|
+
self.bec_service_info_container = {}
|
125
|
+
self.tree_items = {}
|
126
|
+
self.tree_top_item = None
|
127
|
+
self.bec_service_status = BECServiceStatusMixin(client=self.client)
|
128
|
+
|
129
|
+
self.init_ui()
|
130
|
+
self.bec_service_status.services_update.connect(self.update_service_status)
|
131
|
+
self.bec_core_state.connect(self.update_top_item_status)
|
132
|
+
self.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
|
133
|
+
|
134
|
+
def init_ui(self) -> None:
|
135
|
+
"""Initialize the UI for the status box, and add QTreeWidget as the basis for the status box."""
|
136
|
+
self.init_ui_tree_widget()
|
137
|
+
top_label = self._create_status_widget(self.service_name, status=BECStatus.IDLE)
|
138
|
+
self.tree_top_item = QTreeWidgetItem()
|
139
|
+
self.tree_top_item.setExpanded(True)
|
140
|
+
self.tree_top_item.setDisabled(True)
|
141
|
+
self.addTopLevelItem(self.tree_top_item)
|
142
|
+
self.setItemWidget(self.tree_top_item, 0, top_label)
|
143
|
+
self.service_update.connect(top_label.update_config)
|
144
|
+
|
145
|
+
def _create_status_widget(
|
146
|
+
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
|
147
|
+
) -> StatusItem:
|
148
|
+
"""Creates a StatusItem (QWidget) for the given service, and stores all relevant
|
149
|
+
information about the service in the bec_service_info_container.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
service_name (str): The name of the service.
|
153
|
+
status (BECStatus): The status of the service.
|
154
|
+
info Optional(dict): The information about the service. Default is {}
|
155
|
+
metric Optional(dict): Metrics for the respective service. Default is None
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
StatusItem: The status item widget.
|
159
|
+
"""
|
160
|
+
if info is None:
|
161
|
+
info = {}
|
162
|
+
self._update_bec_service_container(service_name, status, info, metrics)
|
163
|
+
item = StatusItem(
|
164
|
+
parent=self,
|
165
|
+
config={
|
166
|
+
"service_name": service_name,
|
167
|
+
"status": status.name,
|
168
|
+
"info": info,
|
169
|
+
"metrics": metrics,
|
170
|
+
},
|
171
|
+
)
|
172
|
+
return item
|
173
|
+
|
174
|
+
@Slot(str)
|
175
|
+
def update_top_item_status(self, status: BECStatus) -> None:
|
176
|
+
"""Method to update the status of the top item in the tree widget.
|
177
|
+
Gets the status from the Signal 'bec_core_state' and updates the StatusItem via the signal 'service_update'.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
status (BECStatus): The state of the core services.
|
181
|
+
"""
|
182
|
+
self.bec_service_info_container[self.service_name].status = status
|
183
|
+
self.service_update.emit(self.bec_service_info_container[self.service_name].model_dump())
|
184
|
+
|
185
|
+
def _update_bec_service_container(
|
186
|
+
self, service_name: str, status: BECStatus, info: dict, metrics: dict = None
|
187
|
+
) -> None:
|
188
|
+
"""Update the bec_service_info_container with the newest status and metrics for the BEC service.
|
189
|
+
If information about the service already exists, it will create a new entry.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
service_name (str): The name of the service.
|
193
|
+
service_info (StatusMessage): A class containing the service status.
|
194
|
+
service_metric (ServiceMetricMessage): A class containing the service metrics.
|
195
|
+
"""
|
196
|
+
container = self.bec_service_info_container.get(service_name, None)
|
197
|
+
if container:
|
198
|
+
container.status = status
|
199
|
+
container.info = info
|
200
|
+
container.metrics = metrics
|
201
|
+
return
|
202
|
+
service_info_item = BECServiceInfoContainer(
|
203
|
+
service_name=service_name, status=status, info=info, metrics=metrics
|
204
|
+
)
|
205
|
+
self.bec_service_info_container.update({service_name: service_info_item})
|
206
|
+
|
207
|
+
@Slot(dict, dict)
|
208
|
+
def update_service_status(self, services_info: dict, services_metric: dict) -> None:
|
209
|
+
"""Callback function services_metric from BECServiceStatusMixin.
|
210
|
+
It updates the status of all services.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
services_info (dict): A dictionary containing the service status for all running BEC services.
|
214
|
+
services_metric (dict): A dictionary containing the service metrics for all running BEC services.
|
215
|
+
"""
|
216
|
+
checked = []
|
217
|
+
services_info = self.update_core_services(services_info, services_metric)
|
218
|
+
checked.extend(self.CORE_SERVICES)
|
219
|
+
|
220
|
+
for service_name, msg in sorted(services_info.items()):
|
221
|
+
checked.append(service_name)
|
222
|
+
metric_msg = services_metric.get(service_name, None)
|
223
|
+
metrics = metric_msg.metrics if metric_msg else None
|
224
|
+
if service_name in self.tree_items:
|
225
|
+
self._update_bec_service_container(
|
226
|
+
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
|
227
|
+
)
|
228
|
+
self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
|
229
|
+
continue
|
230
|
+
|
231
|
+
item_widget = self._create_status_widget(
|
232
|
+
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
|
233
|
+
)
|
234
|
+
item = QTreeWidgetItem()
|
235
|
+
item.setDisabled(True)
|
236
|
+
self.service_update.connect(item_widget.update_config)
|
237
|
+
self.tree_top_item.addChild(item)
|
238
|
+
self.setItemWidget(item, 0, item_widget)
|
239
|
+
self.tree_items.update({service_name: (item, item_widget)})
|
240
|
+
|
241
|
+
self.check_redundant_tree_items(checked)
|
242
|
+
|
243
|
+
def update_core_services(self, services_info: dict, services_metric: dict) -> dict:
|
244
|
+
"""Method to process status and metrics updates of core services (stored in CORE_SERVICES).
|
245
|
+
If a core services is not connected, it should not be removed from the status widget
|
246
|
+
|
247
|
+
Args:
|
248
|
+
services_info (dict): A dictionary containing the service status of different services.
|
249
|
+
services_metric (dict): A dictionary containing the service metrics of different services.
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
dict: The services_info dictionary after removing the info updates related to the CORE_SERVICES
|
253
|
+
"""
|
254
|
+
bec_core_state = "RUNNING"
|
255
|
+
for service_name in sorted(self.CORE_SERVICES):
|
256
|
+
metric_msg = services_metric.get(service_name, None)
|
257
|
+
metrics = metric_msg.metrics if metric_msg else None
|
258
|
+
if service_name not in services_info:
|
259
|
+
self.bec_service_info_container[service_name].status = "NOTCONNECTED"
|
260
|
+
bec_core_state = "ERROR"
|
261
|
+
else:
|
262
|
+
msg = services_info.pop(service_name)
|
263
|
+
self._update_bec_service_container(
|
264
|
+
service_name=service_name, status=msg.status, info=msg.info, metrics=metrics
|
265
|
+
)
|
266
|
+
bec_core_state = (
|
267
|
+
"RUNNING" if (msg.status.value > 1 and bec_core_state == "RUNNING") else "ERROR"
|
268
|
+
)
|
269
|
+
|
270
|
+
if service_name in self.tree_items:
|
271
|
+
self.service_update.emit(self.bec_service_info_container[service_name].model_dump())
|
272
|
+
continue
|
273
|
+
self.add_tree_item(service_name, msg.status, msg.info, metrics)
|
274
|
+
|
275
|
+
self.bec_core_state.emit(bec_core_state)
|
276
|
+
return services_info
|
277
|
+
|
278
|
+
def check_redundant_tree_items(self, checked: list) -> None:
|
279
|
+
"""Utility method to check and remove redundant objects from the BECStatusBox.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
checked (list): A list of services that are currently running.
|
283
|
+
"""
|
284
|
+
to_be_deleted = [key for key in self.tree_items if key not in checked]
|
285
|
+
|
286
|
+
for key in to_be_deleted:
|
287
|
+
item, _ = self.tree_items.pop(key)
|
288
|
+
self.tree_top_item.removeChild(item)
|
289
|
+
|
290
|
+
def add_tree_item(
|
291
|
+
self, service_name: str, status: BECStatus, info: dict = None, metrics: dict = None
|
292
|
+
) -> None:
|
293
|
+
"""Method to add a new QTreeWidgetItem together with a StatusItem to the tree widget.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
service_name (str): The name of the service.
|
297
|
+
service_status_msg (StatusMessage): The status of the service.
|
298
|
+
metrics (dict): The metrics of the service.
|
299
|
+
"""
|
300
|
+
item_widget = self._create_status_widget(
|
301
|
+
service_name=service_name, status=status, info=info, metrics=metrics
|
302
|
+
)
|
303
|
+
item = QTreeWidgetItem()
|
304
|
+
self.service_update.connect(item_widget.update_config)
|
305
|
+
self.tree_top_item.addChild(item)
|
306
|
+
self.setItemWidget(item, 0, item_widget)
|
307
|
+
self.tree_items.update({service_name: (item, item_widget)})
|
308
|
+
|
309
|
+
def init_ui_tree_widget(self) -> None:
|
310
|
+
"""Initialise the tree widget for the status box."""
|
311
|
+
self.setHeaderHidden(True)
|
312
|
+
self.setStyleSheet(
|
313
|
+
"QTreeWidget::item:!selected "
|
314
|
+
"{ "
|
315
|
+
"border: 1px solid gainsboro; "
|
316
|
+
"border-left: none; "
|
317
|
+
"border-top: none; "
|
318
|
+
"}"
|
319
|
+
"QTreeWidget::item:selected {}"
|
320
|
+
)
|
321
|
+
|
322
|
+
@Slot(QTreeWidgetItem, int)
|
323
|
+
def on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None:
|
324
|
+
"""Callback function for double clicks on individual QTreeWidgetItems in the collapsed section.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
item (QTreeWidgetItem): The item that was double clicked.
|
328
|
+
column (int): The column that was double clicked.
|
329
|
+
"""
|
330
|
+
for _, (tree_item, status_widget) in self.tree_items.items():
|
331
|
+
if tree_item == item:
|
332
|
+
status_widget.show_popup()
|
333
|
+
|
334
|
+
def closeEvent(self, event):
|
335
|
+
super().cleanup()
|
336
|
+
QTreeWidget().closeEvent(event)
|
337
|
+
|
338
|
+
|
339
|
+
def main():
|
340
|
+
"""Main method to run the BECStatusBox widget."""
|
341
|
+
# pylint: disable=import-outside-toplevel
|
342
|
+
from qtpy.QtWidgets import QApplication
|
343
|
+
|
344
|
+
app = QApplication(sys.argv)
|
345
|
+
qdarktheme.setup_theme("auto")
|
346
|
+
main_window = BECStatusBox()
|
347
|
+
main_window.show()
|
348
|
+
sys.exit(app.exec())
|
349
|
+
|
350
|
+
|
351
|
+
if __name__ == "__main__":
|
352
|
+
main()
|
@@ -0,0 +1,171 @@
|
|
1
|
+
""" Module for a StatusItem widget to display status and metrics for a BEC service.
|
2
|
+
The widget is bound to be used with the BECStatusBox widget."""
|
3
|
+
|
4
|
+
import enum
|
5
|
+
import sys
|
6
|
+
from datetime import datetime
|
7
|
+
|
8
|
+
import qdarktheme
|
9
|
+
from bec_lib.utils.import_utils import lazy_import_from
|
10
|
+
from pydantic import Field
|
11
|
+
from qtpy.QtCore import Qt, Slot
|
12
|
+
from qtpy.QtWidgets import QDialog, QHBoxLayout, QLabel, QStyle, QVBoxLayout, QWidget
|
13
|
+
|
14
|
+
from bec_widgets.utils.bec_connector import ConnectionConfig
|
15
|
+
|
16
|
+
# TODO : Put normal imports back when Pydantic gets faster
|
17
|
+
BECStatus = lazy_import_from("bec_lib.messages", ("BECStatus",))
|
18
|
+
|
19
|
+
|
20
|
+
class IconsEnum(enum.Enum):
|
21
|
+
"""Enum class for icons in the status item widget."""
|
22
|
+
|
23
|
+
RUNNING = "SP_DialogApplyButton"
|
24
|
+
BUSY = "SP_BrowserReload"
|
25
|
+
IDLE = "SP_MessageBoxWarning"
|
26
|
+
ERROR = "SP_DialogCancelButton"
|
27
|
+
NOTCONNECTED = "SP_TitleBarContextHelpButton"
|
28
|
+
|
29
|
+
|
30
|
+
class StatusWidgetConfig(ConnectionConfig):
|
31
|
+
"""Configuration class for the status item widget."""
|
32
|
+
|
33
|
+
service_name: str
|
34
|
+
status: str
|
35
|
+
info: dict
|
36
|
+
metrics: dict | None
|
37
|
+
icon_size: tuple = Field(default=(24, 24), description="The size of the icon in the widget.")
|
38
|
+
font_size: int = Field(16, description="The font size of the text in the widget.")
|
39
|
+
|
40
|
+
|
41
|
+
class StatusItem(QWidget):
|
42
|
+
"""A widget to display the status of a service.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
parent: The parent widget.
|
46
|
+
config (dict): The configuration for the service.
|
47
|
+
"""
|
48
|
+
|
49
|
+
def __init__(self, parent=None, config: dict = None):
|
50
|
+
if config is None:
|
51
|
+
config = StatusWidgetConfig(widget_class=self.__class__.__name__)
|
52
|
+
else:
|
53
|
+
if isinstance(config, dict):
|
54
|
+
config = StatusWidgetConfig(**config)
|
55
|
+
self.config = config
|
56
|
+
QWidget.__init__(self, parent=parent)
|
57
|
+
self.parent = parent
|
58
|
+
self.layout = None
|
59
|
+
self.config = config
|
60
|
+
self._popup_label_ref = {}
|
61
|
+
self._label = None
|
62
|
+
self._icon = None
|
63
|
+
self.init_ui()
|
64
|
+
|
65
|
+
def init_ui(self) -> None:
|
66
|
+
"""Init the UI for the status item widget."""
|
67
|
+
self.layout = QHBoxLayout()
|
68
|
+
self.layout.setContentsMargins(5, 5, 5, 5)
|
69
|
+
self.setLayout(self.layout)
|
70
|
+
self._label = QLabel()
|
71
|
+
self._icon = QLabel()
|
72
|
+
self.layout.addWidget(self._label)
|
73
|
+
self.layout.addWidget(self._icon)
|
74
|
+
self.update_ui()
|
75
|
+
|
76
|
+
@Slot(dict)
|
77
|
+
def update_config(self, config: dict) -> None:
|
78
|
+
"""Update the configuration of the status item widget.
|
79
|
+
This method is invoked from the parent widget.
|
80
|
+
The UI values are later updated based on the new configuration.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
config (dict): Config updates from parent widget.
|
84
|
+
"""
|
85
|
+
if config["service_name"] != self.config.service_name:
|
86
|
+
return
|
87
|
+
self.config.status = config["status"]
|
88
|
+
self.config.info = config["info"]
|
89
|
+
self.config.metrics = config["metrics"]
|
90
|
+
self.update_ui()
|
91
|
+
|
92
|
+
def update_ui(self) -> None:
|
93
|
+
"""Update the UI of the labels, and popup dialog."""
|
94
|
+
self.set_text()
|
95
|
+
self.set_status()
|
96
|
+
self._set_popup_text()
|
97
|
+
|
98
|
+
def set_text(self) -> None:
|
99
|
+
"""Set the text of the QLabel basae on the config."""
|
100
|
+
service = self.config.service_name
|
101
|
+
status = self.config.status
|
102
|
+
if "BECClient" in service.split("/"):
|
103
|
+
service = service.split("/")[0] + "/..." + service.split("/")[1][-4:]
|
104
|
+
if status == "NOTCONNECTED":
|
105
|
+
status = "NOT CONNECTED"
|
106
|
+
text = f"{service} is {status}"
|
107
|
+
self._label.setText(text)
|
108
|
+
|
109
|
+
def set_status(self) -> None:
|
110
|
+
"""Set the status icon for the status item widget."""
|
111
|
+
icon_name = IconsEnum[self.config.status].value
|
112
|
+
icon = self.style().standardIcon(getattr(QStyle.StandardPixmap, icon_name))
|
113
|
+
self._icon.setPixmap(icon.pixmap(*self.config.icon_size))
|
114
|
+
self._icon.setAlignment(Qt.AlignmentFlag.AlignRight)
|
115
|
+
|
116
|
+
def show_popup(self) -> None:
|
117
|
+
"""Method that is invoked when the user double clicks on the StatusItem widget."""
|
118
|
+
dialog = QDialog(self)
|
119
|
+
dialog.setWindowTitle(f"{self.config.service_name} Details")
|
120
|
+
layout = QVBoxLayout()
|
121
|
+
popup_label = self._make_popup_label()
|
122
|
+
self._set_popup_text()
|
123
|
+
layout.addWidget(popup_label)
|
124
|
+
dialog.setLayout(layout)
|
125
|
+
dialog.finished.connect(self._cleanup_popup_label)
|
126
|
+
dialog.exec()
|
127
|
+
|
128
|
+
def _make_popup_label(self) -> QLabel:
|
129
|
+
"""Create a QLabel for the popup dialog.
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
QLabel: The label for the popup dialog.
|
133
|
+
"""
|
134
|
+
label = QLabel()
|
135
|
+
label.setWordWrap(True)
|
136
|
+
self._popup_label_ref.update({"label": label})
|
137
|
+
return label
|
138
|
+
|
139
|
+
def _set_popup_text(self) -> None:
|
140
|
+
"""Compile the metrics text for the status item widget."""
|
141
|
+
if self._popup_label_ref.get("label") is None:
|
142
|
+
return
|
143
|
+
metrics_text = (
|
144
|
+
f"<b>SERVICE:</b> {self.config.service_name}<br><b>STATUS:</b> {self.config.status}<br>"
|
145
|
+
)
|
146
|
+
if self.config.metrics:
|
147
|
+
for key, value in self.config.metrics.items():
|
148
|
+
if key == "create_time":
|
149
|
+
value = datetime.fromtimestamp(value).strftime("%Y-%m-%d %H:%M:%S")
|
150
|
+
metrics_text += f"<b>{key.upper()}:</b> {value}<br>"
|
151
|
+
self._popup_label_ref["label"].setText(metrics_text)
|
152
|
+
|
153
|
+
def _cleanup_popup_label(self) -> None:
|
154
|
+
"""Cleanup the popup label."""
|
155
|
+
self._popup_label_ref.clear()
|
156
|
+
|
157
|
+
|
158
|
+
def main():
|
159
|
+
"""Run the status item widget."""
|
160
|
+
# pylint: disable=import-outside-toplevel
|
161
|
+
from qtpy.QtWidgets import QApplication
|
162
|
+
|
163
|
+
app = QApplication(sys.argv)
|
164
|
+
qdarktheme.setup_theme("auto")
|
165
|
+
main_window = StatusItem()
|
166
|
+
main_window.show()
|
167
|
+
sys.exit(app.exec())
|
168
|
+
|
169
|
+
|
170
|
+
if __name__ == "__main__":
|
171
|
+
main()
|
@@ -2,11 +2,11 @@
|
|
2
2
|
.gitlab-ci.yml,sha256=RnYDz4zKXjlqltTryprlB1s5vLXxI2-seW-Vb70NNF0,8162
|
3
3
|
.pylintrc,sha256=OstrgmEyP0smNFBKoIN5_26-UmNZgMHnbjvAWX0UrLs,18535
|
4
4
|
.readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
|
5
|
-
CHANGELOG.md,sha256=
|
5
|
+
CHANGELOG.md,sha256=WkuQFvZMK4X4aOXLwutfGkFzmzy-SLKr8ez6vvTXiBY,6851
|
6
6
|
LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
7
|
-
PKG-INFO,sha256=
|
7
|
+
PKG-INFO,sha256=9Cs9V5N0bA887XBwZGWJ08opZvj8rpWinMW3R2r56rs,1302
|
8
8
|
README.md,sha256=y4jB6wvArS7N8_iTbKWnSM_oRAqLA2GqgzUR-FMh5sU,2645
|
9
|
-
pyproject.toml,sha256=
|
9
|
+
pyproject.toml,sha256=gK6dcc2qr6EKwZzWQhdkPeABrJhhE93t6qRmseFko80,2162
|
10
10
|
.git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
|
11
11
|
.gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
|
12
12
|
.gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
|
@@ -17,7 +17,7 @@ bec_widgets/assets/bec_widgets_icon.png,sha256=K8dgGwIjalDh9PRHUsSQBqgdX7a00nM3i
|
|
17
17
|
bec_widgets/assets/terminal_icon.png,sha256=bJl7Tft4Fi2uxvuXI8o14uMHnI9eAWKSU2uftXCH9ws,3889
|
18
18
|
bec_widgets/cli/__init__.py,sha256=d0Q6Fn44e7wFfLabDOBxpcJ1DPKWlFunGYDUBmO-4hA,22
|
19
19
|
bec_widgets/cli/auto_updates.py,sha256=DyBV3HnjMSH-cvVkYNcDiYKVf0Xut4Qy2qGQqkW47Bw,4833
|
20
|
-
bec_widgets/cli/client.py,sha256=
|
20
|
+
bec_widgets/cli/client.py,sha256=Zd4oMSE5-HY3IBUIVcparGGV2Ew86gaWAFTd4OjFVmg,58005
|
21
21
|
bec_widgets/cli/client_utils.py,sha256=D076XKwcukKBKknd11B1UyOcQN_9sN7ZKMVttyCxS9Q,11586
|
22
22
|
bec_widgets/cli/generate_cli.py,sha256=Bi8HxHhge1I87vbdYHZUZiZwvbB-OSkLYS5Xfmwiz9M,4922
|
23
23
|
bec_widgets/cli/rpc_register.py,sha256=QxXUZu5XNg00Yf5O3UHWOXg3-f_pzKjjoZYMOa-MOJc,2216
|
@@ -32,7 +32,7 @@ bec_widgets/examples/motor_movement/motor_control_compilations.py,sha256=8rpA7a2
|
|
32
32
|
bec_widgets/examples/motor_movement/motor_controller.ui,sha256=83XX6NGILwntoUIghvzWnMuGf80O8khK3SduVKTAEFM,29105
|
33
33
|
bec_widgets/utils/__init__.py,sha256=1930ji1Jj6dVuY81Wd2kYBhHYNV-2R0bN_L4o9zBj1U,533
|
34
34
|
bec_widgets/utils/bec_connector.py,sha256=RxHJNF7JjtY5pRbTMu2eQTiRXvoyJ53QuTYxHjZba38,5357
|
35
|
-
bec_widgets/utils/bec_dispatcher.py,sha256=
|
35
|
+
bec_widgets/utils/bec_dispatcher.py,sha256=vvHpD_7ZddRmD6mHR5JWU_XEJYxognpmG7CnEWMjaZk,5989
|
36
36
|
bec_widgets/utils/bec_table.py,sha256=nA2b8ukSeUfquFMAxGrUVOqdrzMoDYD6O_4EYbOG2zk,717
|
37
37
|
bec_widgets/utils/colors.py,sha256=GYSDe0ZxsJSwxvuy-yG2BH17qlf_Sjq8dhDcyp9IhBI,8532
|
38
38
|
bec_widgets/utils/container_utils.py,sha256=m3VUyAYmSWkEwApP9tBvKxPYVtc2kHw4toxIpMryJy4,1495
|
@@ -47,6 +47,9 @@ bec_widgets/utils/validator_delegate.py,sha256=Emj1WF6W8Ke1ruBWUfmHdVJpmOSPezuOt
|
|
47
47
|
bec_widgets/utils/widget_io.py,sha256=f36198CvT_EzWQ_cg2G-4tRRsaMdJ3yVqsZWKJCQEfA,10880
|
48
48
|
bec_widgets/utils/yaml_dialog.py,sha256=cMVif-39SB9WjwGH5FWBJcFs4tnfFJFs5cacydRyhy0,1853
|
49
49
|
bec_widgets/widgets/__init__.py,sha256=6RE9Pot2ud6BNJc_ZKiE--U-lgVRUff2IVR91lPcCbo,214
|
50
|
+
bec_widgets/widgets/bec_status_box/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
|
+
bec_widgets/widgets/bec_status_box/bec_status_box.py,sha256=ARdVzwabmbT_Jti6Wp-5KrxDQIingYVC-pZQHd6uz7o,14375
|
52
|
+
bec_widgets/widgets/bec_status_box/status_item.py,sha256=wPkDm0GCGNXXpy3rR_Ljaxy0ZHeiiYcrWFqEntZnz4E,5869
|
50
53
|
bec_widgets/widgets/buttons/__init__.py,sha256=74ucIRU6-anoqQ-zT7wbrysmxhg_3_04xGhN_kllNUI,48
|
51
54
|
bec_widgets/widgets/buttons/stop_button/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
52
55
|
bec_widgets/widgets/buttons/stop_button/stop_button.py,sha256=x4a7RvlMkHzOd05zKOGYkyTmBza7Me7jgOL9WIgA_c4,906
|
@@ -103,7 +106,7 @@ docs/Makefile,sha256=i2WHuFlgfyAPEW4ssEP8NY4cOibDJrVjvzSEU8_Ggwc,634
|
|
103
106
|
docs/conf.py,sha256=HxLxupNGu0Smhwn57g1kFdjZzFuaWVREgRJKhT1zi2k,2464
|
104
107
|
docs/index.md,sha256=8ZCgaLIbJsYvt-jwi--QxsNwnK4-k3rejIeOOLclG40,1101
|
105
108
|
docs/make.bat,sha256=vKazJE8RW49Cy8K7hm8QYbletvAd8YkeKsaPA_dWnXs,800
|
106
|
-
docs/requirements.txt,sha256=
|
109
|
+
docs/requirements.txt,sha256=TcjRnzVYFOg64N-lJc2sn-G6YQdhPjzp0lrLuPsPMXY,156
|
107
110
|
docs/_static/custom.css,sha256=v4Nk7r8LZslhOV8RaSUb15bG4miwiZ4-kZyXBLnSyms,13487
|
108
111
|
docs/_templates/custom-class-template.rst,sha256=HPuPaGJob2zXlWOl5FmA-hAZRbUTGQmdqo3HS1iIFog,711
|
109
112
|
docs/_templates/custom-module-template.rst,sha256=MXYXAz06HP_mbblO--iFwL08xROmSBo7U4O-hPbMcZU,1228
|
@@ -118,7 +121,7 @@ docs/assets/rocket_launch_48dp.svg,sha256=pdrPrBcKWUa5OlgWKM0B6TA6qAW7E57d7C7YW2
|
|
118
121
|
docs/developer/developer.md,sha256=VUdMnQBsSRCawYMFCe0vya5oj1MimLML7Pd58kY6fYY,765
|
119
122
|
docs/developer/getting_started/development.md,sha256=aYLmuLMYpp5FcIXeDUqCfcStIV8veuiMBjOt5bTW_30,1406
|
120
123
|
docs/developer/getting_started/getting_started.md,sha256=My_K_6O7LLaXVB_eINrRku5o-jVx95lsmGgHxgZhT7A,378
|
121
|
-
docs/developer/widgets/widgets.md,sha256=
|
124
|
+
docs/developer/widgets/widgets.md,sha256=aNsJgG7R-3EerumNB6GH84JLIXfZqGN5GjvpKWDi0Hk,504
|
122
125
|
docs/introduction/introduction.md,sha256=wp7jmhkUtJnSnEnmIAZGUcau_3-5e5-FohvZb63khw4,1432
|
123
126
|
docs/user/customisation.md,sha256=wCW8fAbqtlgGE3mURvXOrK67Xo0_B-lxfg0sYuQWB40,3186
|
124
127
|
docs/user/user.md,sha256=uCTcjclIi6rdjYRQebko6bWFEVsjyfshsVU3BDYrC-Y,1403
|
@@ -128,11 +131,13 @@ docs/user/getting_started/BECDockArea.png,sha256=t3vSm_rVRk371J5LOutbolETuEjStNc
|
|
128
131
|
docs/user/getting_started/auto_updates.md,sha256=Gicx3lplI6JRBlnPj_VL6IhqOIcsWjYF4_EdZSCje2A,3754
|
129
132
|
docs/user/getting_started/getting_started.md,sha256=lxZXCr6HAkM61oo5Bu-YjINSKo4wihWhAPJdotEAAVQ,358
|
130
133
|
docs/user/getting_started/gui_complex_gui.gif,sha256=ovv9u371BGG5GqhzyBMl4mvqMHLfJS0ylr-dR0Ydwtw,6550393
|
131
|
-
docs/user/getting_started/installation.md,sha256=
|
134
|
+
docs/user/getting_started/installation.md,sha256=5_fPbmUqLGtwOskFHTlytd4PJKrMcHqHShzM9ymM0oI,1149
|
132
135
|
docs/user/getting_started/quick_start.md,sha256=VGU880GwamcIZcBE8tjxuqX2syE-71jqZedtskCoBbA,9405
|
133
136
|
docs/user/widgets/BECFigure.png,sha256=8dQr4u0uk_y0VV-R1Jh9yTR3Vidd9HDEno_07R0swaE,1605920
|
134
137
|
docs/user/widgets/bec_figure.md,sha256=BwcumbhZd6a2zKmoHTvwKr8kG8WxBx9lS_QwxNiBMpQ,5155
|
135
|
-
docs/user/widgets/
|
138
|
+
docs/user/widgets/bec_status_box.gif,sha256=kLxf40HbS6fjdUIQ2b9SiduBEXdBd4DDWGEnQDOFMcY,1259044
|
139
|
+
docs/user/widgets/bec_status_box.md,sha256=0ILY12UnSjiVCtd5qpC8G2dPBYhug3J_rmQLilDxulY,1164
|
140
|
+
docs/user/widgets/buttons.md,sha256=Yci21PmxlRvfKcrvY7mVI7JkECPmE6j9WyWyH3j7Y2o,1221
|
136
141
|
docs/user/widgets/image_plot.gif,sha256=_mVFhMTXGqwDOcEtrBHMZj5Thn2sLhDAHEeL2XyHN-s,14098977
|
137
142
|
docs/user/widgets/motor.gif,sha256=FtaWdRHx4UZaGJPpq8LNhMMgX4PFcAB6IZ93JCMEh_w,2280719
|
138
143
|
docs/user/widgets/progress_bar.gif,sha256=5jh0Zw2BBGPuNxszV1DBLJCb4_6glIRX-U2ABjnsK2k,5263592
|
@@ -141,7 +146,7 @@ docs/user/widgets/spiral_progress_bar.md,sha256=QTgUDIl6XPuK_HwSfB6sNijZ4bss26bi
|
|
141
146
|
docs/user/widgets/text_box.md,sha256=_ST7RQWXl67MKLm6dTa995GjoughPUyK_hLnF8SPZcM,925
|
142
147
|
docs/user/widgets/w1D.gif,sha256=tuHbleJpl6bJFNNC2OdndF5LF7IyfvlkFCMGZajrQPs,622773
|
143
148
|
docs/user/widgets/website.md,sha256=wfudAupdtHX-Sfritg0xMWXZLLczJ4XwMLNWvu6ww-w,705
|
144
|
-
docs/user/widgets/widgets.md,sha256=
|
149
|
+
docs/user/widgets/widgets.md,sha256=6H8C8M2fFmTxFFlrAuOE-jBpOUVUrOIIzWP0l_EwMGo,397
|
145
150
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
146
151
|
tests/end-2-end/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
147
152
|
tests/end-2-end/conftest.py,sha256=j1O1SxXRJ8jcrunn6dcfbZLK2Jc-VUxyh9ZuCSc6Qj4,1816
|
@@ -156,6 +161,7 @@ tests/unit_tests/test_bec_dispatcher.py,sha256=rYPiRizHaswhGZw55IBMneDFxmPiCCLAZ
|
|
156
161
|
tests/unit_tests/test_bec_dock.py,sha256=BXKXpuyIYj-l6KSyhQtM_p3kRFCRECIoXLzvkcJZDlM,3611
|
157
162
|
tests/unit_tests/test_bec_figure.py,sha256=aEd2R8K6fU2ON8QvPemGWpql_LaaYLipRlvnjBY2qFA,8009
|
158
163
|
tests/unit_tests/test_bec_motor_map.py,sha256=AfD_9-x6VV3TPnkQgNfFYRndPHDsGx-a_YknFeDr6hc,4588
|
164
|
+
tests/unit_tests/test_bec_status_box.py,sha256=zZ4pe7DaBzzpRsy62yHFkUGgAGb3zZU3I6zQIPsqUTY,6070
|
159
165
|
tests/unit_tests/test_client_utils.py,sha256=eViJ1Tz-HX9TkMvQH6W8cO-c3_1I8bUc4_Yen6LOc0E,830
|
160
166
|
tests/unit_tests/test_color_validation.py,sha256=csdvVKAohENZIRY-JQ97Hv-TShb1erj4oKMX7QRwo78,1883
|
161
167
|
tests/unit_tests/test_crosshair.py,sha256=3OMAJ2ZaISYXMOtkXf1rPdy94vCr8njeLi6uHblBL9Q,5045
|
@@ -180,8 +186,8 @@ tests/unit_tests/test_configs/config_device_no_entry.yaml,sha256=hdvue9KLc_kfNzG
|
|
180
186
|
tests/unit_tests/test_configs/config_scan.yaml,sha256=vo484BbWOjA_e-h6bTjSV9k7QaQHrlAvx-z8wtY-P4E,1915
|
181
187
|
tests/unit_tests/test_msgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
182
188
|
tests/unit_tests/test_msgs/available_scans_message.py,sha256=m_z97hIrjHXXMa2Ex-UvsPmTxOYXfjxyJaGkIY6StTY,46532
|
183
|
-
bec_widgets-0.
|
184
|
-
bec_widgets-0.
|
185
|
-
bec_widgets-0.
|
186
|
-
bec_widgets-0.
|
187
|
-
bec_widgets-0.
|
189
|
+
bec_widgets-0.67.0.dist-info/METADATA,sha256=9Cs9V5N0bA887XBwZGWJ08opZvj8rpWinMW3R2r56rs,1302
|
190
|
+
bec_widgets-0.67.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
191
|
+
bec_widgets-0.67.0.dist-info/entry_points.txt,sha256=OvoqiNzNF9bizFQNhbAmmdc_njHrnVewLE-Kl-u9sh0,115
|
192
|
+
bec_widgets-0.67.0.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
|
193
|
+
bec_widgets-0.67.0.dist-info/RECORD,,
|
docs/requirements.txt
CHANGED
@@ -13,13 +13,13 @@ To install BEC Widgets using the pip package manager, execute the following comm
|
|
13
13
|
|
14
14
|
|
15
15
|
```bash
|
16
|
-
pip install bec_widgets[pyqt6]
|
16
|
+
pip install 'bec_widgets[pyqt6]'
|
17
17
|
```
|
18
18
|
|
19
19
|
In case you want to use PyQt5, you can install it by using the following command:
|
20
20
|
|
21
21
|
```bash
|
22
|
-
pip install bec_widgets[pyqt5]
|
22
|
+
pip install 'bec_widgets[pyqt5]'
|
23
23
|
```
|
24
24
|
|
25
25
|
**Troubleshooting**
|
Binary file
|
@@ -0,0 +1,30 @@
|
|
1
|
+
(user.widgets.bec_status_box)=
|
2
|
+
# BEC Status Box
|
3
|
+
**Purpose:**
|
4
|
+
|
5
|
+
The [BECStatusBox]](/api_reference/_autosummary/bec_widgets.cli.client.BECStatusBox) Widget is a widget that allows you to monitor the status/health of the all running BEC processes. The widget generates the view automatically and updates the status of the processes in real-time. The top level indicates the overall state of the BEC core services (DeviceServer, ScanServer, SciHub, ScanBundler and FileWriter), but you can also see the status of each individual process by opening the collapsed view. In the collapsed view, you can double click on each process to get a popup window with live updates of the metrics for each process in real-time.
|
6
|
+
|
7
|
+
**Key Features:**
|
8
|
+
|
9
|
+
- monitor the state of individual BEC services.
|
10
|
+
- automatically track BEC services, i.e. additional clients connecting.
|
11
|
+
- live-updates of the metrics for each process.
|
12
|
+
|
13
|
+
**Example of Use:**
|
14
|
+

|
15
|
+
|
16
|
+
**Code example:**
|
17
|
+
|
18
|
+
The following code snipped demonstrates how to create a `BECStatusBox` widget using BEC Widgets within BEC.
|
19
|
+
```python
|
20
|
+
bec_status_box = gui.add_dock().add_widget("BECStatusBox")
|
21
|
+
```
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
|
docs/user/widgets/buttons.md
CHANGED
docs/user/widgets/widgets.md
CHANGED
pyproject.toml
CHANGED
@@ -0,0 +1,152 @@
|
|
1
|
+
import re
|
2
|
+
from unittest import mock
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
from bec_lib.messages import BECStatus, ServiceMetricMessage, StatusMessage
|
6
|
+
from qtpy.QtCore import QMetaMethod
|
7
|
+
|
8
|
+
from bec_widgets.widgets.bec_status_box.bec_status_box import BECServiceInfoContainer, BECStatusBox
|
9
|
+
|
10
|
+
from .client_mocks import mocked_client
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.fixture
|
14
|
+
def status_box(qtbot, mocked_client):
|
15
|
+
with mock.patch(
|
16
|
+
"bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
|
17
|
+
) as mock_service_status_mixin:
|
18
|
+
widget = BECStatusBox(client=mocked_client)
|
19
|
+
qtbot.addWidget(widget)
|
20
|
+
qtbot.waitExposed(widget)
|
21
|
+
yield widget
|
22
|
+
|
23
|
+
|
24
|
+
def test_status_box_init(qtbot, mocked_client):
|
25
|
+
with mock.patch(
|
26
|
+
"bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
|
27
|
+
) as mock_service_status_mixin:
|
28
|
+
name = "my test"
|
29
|
+
widget = BECStatusBox(parent=None, service_name=name, client=mocked_client)
|
30
|
+
qtbot.addWidget(widget)
|
31
|
+
qtbot.waitExposed(widget)
|
32
|
+
assert widget.headerItem().DontShowIndicator.value == 1
|
33
|
+
assert widget.children()[0].children()[0].config.service_name == name
|
34
|
+
|
35
|
+
|
36
|
+
def test_update_top_item(qtbot, mocked_client):
|
37
|
+
with (
|
38
|
+
mock.patch(
|
39
|
+
"bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
|
40
|
+
) as mock_service_status_mixin,
|
41
|
+
mock.patch(
|
42
|
+
"bec_widgets.widgets.bec_status_box.status_item.StatusItem.update_config"
|
43
|
+
) as mock_update,
|
44
|
+
):
|
45
|
+
name = "my test"
|
46
|
+
widget = BECStatusBox(parent=None, service_name=name, client=mocked_client)
|
47
|
+
qtbot.addWidget(widget)
|
48
|
+
qtbot.waitExposed(widget)
|
49
|
+
widget.update_top_item_status(status="RUNNING")
|
50
|
+
assert widget.bec_service_info_container[name].status == "RUNNING"
|
51
|
+
assert mock_update.call_args == mock.call(widget.bec_service_info_container[name].dict())
|
52
|
+
|
53
|
+
|
54
|
+
def test_create_status_widget(status_box):
|
55
|
+
name = "test_service"
|
56
|
+
status = BECStatus.IDLE
|
57
|
+
info = {"test": "test"}
|
58
|
+
metrics = {"metric": "test_metric"}
|
59
|
+
item = status_box._create_status_widget(name, status, info, metrics)
|
60
|
+
assert item.config.service_name == name
|
61
|
+
assert item.config.status == status.name
|
62
|
+
assert item.config.info == info
|
63
|
+
assert item.config.metrics == metrics
|
64
|
+
|
65
|
+
|
66
|
+
def test_bec_service_container(status_box):
|
67
|
+
name = "test_service"
|
68
|
+
status = BECStatus.IDLE
|
69
|
+
info = {"test": "test"}
|
70
|
+
metrics = {"metric": "test_metric"}
|
71
|
+
expected_return = BECServiceInfoContainer(
|
72
|
+
service_name=name, status=status, info=info, metrics=metrics
|
73
|
+
)
|
74
|
+
assert status_box.service_name in status_box.bec_service_info_container
|
75
|
+
assert len(status_box.bec_service_info_container) == 1
|
76
|
+
status_box._update_bec_service_container(name, status, info, metrics)
|
77
|
+
assert len(status_box.bec_service_info_container) == 2
|
78
|
+
assert status_box.bec_service_info_container[name] == expected_return
|
79
|
+
|
80
|
+
|
81
|
+
def test_add_tree_item(status_box):
|
82
|
+
name = "test_service"
|
83
|
+
status = BECStatus.IDLE
|
84
|
+
info = {"test": "test"}
|
85
|
+
metrics = {"metric": "test_metric"}
|
86
|
+
assert len(status_box.children()[0].children()) == 1
|
87
|
+
status_box.add_tree_item(name, status, info, metrics)
|
88
|
+
assert len(status_box.children()[0].children()) == 2
|
89
|
+
assert name in status_box.tree_items
|
90
|
+
|
91
|
+
|
92
|
+
def test_update_service_status(status_box):
|
93
|
+
"""Also checks check redundant tree items"""
|
94
|
+
name = "test_service"
|
95
|
+
status = BECStatus.IDLE
|
96
|
+
info = {"test": "test"}
|
97
|
+
metrics = {"metric": "test_metric"}
|
98
|
+
status_box.add_tree_item(name, status, info, {})
|
99
|
+
not_connected_name = "invalid_service"
|
100
|
+
status_box.add_tree_item(not_connected_name, status, info, metrics)
|
101
|
+
|
102
|
+
services_status = {name: StatusMessage(name=name, status=status, info=info)}
|
103
|
+
services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
|
104
|
+
|
105
|
+
with mock.patch.object(status_box, "update_core_services", return_value=services_status):
|
106
|
+
assert not_connected_name in status_box.tree_items
|
107
|
+
status_box.update_service_status(services_status, services_metrics)
|
108
|
+
assert status_box.tree_items[name][1].config.metrics == metrics
|
109
|
+
assert not_connected_name not in status_box.tree_items
|
110
|
+
|
111
|
+
|
112
|
+
def test_update_core_services(qtbot, mocked_client):
|
113
|
+
with (
|
114
|
+
mock.patch(
|
115
|
+
"bec_widgets.widgets.bec_status_box.bec_status_box.BECServiceStatusMixin"
|
116
|
+
) as mock_service_status_mixin,
|
117
|
+
mock.patch(
|
118
|
+
"bec_widgets.widgets.bec_status_box.bec_status_box.BECStatusBox.update_top_item_status"
|
119
|
+
) as mock_update,
|
120
|
+
):
|
121
|
+
name = "my test"
|
122
|
+
status_box = BECStatusBox(parent=None, service_name=name, client=mocked_client)
|
123
|
+
qtbot.addWidget(status_box)
|
124
|
+
qtbot.waitExposed(status_box)
|
125
|
+
status_box.CORE_SERVICES = ["test_service"]
|
126
|
+
name = "test_service"
|
127
|
+
status = BECStatus.RUNNING
|
128
|
+
info = {"test": "test"}
|
129
|
+
metrics = {"metric": "test_metric"}
|
130
|
+
services_status = {name: StatusMessage(name=name, status=status, info=info)}
|
131
|
+
services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
|
132
|
+
|
133
|
+
status_box.update_core_services(services_status, services_metrics)
|
134
|
+
assert mock_update.call_args == mock.call(status.name)
|
135
|
+
|
136
|
+
status = BECStatus.IDLE
|
137
|
+
services_status = {name: StatusMessage(name=name, status=status, info=info)}
|
138
|
+
services_metrics = {name: ServiceMetricMessage(name=name, metrics=metrics)}
|
139
|
+
status_box.update_core_services(services_status, services_metrics)
|
140
|
+
assert mock_update.call_args == mock.call("ERROR")
|
141
|
+
|
142
|
+
|
143
|
+
def test_double_click_item(status_box):
|
144
|
+
name = "test_service"
|
145
|
+
status = BECStatus.IDLE
|
146
|
+
info = {"test": "test"}
|
147
|
+
metrics = {"MyData": "This should be shown nicely"}
|
148
|
+
status_box.add_tree_item(name, status, info, metrics)
|
149
|
+
item, status_item = status_box.tree_items[name]
|
150
|
+
with mock.patch.object(status_item, "show_popup") as mock_show_popup:
|
151
|
+
status_box.itemDoubleClicked.emit(item, 0)
|
152
|
+
assert mock_show_popup.call_count == 1
|
File without changes
|
File without changes
|
File without changes
|