0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(queue): BECQueue controls extended with Resume, Stop, Abort, Reset buttons

This commit is contained in:
2024-08-29 17:01:47 +02:00
committed by wyzula_j
parent df5eff3147
commit 0d7c10e670
9 changed files with 201 additions and 28 deletions

View File

@ -162,6 +162,7 @@ class WidgetAction(ToolBarAction):
def add_to_toolbar(self, toolbar, target):
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
if self.label is not None:
label = QLabel(f"{self.label}")
layout.addWidget(label)

View File

@ -1,11 +1,18 @@
from __future__ import annotations
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QHBoxLayout, QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from qtpy.QtCore import Property, Qt, Slot
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QHeaderView, QLabel, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget
from bec_qthemes import material_icon
from bec_widgets.qt_utils.toolbar import ModularToolBar, SeparatorAction, WidgetAction
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.button_abort.button_abort import AbortButton
from bec_widgets.widgets.button_reset.button_reset import ResetButton
from bec_widgets.widgets.button_resume.button_resume import ResumeButton
from bec_widgets.widgets.stop_button.stop_button import StopButton
class BECQueue(BECWidget, QWidget):
@ -14,6 +21,15 @@ class BECQueue(BECWidget, QWidget):
"""
ICON_NAME = "edit_note"
status_colors = {
"STOPPED": "red",
"PENDING": "orange",
"IDLE": "gray",
"PAUSED": "yellow",
"DEFERRED_PAUSE": "lightyellow",
"RUNNING": "green",
"COMPLETED": "blue",
}
def __init__(
self,
@ -21,19 +37,79 @@ class BECQueue(BECWidget, QWidget):
client=None,
config: ConnectionConfig = None,
gui_id: str = None,
refresh_upon_start: bool = True,
):
super().__init__(client, config, gui_id)
QWidget.__init__(self, parent=parent)
self.table = QTableWidget(self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.table)
self.layout = QVBoxLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
# Set up the toolbar
self.set_toolbar()
# Set up the table
self.table = QTableWidget(self)
self.layout.addWidget(self.table)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"])
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
self.reset_content()
if refresh_upon_start:
self.refresh_queue()
def set_toolbar(self):
"""
Set the toolbar.
"""
widget_label = QLabel("Live Queue")
widget_label.setStyleSheet("font-weight: bold;")
self.toolbar = ModularToolBar(
actions={
"widget_label": WidgetAction(widget=widget_label),
"separator_1": SeparatorAction(),
"resume": WidgetAction(widget=ResumeButton(toolbar=False)),
"stop": WidgetAction(widget=StopButton(toolbar=False)),
"reset": WidgetAction(widget=ResetButton(toolbar=False)),
},
target_widget=self,
)
self.layout.addWidget(self.toolbar)
@Property(bool)
def hide_toolbar(self):
"""Property to hide the BEC Queue toolbar."""
return not self.toolbar.isVisible()
@hide_toolbar.setter
def hide_toolbar(self, hide: bool):
"""
Setters for the hide_toolbar property.
Args:
hide(bool): Whether to hide the toolbar.
"""
self._hide_toolbar(hide)
def _hide_toolbar(self, hide: bool):
"""
Hide the toolbar.
Args:
hide(bool): Whether to hide the toolbar.
"""
self.toolbar.setVisible(not hide)
def refresh_queue(self):
"""
Refresh the queue.
"""
msg = self.client.connector.get(MessageEndpoints.scan_queue_status())
self.update_queue(msg.content, msg.metadata)
@Slot(dict, dict)
def update_queue(self, content, _metadata):
@ -57,6 +133,7 @@ class BECQueue(BECWidget, QWidget):
blocks = item.get("request_blocks", [])
scan_types = []
scan_numbers = []
scan_ids = []
status = item.get("status", "")
for request_block in blocks:
scan_type = request_block.get("content", {}).get("scan_type", "")
@ -65,13 +142,18 @@ class BECQueue(BECWidget, QWidget):
scan_number = request_block.get("scan_number", "")
if scan_number:
scan_numbers.append(str(scan_number))
scan_id = request_block.get("scan_id", "")
if scan_id:
scan_ids.append(scan_id)
if scan_types:
scan_types = ", ".join(scan_types)
if scan_numbers:
scan_numbers = ", ".join(scan_numbers)
self.set_row(index, scan_numbers, scan_types, status)
if scan_ids:
scan_ids = ", ".join(scan_ids)
self.set_row(index, scan_numbers, scan_types, status, scan_ids)
def format_item(self, content: str) -> QTableWidgetItem:
def format_item(self, content: str, status=False) -> QTableWidgetItem:
"""
Format the content of the table item.
@ -85,9 +167,17 @@ class BECQueue(BECWidget, QWidget):
content = ""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
# item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
if status:
try:
color = self.status_colors.get(content, "black") # Default to black if not found
item.setForeground(QColor(color))
except:
return item
return item
def set_row(self, index: int, scan_number: str, scan_type: str, status: str):
def set_row(self, index: int, scan_number: str, scan_type: str, status: str, scan_id: str):
"""
Set the row of the table.
@ -97,10 +187,42 @@ class BECQueue(BECWidget, QWidget):
scan_type (str): The scan type.
status (str): The status.
"""
abort_button = self._create_abort_button(scan_id)
abort_button.button.clicked.connect(self.delete_selected_row)
self.table.setItem(index, 0, self.format_item(scan_number))
self.table.setItem(index, 1, self.format_item(scan_type))
self.table.setItem(index, 2, self.format_item(status))
self.table.setItem(index, 2, self.format_item(status, status=True))
self.table.setCellWidget(index, 3, abort_button)
def _create_abort_button(self, scan_id: str) -> AbortButton:
"""
Create an abort button with styling for BEC Queue widget for certain scan_id.
Args:
scan_id(str): The scan id to abort.
Returns:
AbortButton: The abort button.
"""
abort_button = AbortButton(scan_id=scan_id)
abort_button.button.setText("")
abort_button.button.setIcon(
material_icon("cancel", color="#cc181e", filled=True, convert_to_pixmap=False)
)
abort_button.button.setStyleSheet("background-color: rgba(0,0,0,0) ")
abort_button.button.setFlat(True)
return abort_button
def delete_selected_row(self):
button = self.sender()
row = self.table.indexAt(button.pos()).row()
self.table.removeRow(row)
button.deleteLater()
def reset_content(self):
"""
@ -108,7 +230,7 @@ class BECQueue(BECWidget, QWidget):
"""
self.table.setRowCount(1)
self.set_row(0, "", "", "")
self.set_row(0, "", "", "", "")
if __name__ == "__main__": # pragma: no cover

