GUI: added automation_progress.py and updated comapct automation view
This commit is contained in:
1 parent
86ceebb120
commit
675549c1c2
3 files changed
+327
-96
No files matched your search
@@ -412,6 +412,9 @@ class MainWindow(QMainWindow):
|
||||
self.job_list_panel.step_through_changed.connect(
|
||||
self.compact_automation_panel.set_step_through
|
||||
)
|
||||
self.job_list_panel.samples_in_queue_changed.connect(
|
||||
self.compact_automation_panel.set_samples_in_queue
|
||||
)
|
||||
self.job_list_panel.samples_in_queue_changed.connect(
|
||||
self._refresh_compact_queue_preview
|
||||
)
|
||||
@@ -422,6 +425,9 @@ class MainWindow(QMainWindow):
|
||||
self.automation_progress_panel.set_running(self.job_list_panel.is_running())
|
||||
self.compact_automation_panel.set_running(self.job_list_panel.is_running())
|
||||
self.compact_automation_panel.set_step_through(self.job_list_panel.is_step_through())
|
||||
self.compact_automation_panel.set_samples_in_queue(
|
||||
len(self.job_list_panel.table_model.samples)
|
||||
)
|
||||
self._refresh_compact_queue_preview()
|
||||
|
||||
# smargon trace panel
|
||||
@@ -515,6 +521,7 @@ class MainWindow(QMainWindow):
|
||||
self.daq.baton_response_result.connect(self._on_baton_response_result)
|
||||
self.daq.baton_timeout_checked.connect(self._on_baton_timeout_checked)
|
||||
self.daq.automation_progress.connect(self.automation_progress_panel.set_progress)
|
||||
self.daq.automation_progress.connect(self.compact_automation_panel.set_progress)
|
||||
|
||||
self.status_bar.baton_request_received.connect(self._show_baton_request_dialog)
|
||||
self.status_bar.baton_request_accepted.connect(self._accept_baton_request)
|
||||
|
||||
@@ -10,7 +10,9 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aare.common.automation_models import AutomationProgress
|
||||
from aare.common.models import SampleShortInfo
|
||||
from aare.gui.widgets.automation_progress import CompactAutomationProgressStrip
|
||||
|
||||
|
||||
class CompactAutomationPanel(QFrame):
|
||||
@@ -35,162 +37,169 @@ class CompactAutomationPanel(QFrame):
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QFrame#compactAutomationPanel {
|
||||
background: #eef4ff;
|
||||
background: #edf3fb;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
QFrame#compactTopCard,
|
||||
QFrame#compactProgressCard,
|
||||
QFrame#compactControlsCard {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9e4f2;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
QFrame#compactAutomationHeader {
|
||||
background: #1f3b63;
|
||||
border-radius: 14px;
|
||||
QFrame#compactQueueItem {
|
||||
background: #f7faff;
|
||||
border: 1px solid #dbe6f5;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
QLabel#compactAutomationTitle {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
QLabel#compactQueueTitle {
|
||||
color: #4b5563;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
QLabel#compactQueueValue {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#compactAutomationSubtitle {
|
||||
color: #d7e6ff;
|
||||
QLabel#compactDashboardLabel {
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QToolButton#compactMenuButton {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 18px;
|
||||
padding: 8px 12px;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QToolButton#compactMenuButton:hover {
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
QFrame#compactInfoPanel {
|
||||
background: white;
|
||||
border: 1px solid #d5e0f2;
|
||||
border-radius: 14px;
|
||||
QFrame#compactAutomationProgressStrip {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
QLabel#compactSectionTitle {
|
||||
color: #1f3b63;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#compactInfoLabel {
|
||||
background: #f6f9ff;
|
||||
border: 1px solid #dde7f6;
|
||||
border-radius: 10px;
|
||||
color: #24364d;
|
||||
padding: 10px 12px;
|
||||
QLabel#compactProgressSummary {
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
padding: 2px 2px 6px 2px;
|
||||
}
|
||||
|
||||
QLabel#compactProgressStep {
|
||||
background: #f8fbff;
|
||||
border: 1px solid #dbe6f5;
|
||||
border-radius: 10px;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
QPushButton#compactPrimaryButton {
|
||||
background: #2f6fed;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
border-radius: 14px;
|
||||
padding: 14px 18px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#compactPrimaryButton:hover {
|
||||
background: #255fd0;
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
QPushButton#compactSecondaryButton {
|
||||
background: white;
|
||||
color: #1f3b63;
|
||||
border: 1px solid #c8d7ef;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 14px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#compactSecondaryButton:hover,
|
||||
QToolButton#compactSecondaryButton:hover {
|
||||
background: #f4f8ff;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
QToolButton#compactSecondaryButton {
|
||||
background: white;
|
||||
color: #1f3b63;
|
||||
border: 1px solid #c8d7ef;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 14px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setSpacing(14)
|
||||
main_layout.setContentsMargins(18, 18, 18, 18)
|
||||
main_layout.setSpacing(12)
|
||||
|
||||
header = QFrame(self)
|
||||
header.setObjectName("compactAutomationHeader")
|
||||
header_layout = QHBoxLayout(header)
|
||||
header_layout.setContentsMargins(14, 12, 14, 12)
|
||||
header_layout.setSpacing(10)
|
||||
top_card = QFrame(self)
|
||||
top_card.setObjectName("compactTopCard")
|
||||
top_layout = QHBoxLayout(top_card)
|
||||
top_layout.setContentsMargins(12, 12, 12, 12)
|
||||
top_layout.setSpacing(10)
|
||||
|
||||
self.current_card = self._build_queue_item("Current", "—")
|
||||
self.next_card = self._build_queue_item("Next", "—")
|
||||
self.next_next_card = self._build_queue_item("Then", "—")
|
||||
|
||||
top_layout.addWidget(self.current_card["frame"], 2)
|
||||
top_layout.addWidget(self.next_card["frame"], 2)
|
||||
top_layout.addWidget(self.next_next_card["frame"], 2)
|
||||
|
||||
self.menu_button = QToolButton(self)
|
||||
self.menu_button.setObjectName("compactMenuButton")
|
||||
self.menu_button.setText("☰")
|
||||
self.menu_button.setToolTip("Return to full view")
|
||||
self.menu_button.setToolTip("Return to main view")
|
||||
self.menu_button.clicked.connect(self.show_full_view_requested.emit)
|
||||
top_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
title_container = QWidget(self)
|
||||
title_layout = QVBoxLayout(title_container)
|
||||
title_layout.setContentsMargins(0, 0, 0, 0)
|
||||
title_layout.setSpacing(2)
|
||||
|
||||
self.title_label = QLabel("Automation View", self)
|
||||
self.title_label.setObjectName("compactAutomationTitle")
|
||||
|
||||
self.subtitle_label = QLabel("Focused queue control and sample camera", self)
|
||||
self.subtitle_label.setObjectName("compactAutomationSubtitle")
|
||||
|
||||
title_layout.addWidget(self.title_label)
|
||||
title_layout.addWidget(self.subtitle_label)
|
||||
|
||||
header_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop)
|
||||
header_layout.addWidget(title_container, 1)
|
||||
|
||||
main_layout.addWidget(header)
|
||||
main_layout.addWidget(top_card)
|
||||
main_layout.addWidget(camera_widget, 1)
|
||||
|
||||
info_panel = QFrame(self)
|
||||
info_panel.setObjectName("compactInfoPanel")
|
||||
info_layout = QVBoxLayout(info_panel)
|
||||
info_layout.setContentsMargins(14, 14, 14, 14)
|
||||
info_layout.setSpacing(8)
|
||||
progress_card = QFrame(self)
|
||||
progress_card.setObjectName("compactProgressCard")
|
||||
progress_layout = QVBoxLayout(progress_card)
|
||||
progress_layout.setContentsMargins(12, 10, 12, 10)
|
||||
progress_layout.setSpacing(6)
|
||||
|
||||
info_title = QLabel("Queue preview", self)
|
||||
info_title.setObjectName("compactSectionTitle")
|
||||
progress_title = QLabel("Automation progress", self)
|
||||
progress_title.setObjectName("compactDashboardLabel")
|
||||
progress_layout.addWidget(progress_title)
|
||||
|
||||
self.current_label = QLabel("Current: —", self)
|
||||
self.next_label = QLabel("Next: —", self)
|
||||
self.next_next_label = QLabel("Then: —", self)
|
||||
self.progress_strip = CompactAutomationProgressStrip(self)
|
||||
progress_layout.addWidget(self.progress_strip)
|
||||
|
||||
for label in (self.current_label, self.next_label, self.next_next_label):
|
||||
label.setObjectName("compactInfoLabel")
|
||||
label.setWordWrap(True)
|
||||
main_layout.addWidget(progress_card)
|
||||
|
||||
info_layout.addWidget(info_title)
|
||||
info_layout.addWidget(self.current_label)
|
||||
info_layout.addWidget(self.next_label)
|
||||
info_layout.addWidget(self.next_next_label)
|
||||
controls_card = QFrame(self)
|
||||
controls_card.setObjectName("compactControlsCard")
|
||||
controls_layout = QVBoxLayout(controls_card)
|
||||
controls_layout.setContentsMargins(12, 12, 12, 12)
|
||||
controls_layout.setSpacing(10)
|
||||
|
||||
main_layout.addWidget(info_panel)
|
||||
controls_title = QLabel("Controls", self)
|
||||
controls_title.setObjectName("compactDashboardLabel")
|
||||
controls_layout.addWidget(controls_title)
|
||||
|
||||
button_row = QHBoxLayout()
|
||||
button_row.setSpacing(10)
|
||||
@@ -226,12 +235,38 @@ class CompactAutomationPanel(QFrame):
|
||||
button_row.addWidget(self.step_button, 1)
|
||||
button_row.addWidget(self.annotation_button, 1)
|
||||
|
||||
main_layout.addLayout(button_row)
|
||||
controls_layout.addLayout(button_row)
|
||||
main_layout.addWidget(controls_card)
|
||||
|
||||
def _build_queue_item(self, title: str, value: str) -> dict[str, QWidget | QLabel]:
|
||||
frame = QFrame(self)
|
||||
frame.setObjectName("compactQueueItem")
|
||||
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
layout.setSpacing(4)
|
||||
|
||||
title_label = QLabel(title, self)
|
||||
title_label.setObjectName("compactQueueTitle")
|
||||
|
||||
value_label = QLabel(value, self)
|
||||
value_label.setObjectName("compactQueueValue")
|
||||
value_label.setWordWrap(True)
|
||||
|
||||
layout.addWidget(title_label)
|
||||
layout.addWidget(value_label)
|
||||
|
||||
return {
|
||||
"frame": frame,
|
||||
"title": title_label,
|
||||
"value": value_label,
|
||||
}
|
||||
|
||||
@Slot(bool)
|
||||
def set_running(self, running: bool) -> None:
|
||||
self._running = bool(running)
|
||||
self.play_pause_button.setText("⏸ Pause" if self._running else "▶ Play")
|
||||
self.progress_strip.set_running(running)
|
||||
|
||||
@Slot(bool)
|
||||
def set_step_through(self, enabled: bool) -> None:
|
||||
@@ -240,6 +275,14 @@ class CompactAutomationPanel(QFrame):
|
||||
self.step_button.setChecked(self._step_through)
|
||||
self.step_button.blockSignals(False)
|
||||
|
||||
@Slot(int)
|
||||
def set_samples_in_queue(self, count: int) -> None:
|
||||
self.progress_strip.set_samples_in_queue(count)
|
||||
|
||||
@Slot(object)
|
||||
def set_progress(self, progress: AutomationProgress) -> None:
|
||||
self.progress_strip.set_progress(progress)
|
||||
|
||||
@Slot(object, object, object)
|
||||
def set_samples(
|
||||
self,
|
||||
@@ -251,9 +294,9 @@ class CompactAutomationPanel(QFrame):
|
||||
self._next_sample = next_sample
|
||||
self._next_next_sample = next_next_sample
|
||||
|
||||
self.current_label.setText(f"Current: {self._format_sample(current_sample)}")
|
||||
self.next_label.setText(f"Next: {self._format_sample(next_sample)}")
|
||||
self.next_next_label.setText(f"Then: {self._format_sample(next_next_sample)}")
|
||||
self.current_card["value"].setText(self._format_sample(current_sample))
|
||||
self.next_card["value"].setText(self._format_sample(next_sample))
|
||||
self.next_next_card["value"].setText(self._format_sample(next_next_sample))
|
||||
|
||||
def _on_step_toggled(self, checked: bool) -> None:
|
||||
self._step_through = checked
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from PySide6.QtCore import QTimer, Slot
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from aare.common.automation_models import AutomationProgress, StepStatus, WorkflowStateKind
|
||||
|
||||
|
||||
class CompactAutomationProgressStrip(QFrame):
|
||||
DEFAULT_SAMPLE_ESTIMATE_S = 150.0
|
||||
|
||||
def __init__(self, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self._progress: AutomationProgress | None = None
|
||||
self._queue_count = 0
|
||||
self._running = False
|
||||
self._step_labels: dict[WorkflowStateKind, QLabel] = {}
|
||||
self._summary_label: QLabel | None = None
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setInterval(1000)
|
||||
self._timer.timeout.connect(self._refresh_live_view)
|
||||
|
||||
self.setObjectName("compactAutomationProgressStrip")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(14, 12, 14, 12)
|
||||
layout.setSpacing(10)
|
||||
|
||||
self._summary_label = QLabel("Automation idle", self)
|
||||
self._summary_label.setObjectName("compactProgressSummary")
|
||||
self._summary_label.setWordWrap(True)
|
||||
layout.addWidget(self._summary_label)
|
||||
|
||||
steps_row = QHBoxLayout()
|
||||
steps_row.setSpacing(8)
|
||||
|
||||
for step in (
|
||||
WorkflowStateKind.MOUNT,
|
||||
WorkflowStateKind.LOOP_CENTRE,
|
||||
WorkflowStateKind.RASTER,
|
||||
WorkflowStateKind.DATA_COLLECTION,
|
||||
WorkflowStateKind.FINAL,
|
||||
):
|
||||
label = QLabel(self)
|
||||
label.setObjectName("compactProgressStep")
|
||||
label.setMinimumWidth(90)
|
||||
label.setWordWrap(True)
|
||||
steps_row.addWidget(label, 1)
|
||||
self._step_labels[step] = label
|
||||
|
||||
layout.addLayout(steps_row)
|
||||
self._apply_empty_state()
|
||||
|
||||
def _apply_empty_state(self) -> None:
|
||||
if self._summary_label is not None:
|
||||
self._summary_label.setText("Automation idle")
|
||||
for step, label in self._step_labels.items():
|
||||
label.setText(self._format_step_html(step, StepStatus.PENDING))
|
||||
|
||||
@staticmethod
|
||||
def _step_title(step: WorkflowStateKind) -> str:
|
||||
return {
|
||||
WorkflowStateKind.MOUNT: "Mount",
|
||||
WorkflowStateKind.LOOP_CENTRE: "Center",
|
||||
WorkflowStateKind.RASTER: "Raster",
|
||||
WorkflowStateKind.DATA_COLLECTION: "Collect",
|
||||
WorkflowStateKind.FINAL: "Finish",
|
||||
}.get(step, str(step.value))
|
||||
|
||||
@staticmethod
|
||||
def _step_icon(status: StepStatus) -> str:
|
||||
return {
|
||||
StepStatus.PENDING: "○",
|
||||
StepStatus.RUNNING: "◉",
|
||||
StepStatus.SUCCESS: "✓",
|
||||
StepStatus.FAILED: "✕",
|
||||
StepStatus.SKIPPED: "↷",
|
||||
StepStatus.PAUSED: "⏸",
|
||||
}.get(status, "○")
|
||||
|
||||
@staticmethod
|
||||
def _step_color(status: StepStatus) -> str:
|
||||
return {
|
||||
StepStatus.PENDING: "#64748b",
|
||||
StepStatus.RUNNING: "#2563eb",
|
||||
StepStatus.SUCCESS: "#15803d",
|
||||
StepStatus.FAILED: "#b91c1c",
|
||||
StepStatus.SKIPPED: "#64748b",
|
||||
StepStatus.PAUSED: "#c2410c",
|
||||
}.get(status, "#64748b")
|
||||
|
||||
def _format_step_html(self, step: WorkflowStateKind, status: StepStatus) -> str:
|
||||
color = self._step_color(status)
|
||||
icon = self._step_icon(status)
|
||||
title = self._step_title(step)
|
||||
return (
|
||||
f"<div style='text-align:center;'>"
|
||||
f"<div style='font-size:18px; color:{color}; font-weight:700;'>{icon}</div>"
|
||||
f"<div style='font-size:12px; color:{color}; font-weight:700;'>{title}</div>"
|
||||
f"</div>"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(seconds: float | None) -> str:
|
||||
if seconds is None or seconds <= 0:
|
||||
return "0m 00s"
|
||||
total = int(round(seconds))
|
||||
minutes, secs = divmod(total, 60)
|
||||
if minutes < 60:
|
||||
return f"{minutes}m {secs:02d}s"
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return f"{hours}h {minutes:02d}m"
|
||||
|
||||
@staticmethod
|
||||
def _format_eta(epoch_seconds: float | None) -> str:
|
||||
if epoch_seconds is None:
|
||||
return "N/A"
|
||||
return datetime.fromtimestamp(epoch_seconds).strftime("%H:%M:%S")
|
||||
|
||||
def _refresh_live_view(self) -> None:
|
||||
if self._progress is not None:
|
||||
self.set_progress(self._progress)
|
||||
|
||||
@Slot(bool)
|
||||
def set_running(self, running: bool) -> None:
|
||||
self._running = bool(running)
|
||||
if self._progress is not None:
|
||||
self.set_progress(self._progress)
|
||||
|
||||
@Slot(int)
|
||||
def set_samples_in_queue(self, count: int) -> None:
|
||||
self._queue_count = max(0, int(count))
|
||||
if self._progress is not None:
|
||||
self.set_progress(self._progress)
|
||||
|
||||
@Slot(object)
|
||||
def set_progress(self, progress: AutomationProgress) -> None:
|
||||
self._progress = progress
|
||||
|
||||
any_running = any(step.status == StepStatus.RUNNING for step in progress.steps)
|
||||
if any_running and self._running:
|
||||
if not self._timer.isActive():
|
||||
self._timer.start()
|
||||
else:
|
||||
self._timer.stop()
|
||||
|
||||
current_sample = progress.current_sample_name or "None"
|
||||
avg_time = progress.avg_time_per_sample if progress.avg_time_per_sample > 0 else self.DEFAULT_SAMPLE_ESTIMATE_S
|
||||
queue_remaining = avg_time * self._queue_count
|
||||
eta = time.time() + queue_remaining if queue_remaining > 0 else None
|
||||
|
||||
state_text = "Paused"
|
||||
state_color = "#c2410c"
|
||||
if progress.finished and progress.success is True:
|
||||
state_text = "Completed"
|
||||
state_color = "#15803d"
|
||||
elif progress.finished and progress.success is False:
|
||||
state_text = "Failed"
|
||||
state_color = "#b91c1c"
|
||||
elif self._running:
|
||||
state_text = "Running"
|
||||
state_color = "#2563eb"
|
||||
elif not self._running:
|
||||
state_text = "Paused"
|
||||
state_color = "#c2410c"
|
||||
|
||||
if self._summary_label is not None:
|
||||
self._summary_label.setText(
|
||||
f"<b>Status:</b> <span style='color:{state_color}; font-weight:700;'>{state_text}</span>"
|
||||
f" <b>Current:</b> {current_sample}"
|
||||
f" <b>Queue:</b> {self._queue_count}"
|
||||
f" <b>ETA:</b> {self._format_duration(queue_remaining)}"
|
||||
f" <b>Done by:</b> {self._format_eta(eta)}"
|
||||
)
|
||||
|
||||
states = {step.step: step.status for step in progress.steps}
|
||||
for step, label in self._step_labels.items():
|
||||
label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING)))
|
||||
Reference in new issue
Block a user