mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-08 01:37:52 +01:00
feat: add procedure panel with control and logs
This commit is contained in:
@@ -133,8 +133,4 @@ if __name__ == "__main__":
|
||||
exclusive=True,
|
||||
)
|
||||
_app.show()
|
||||
# developer_view.show()
|
||||
# developer_view.setWindowTitle("Developer View")
|
||||
# developer_view.resize(1920, 1080)
|
||||
# developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -4,9 +4,10 @@ import re
|
||||
|
||||
import markdown
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import ProcedureRequestMessage
|
||||
from bec_lib.script_executor import upload_script
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtGui import QKeySequence, QShortcut
|
||||
from qtpy.QtGui import QKeySequence, QShortcut # type: ignore
|
||||
from qtpy.QtWidgets import QTextEdit
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
@@ -16,6 +17,7 @@ from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.containers.qt_ads import CDockWidget
|
||||
from bec_widgets.widgets.control.procedure_control.procedure_panel import ProcedurePanel
|
||||
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
from bec_widgets.widgets.editors.web_console.web_console import BECShell, WebConsole
|
||||
@@ -125,6 +127,9 @@ class DeveloperWidget(DockAreaWidget):
|
||||
self._current_script_id: str | None = None
|
||||
self.script_editor_tab = None
|
||||
|
||||
self.procedures = ProcedurePanel(self)
|
||||
self.procedures.setObjectName("Procedure Control")
|
||||
|
||||
self._initialize_layout()
|
||||
|
||||
# Connect editor signals
|
||||
@@ -183,24 +188,16 @@ class DeveloperWidget(DockAreaWidget):
|
||||
)
|
||||
|
||||
# Plotting area on the right with signature help tabbed alongside
|
||||
self.plotting_ads_dock = self.new(
|
||||
self.plotting_ads,
|
||||
where="right",
|
||||
closable=False,
|
||||
floatable=False,
|
||||
movable=False,
|
||||
return_dock=True,
|
||||
title_buttons={"float": True},
|
||||
)
|
||||
self.signature_dock = self.new(
|
||||
self.signature_help,
|
||||
closable=False,
|
||||
floatable=False,
|
||||
movable=False,
|
||||
tab_with=self.plotting_ads_dock,
|
||||
return_dock=True,
|
||||
title_buttons={"float": False, "close": False},
|
||||
)
|
||||
_r_panel = {
|
||||
"closable": False,
|
||||
"floatable": False,
|
||||
"movable": False,
|
||||
"return_dock": True,
|
||||
"title_buttons": {"float": True},
|
||||
}
|
||||
self.plotting_dock = self.new(self.plotting_ads, where="right", **_r_panel)
|
||||
self.signature_dock = self.new(self.signature_help, **_r_panel, tab_with=self.plotting_dock)
|
||||
self.procedure_dock = self.new(self.procedures, **_r_panel, tab_with=self.plotting_dock)
|
||||
|
||||
self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3])
|
||||
|
||||
@@ -233,6 +230,16 @@ class DeveloperWidget(DockAreaWidget):
|
||||
run_action.action.triggered.connect(self.on_execute)
|
||||
self.toolbar.components.add_safe("run", run_action)
|
||||
|
||||
submit_action = MaterialIconAction(
|
||||
icon_name="animated_images",
|
||||
tooltip="Run current file as a BEC procedure",
|
||||
label_text="Run on server",
|
||||
filled=True,
|
||||
parent=self,
|
||||
)
|
||||
submit_action.action.triggered.connect(self.on_submit_procedure)
|
||||
self.toolbar.components.add_safe("run_proc", submit_action)
|
||||
|
||||
stop_action = MaterialIconAction(
|
||||
icon_name="stop",
|
||||
tooltip="Stop current execution",
|
||||
@@ -246,6 +253,7 @@ class DeveloperWidget(DockAreaWidget):
|
||||
execution_bundle = ToolbarBundle("execution", self.toolbar.components)
|
||||
execution_bundle.add_action("run")
|
||||
execution_bundle.add_action("stop")
|
||||
execution_bundle.add_action("run_proc")
|
||||
self.toolbar.add_bundle(execution_bundle)
|
||||
|
||||
vim_action = MaterialIconAction(
|
||||
@@ -305,24 +313,41 @@ class DeveloperWidget(DockAreaWidget):
|
||||
self.toolbar.components.get_action("save").action.setEnabled(enabled)
|
||||
self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
|
||||
|
||||
@SafeSlot()
|
||||
def on_execute(self):
|
||||
"""Upload and run the currently focused script in the Monaco editor."""
|
||||
def _try_upload(self) -> str | None:
|
||||
self.script_editor_tab = self.monaco.last_focused_editor
|
||||
if not self.script_editor_tab:
|
||||
return
|
||||
widget = self.script_editor_tab.widget()
|
||||
if not isinstance(widget, MonacoWidget):
|
||||
return
|
||||
return None
|
||||
if not isinstance(widget := self.script_editor_tab.widget(), MonacoWidget):
|
||||
return None
|
||||
if widget.modified:
|
||||
# Save the file before execution if there are unsaved changes
|
||||
self.monaco.save_file()
|
||||
if widget.modified:
|
||||
# If still modified, user likely cancelled save dialog
|
||||
return
|
||||
self.current_script_id = upload_script(self.client.connector, widget.get_text())
|
||||
self.console.write(f'bec._run_script("{self.current_script_id}")')
|
||||
return None
|
||||
return upload_script(self.client.connector, widget.get_text())
|
||||
|
||||
@SafeSlot()
|
||||
def on_execute(self):
|
||||
"""Upload and run the currently focused script in the Monaco editor."""
|
||||
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||
if (script_id := self._try_upload()) is not None:
|
||||
self.current_script_id = script_id
|
||||
self.console.write(f'bec._run_script("{self.current_script_id}")')
|
||||
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||
|
||||
@SafeSlot()
|
||||
def on_submit_procedure(self):
|
||||
"""Upload and run the currently focused script in the Monaco editor as a procedure."""
|
||||
if (script_id := self._try_upload()) is not None:
|
||||
self.current_script_id = script_id
|
||||
print(f"Uploaded script with ID: {self.current_script_id}")
|
||||
self.client.connector.xadd(
|
||||
MessageEndpoints.procedure_request(),
|
||||
ProcedureRequestMessage(
|
||||
identifier="run_script", args_kwargs=((self.current_script_id,), {})
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def on_stop(self):
|
||||
|
||||
@@ -12,8 +12,12 @@ from bec_lib.messages import (
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from bec_server.scan_server.procedures.helper import FrontendProcedureHelper
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from qtpy.QtCore import QSize, Qt, Signal
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QToolButton,
|
||||
QTreeWidget,
|
||||
QTreeWidgetItem,
|
||||
@@ -107,6 +111,9 @@ class JobItem(_ActionItem):
|
||||
self._msg = config.msg
|
||||
self._init_params_display()
|
||||
|
||||
def queue(self):
|
||||
return self._msg.queue
|
||||
|
||||
def _init_params_display(self):
|
||||
self.setText(self._config.params_column, self._short_params_text())
|
||||
self.setToolTip(self._config.params_column, self._long_params_html())
|
||||
@@ -176,6 +183,9 @@ class QueueItem(_ActionItem):
|
||||
_ItemConfig(base=self._config, msg=msg),
|
||||
)
|
||||
|
||||
def queue(self):
|
||||
return self._queue
|
||||
|
||||
@SafeSlot()
|
||||
def _abort_self(self):
|
||||
self._config.helper.request.abort_queue(self._queue)
|
||||
@@ -203,10 +213,13 @@ class CategoryItem(QTreeWidgetItem):
|
||||
self._queues[queue] = QueueItem(
|
||||
self, [queue], _QueueConfig(base=self._config, queue=queue, msgs=msgs)
|
||||
)
|
||||
self._queues[queue].setExpanded(True)
|
||||
|
||||
|
||||
class ProcedureControl(BECWidget, QWidget):
|
||||
|
||||
queue_selected = Signal(str)
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs):
|
||||
config = config or ConnectionConfig()
|
||||
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
@@ -215,6 +228,17 @@ class ProcedureControl(BECWidget, QWidget):
|
||||
self._setup_ui()
|
||||
self.bec_dispatcher.connect_slot(self._update, MessageEndpoints.procedure_queue_notif())
|
||||
self._init_queues()
|
||||
self._content.itemSelectionChanged.connect(self.on_selection_changed)
|
||||
|
||||
def on_selection_changed(self):
|
||||
selected_items = self._content.selectedItems()
|
||||
if len(selected_items) != 1:
|
||||
self.queue_selected.emit("")
|
||||
return
|
||||
if isinstance((item := selected_items[0]), (QueueItem, JobItem)):
|
||||
self.queue_selected.emit(item.queue())
|
||||
return
|
||||
self.queue_selected.emit("")
|
||||
|
||||
@SafeSlot(ProcedureQNotifMessage, dict)
|
||||
def _update(self, msg: dict | ProcedureQNotifMessage, _):
|
||||
@@ -235,7 +259,6 @@ class ProcedureControl(BECWidget, QWidget):
|
||||
self._content.setAlternatingRowColors(True)
|
||||
self._content.setHeaderLabels(["name", "status", "params", "actions"])
|
||||
self._layout.addWidget(self._content)
|
||||
self._content.header().resizeSection(0, 250)
|
||||
|
||||
config = partial(_BaseConfig, helper=self._helper, tree=self._content, actions_column=3)
|
||||
|
||||
@@ -245,6 +268,7 @@ class ProcedureControl(BECWidget, QWidget):
|
||||
config(actions={"abort"}, child_actions={"abort"}, active_queue=True),
|
||||
)
|
||||
self._content.addTopLevelItem(self._active_queues)
|
||||
self._active_queues.setExpanded(True)
|
||||
|
||||
self._unhandled_queues = CategoryItem(
|
||||
self._content,
|
||||
@@ -252,6 +276,7 @@ class ProcedureControl(BECWidget, QWidget):
|
||||
config(actions={"delete"}, child_actions={"delete", "resubmit"}),
|
||||
)
|
||||
self._content.addTopLevelItem(self._unhandled_queues)
|
||||
self._active_queues.setExpanded(True)
|
||||
|
||||
def _init_queues(self):
|
||||
for queue in self._helper.get.active_and_pending_queue_names():
|
||||
@@ -260,6 +285,47 @@ class ProcedureControl(BECWidget, QWidget):
|
||||
self._unhandled_queues.update(queue, self._helper.get.unhandled_queue(queue))
|
||||
|
||||
|
||||
class ProcedureSubmissionOptionsDialog(QDialog):
|
||||
"""
|
||||
Dialog to customize procedure options
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, client=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Procedure execution options")
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(600, 800)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the dialog UI with ScanControl widget and buttons."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Create the scan control widget
|
||||
|
||||
# Create dialog buttons
|
||||
button_box = QDialogButtonBox(Qt.Orientation.Horizontal, self)
|
||||
|
||||
# Create custom buttons with appropriate text
|
||||
insert_button = QPushButton("Insert")
|
||||
cancel_button = QPushButton("Cancel")
|
||||
|
||||
button_box.addButton(insert_button, QDialogButtonBox.ButtonRole.AcceptRole)
|
||||
button_box.addButton(cancel_button, QDialogButtonBox.ButtonRole.RejectRole)
|
||||
|
||||
layout.addWidget(button_box)
|
||||
|
||||
# Connect button signals
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
def accept(self):
|
||||
"""Override accept to generate code before closing."""
|
||||
super().accept()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@ class ProcedureLogs(BECWidget, QWidget):
|
||||
self._layout.addWidget(self.widget)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def _trigger_update(self, msg, _):
|
||||
def _update(self, msg, _):
|
||||
self.widget.append(msg.get("data").strip())
|
||||
|
||||
def _update(self):
|
||||
def _init_content(self):
|
||||
if self._queue is None:
|
||||
self.widget.setText("")
|
||||
return
|
||||
if msgs := self._conn.xread(MessageEndpoints.procedure_logs(self._queue)):
|
||||
self.widget.append("".join(msg.get("data").data.strip() for msg in msgs))
|
||||
if msgs := self._conn.xread(MessageEndpoints.procedure_logs(self._queue), from_start=True):
|
||||
self.widget.append("\n".join(msg.get("data").data.strip() for msg in msgs))
|
||||
|
||||
@SafeSlot(None)
|
||||
@SafeSlot(str)
|
||||
@@ -53,16 +53,18 @@ class ProcedureLogs(BECWidget, QWidget):
|
||||
|
||||
@queue.setter
|
||||
def queue(self, queue: str | None) -> None:
|
||||
if self._queue == queue:
|
||||
return
|
||||
if self._queue is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self._trigger_update, MessageEndpoints.procedure_logs(self._queue)
|
||||
self._update, MessageEndpoints.procedure_logs(self._queue)
|
||||
)
|
||||
self._queue = queue
|
||||
self._queue = queue or None
|
||||
if self._queue is not None:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self._trigger_update, MessageEndpoints.procedure_logs(self._queue)
|
||||
self._update, MessageEndpoints.procedure_logs(self._queue)
|
||||
)
|
||||
self._update()
|
||||
self._init_content()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
|
||||
from bec_widgets.widgets.control.procedure_control.procedure_control import ProcedureControl
|
||||
from bec_widgets.widgets.control.procedure_control.procedure_logs import ProcedureLogs
|
||||
|
||||
|
||||
class ProcedurePanel(DockAreaWidget):
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self.procedure_control = ProcedureControl(parent=self)
|
||||
self.procedure_control.setObjectName("Procedure Queue Control")
|
||||
self.procedure_logs = ProcedureLogs(parent=self)
|
||||
self.procedure_logs.setObjectName("Procedure Logs")
|
||||
|
||||
_dock_kwargs = {"closable": False, "movable": False, "floatable": False}
|
||||
self.new(self.procedure_control, **_dock_kwargs)
|
||||
self.new(self.procedure_logs, where="bottom", **_dock_kwargs)
|
||||
|
||||
self.procedure_control.queue_selected.connect(self.procedure_logs.set_queue)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = ProcedurePanel()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
Reference in New Issue
Block a user