View File

@ -11,7 +11,9 @@ class AbortButton(BECWidget, QWidget):
ICON_NAME = "cancel"
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False):
def __init__(
self, parent=None, client=None, config=None, gui_id=None, toolbar=False, scan_id=None
):
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
@ -25,17 +27,19 @@ class AbortButton(BECWidget, QWidget):
if toolbar:
icon = material_icon("cancel", color="#666666", filled=True)
self.button = QToolButton(icon=icon)
self.button.triggered.connect(self.abort_scan)
self.button.setToolTip("Abort the scan")
else:
self.button = QPushButton()
self.button.setText("Abort")
self.button.setStyleSheet(
"background-color: #666666; color: white; font-weight: bold; font-size: 12px;"
)
self.button.clicked.connect(self.abort_scan)
self.button.clicked.connect(self.abort_scan)
self.layout.addWidget(self.button)
self.scan_id = scan_id
@SafeSlot()
def abort_scan(
self,
@ -46,4 +50,8 @@ class AbortButton(BECWidget, QWidget):
Args:
scan_id(str|None): The scan id to abort. If None, the current scan will be aborted.
"""
self.queue.request_scan_abortion()
if self.scan_id is not None:
print(f"Aborting scan with scan_id: {self.scan_id}")
self.queue.request_scan_abortion(scan_id=self.scan_id)
else:
self.queue.request_scan_abortion()

View File

@ -23,16 +23,18 @@ class ResetButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("restart_alt", color="#F19E39", filled=True)
icon = material_icon(
"restart_alt", color="#F19E39", filled=True, convert_to_pixmap=False
)
self.button = QToolButton(icon=icon)
self.button.triggered.connect(self.reset_queue)
self.button.setToolTip("Reset the scan queue")
else:
self.button = QPushButton()
self.button.setText("Reset Queue")
self.button.setStyleSheet(
"background-color: #F19E39; color: white; font-weight: bold; font-size: 12px;"
)
self.button.clicked.connect(self.reset_queue)
self.button.clicked.connect(self.reset_queue)
self.layout.addWidget(self.button)

View File

@ -23,16 +23,16 @@ class ResumeButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("resume", color="#2793e8", filled=True)
icon = material_icon("resume", color="#2793e8", filled=True, convert_to_pixmap=False)
self.button = QToolButton(icon=icon)
self.button.triggered.connect(self.continue_scan)
self.button.setToolTip("Resume the scan queue")
else:
self.button = QPushButton()
self.button.setText("Resume")
self.button.setStyleSheet(
"background-color: #2793e8; color: white; font-weight: bold; font-size: 12px;"
)
self.button.clicked.connect(self.continue_scan)
self.button.clicked.connect(self.continue_scan)
self.layout.addWidget(self.button)

View File

@ -23,16 +23,16 @@ class StopButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("stop", color="#cc181e", filled=True)
icon = material_icon("stop", color="#cc181e", filled=True, convert_to_pixmap=False)
self.button = QToolButton(icon=icon)
self.button.triggered.connect(self.stop_scan)
self.button.setToolTip("Stop the scan queue")
else:
self.button = QPushButton()
self.button.setText("Stop")
self.button.setStyleSheet(
"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
)
self.button.clicked.connect(self.stop_scan)
self.button.clicked.connect(self.stop_scan)
self.layout.addWidget(self.button)

View File

@ -4,11 +4,12 @@
````{tab} Overview
The [`BEC Queue Widget`](/api_reference/_autosummary/bec_widgets.cli.client.BECQueue) provides a real-time display of the BEC scan queue, allowing users to monitor and track the status of ongoing and pending scans. The widget automatically updates to reflect the current state of the scan queue, displaying critical information such as scan numbers, types, and statuses. This widget is particularly useful for users who need to manage and oversee multiple scans in the BEC environment.
The [`BEC Queue Widget`](/api_reference/_autosummary/bec_widgets.cli.client.BECQueue) provides a real-time display and control of the BEC scan queue, allowing users to monitor, manage, and control the status of ongoing and pending scans. The widget automatically updates to reflect the current state of the scan queue, displaying critical information such as scan numbers, types, and statuses. Additionally, it provides control options to stop individual scans, stop the entire queue, resume, and reset the queue, making it a powerful tool for managing scan operations in the BEC environment.
## Key Features:
- **Real-Time Queue Monitoring**: Displays the current state of the BEC scan queue, with automatic updates as the queue changes.
- **Detailed Scan Information**: Provides a clear view of scan numbers, types, and statuses, helping users track the progress and state of each scan.
- **Queue Control**: Allows users to stop specific scans, stop the entire queue, resume paused scans, and reset the queue.
- **Interactive Table Layout**: The queue is presented in a table format, with customizable columns that stretch to fit the available space.
- **Flexible Integration**: The widget can be integrated into both [`BECDockArea`](user.widgets.bec_dock_area) and used as an individual component in your application through `QtDesigner`.

View File

@ -2,10 +2,13 @@
from unittest.mock import MagicMock, patch
import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_lib.messages import ScanQueueStatusMessage
from bec_widgets.widgets.dock import BECDock, BECDockArea
from .client_mocks import mocked_client
from .test_bec_queue import bec_queue_msg_full
@pytest.fixture
@ -135,7 +138,10 @@ def test_toolbar_add_device_positioner_box(bec_dock_area):
)
def test_toolbar_add_utils_queue(bec_dock_area):
def test_toolbar_add_utils_queue(bec_dock_area, bec_queue_msg_full):
bec_dock_area.client.connector.set_and_publish(
MessageEndpoints.scan_queue_status(), bec_queue_msg_full
)
bec_dock_area.toolbar.widgets["menu_utils"].widgets["queue"].trigger()
assert "queue_1" in bec_dock_area.panels
assert bec_dock_area.panels["queue_1"].widgets[0].config.widget_class == "BECQueue"

View File

@ -89,7 +89,7 @@ def bec_queue_msg_full():
@pytest.fixture
def bec_queue(qtbot, mocked_client):
widget = BECQueue(client=mocked_client)
widget = BECQueue(client=mocked_client, refresh_upon_start=False)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@ -109,3 +109,36 @@ def test_bec_queue_empty(bec_queue):
assert bec_queue.table.item(0, 0).text() == ""
assert bec_queue.table.item(0, 1).text() == ""
assert bec_queue.table.item(0, 2).text() == ""
def test_queue_abort(bec_queue, bec_queue_msg_full):
# test if abort buttons are not leaking, so making 3 times the same abort request and fixture should check the leak of the buttons automatically
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button.button.click()
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button.button.click()
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
abort_button = bec_queue.table.cellWidget(0, 3)
abort_button.button.click()