11"""Run History tab: browse past scheduler / trigger / hotkey fires."""
22import datetime as _dt
33from pathlib import Path
4- from typing import Optional
4+ from typing import List , Optional
55
66from PySide6 .QtCore import QTimer , Qt , QUrl
7- from PySide6 .QtGui import QDesktopServices
7+ from PySide6 .QtGui import QDesktopServices , QPixmap
88from PySide6 .QtWidgets import (
9- QAbstractItemView , QComboBox , QHBoxLayout , QHeaderView , QLabel ,
10- QMessageBox , QPushButton , QTableWidget , QTableWidgetItem ,
9+ QAbstractItemView , QComboBox , QFrame , QHBoxLayout , QHeaderView , QLabel ,
10+ QMessageBox , QPushButton , QSplitter , QTableWidget , QTableWidgetItem ,
1111 QVBoxLayout , QWidget ,
1212)
1313
1414from je_auto_control .gui ._i18n_helpers import TranslatableMixin
1515from je_auto_control .gui .language_wrapper .multi_language_wrapper import (
1616 language_wrapper ,
1717)
18+ from je_auto_control .gui .run_history_timeline import RunHistoryTimeline
1819from je_auto_control .utils .run_history .history_store import (
1920 SOURCE_HOTKEY , SOURCE_MANUAL , SOURCE_REST , SOURCE_SCHEDULER ,
20- SOURCE_TRIGGER , STATUS_ERROR , STATUS_OK , STATUS_RUNNING ,
21+ SOURCE_TRIGGER , STATUS_ERROR , STATUS_OK , STATUS_RUNNING , RunRecord ,
2122 default_history_store ,
2223)
2324
@@ -63,6 +64,7 @@ class RunHistoryTab(TranslatableMixin, QWidget):
6364 def __init__ (self , parent : Optional [QWidget ] = None ) -> None :
6465 super ().__init__ (parent )
6566 self ._tr_init ()
67+ self ._records : List [RunRecord ] = []
6668 self ._filter = QComboBox ()
6769 self ._populate_filter ()
6870 self ._filter .currentIndexChanged .connect (self ._refresh )
@@ -75,6 +77,17 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
7577 header .setSectionResizeMode (QHeaderView .Interactive )
7678 header .setStretchLastSection (True )
7779 self ._count_label = QLabel ()
80+ self ._timeline = RunHistoryTimeline ()
81+ self ._timeline .run_clicked .connect (self ._on_timeline_clicked )
82+ self ._thumb_label = QLabel ()
83+ self ._thumb_label .setAlignment (Qt .AlignCenter )
84+ self ._thumb_label .setMinimumSize (220 , 160 )
85+ self ._thumb_label .setFrameShape (QFrame .StyledPanel )
86+ self ._thumb_label .setScaledContents (False )
87+ self ._thumb_caption = QLabel ()
88+ self ._thumb_caption .setWordWrap (True )
89+ self ._thumb_caption .setAlignment (Qt .AlignTop )
90+ self ._thumb_caption .setTextInteractionFlags (Qt .TextSelectableByMouse )
7891 self ._timer = QTimer (self )
7992 self ._timer .setInterval (_REFRESH_INTERVAL_MS )
8093 self ._timer .timeout .connect (self ._refresh )
@@ -89,6 +102,10 @@ def retranslate(self) -> None:
89102 self ._repopulate_filter_labels ()
90103 self ._refresh ()
91104
105+ def resizeEvent (self , event ) -> None :
106+ super ().resizeEvent (event )
107+ self ._refresh_preview ()
108+
92109 def _populate_filter (self ) -> None :
93110 self ._filter .blockSignals (True )
94111 for label_key , source_value in _SOURCES :
@@ -124,8 +141,27 @@ def _build_layout(self) -> None:
124141 clear_btn .clicked .connect (self ._on_clear )
125142 top .addWidget (clear_btn )
126143 root .addLayout (top )
127- root .addWidget (self ._table , stretch = 1 )
144+
145+ self ._tr (QLabel (), "rh_timeline_heading" )
146+ timeline_label = self ._tr (QLabel (), "rh_timeline_heading" )
147+ root .addWidget (timeline_label )
148+ root .addWidget (self ._timeline )
149+
150+ splitter = QSplitter (Qt .Horizontal )
151+ splitter .addWidget (self ._table )
152+ preview = QWidget ()
153+ preview_layout = QVBoxLayout (preview )
154+ preview_layout .setContentsMargins (0 , 0 , 0 , 0 )
155+ preview_layout .addWidget (self ._tr (QLabel (), "rh_preview_heading" ))
156+ preview_layout .addWidget (self ._thumb_label , stretch = 1 )
157+ preview_layout .addWidget (self ._thumb_caption )
158+ splitter .addWidget (preview )
159+ splitter .setStretchFactor (0 , 3 )
160+ splitter .setStretchFactor (1 , 1 )
161+ root .addWidget (splitter , stretch = 1 )
162+
128163 self ._table .cellDoubleClicked .connect (self ._on_cell_double_clicked )
164+ self ._table .itemSelectionChanged .connect (self ._on_selection_changed )
129165 open_row = QHBoxLayout ()
130166 self ._open_artifact_btn = self ._tr (QPushButton (), "rh_open_artifact" )
131167 self ._open_artifact_btn .clicked .connect (self ._open_selected_artifact )
@@ -149,12 +185,15 @@ def _refresh(self) -> None:
149185 runs = default_history_store .list_runs (limit = 500 , source_type = source )
150186 except ValueError :
151187 runs = []
188+ self ._records = runs
152189 self ._table .setRowCount (len (runs ))
153190 for row , record in enumerate (runs ):
154191 self ._set_row (row , record )
155192 self ._count_label .setText (
156193 _t ("rh_count_label" ).replace ("{n}" , str (len (runs ))),
157194 )
195+ self ._timeline .set_records (runs )
196+ self ._refresh_preview ()
158197
159198 def _set_row (self , row : int , record ) -> None :
160199 status_key = _STATUS_LABEL_KEYS .get (record .status , record .status )
@@ -176,6 +215,54 @@ def _set_row(self, row: int, record) -> None:
176215 item .setFlags (item .flags () & ~ Qt .ItemIsEditable )
177216 self ._table .setItem (row , col , item )
178217
218+ def _selected_record (self ) -> Optional [RunRecord ]:
219+ row = self ._table .currentRow ()
220+ if row < 0 or row >= len (self ._records ):
221+ return None
222+ return self ._records [row ]
223+
224+ def _on_selection_changed (self ) -> None :
225+ record = self ._selected_record ()
226+ self ._timeline .set_highlighted (record .id if record is not None else None )
227+ self ._refresh_preview ()
228+
229+ def _on_timeline_clicked (self , run_id : int ) -> None :
230+ for row , record in enumerate (self ._records ):
231+ if record .id == run_id :
232+ self ._table .selectRow (row )
233+ return
234+
235+ def _refresh_preview (self ) -> None :
236+ record = self ._selected_record ()
237+ if record is None :
238+ self ._thumb_label .clear ()
239+ self ._thumb_label .setText (_t ("rh_preview_empty" ))
240+ self ._thumb_caption .setText ("" )
241+ return
242+ path = record .artifact_path
243+ if not path :
244+ self ._thumb_label .clear ()
245+ self ._thumb_label .setText (_t ("rh_preview_no_artifact" ))
246+ else :
247+ pixmap = QPixmap (path )
248+ if pixmap .isNull ():
249+ self ._thumb_label .clear ()
250+ self ._thumb_label .setText (_t ("rh_artifact_missing" ))
251+ else :
252+ self ._thumb_label .setPixmap (pixmap .scaled (
253+ self ._thumb_label .size (), Qt .KeepAspectRatio ,
254+ Qt .SmoothTransformation ,
255+ ))
256+ caption = (
257+ f"#{ record .id } • { record .source_type } /{ record .source_id } \n "
258+ f"{ record .script_path } \n "
259+ f"{ _format_time (record .started_at )} • "
260+ f"{ _format_duration (record .duration_seconds )} "
261+ )
262+ if record .error_text :
263+ caption += f"\n { record .error_text } "
264+ self ._thumb_caption .setText (caption )
265+
179266 def _selected_artifact_path (self ) -> Optional [str ]:
180267 row = self ._table .currentRow ()
181268 if row < 0 :
0 commit comments