Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _add_create_new_ensemble_tab(self) -> None:
self._storage_info_widget.setRealization
)

self.addTab(panel, "Create new experiment")
self.addTab(panel, "Experiments Overview")

def _add_initialize_from_scratch_tab(self) -> None:
panel = QWidget()
Expand Down
11 changes: 11 additions & 0 deletions src/ert/gui/tools/manage_experiments/storage_info_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ def __init__(self) -> None:
info_frame = QFrame()
self._name_label = QLabel()
self._uuid_label = QLabel()
self._created_at_label = QLabel()

layout = QVBoxLayout()
layout.addWidget(self._name_label)
layout.addWidget(self._uuid_label)
layout.addWidget(self._created_at_label)
layout.addStretch()

info_frame.setLayout(layout)
Expand Down Expand Up @@ -113,6 +115,15 @@ def setExperiment(self, experiment: Experiment) -> None:

self._name_label.setText(f"Name: {experiment.name!s}")
self._uuid_label.setText(f"UUID: {experiment.id!s}")
# Determine creation time from the earliest ensemble start time
created_text = "Unknown"
try:
ensemble_start_times = [ens.started_at for ens in experiment.ensembles]
if ensemble_start_times:
created_text = min(ensemble_start_times).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
created_text = "Unknown"
Comment on lines +120 to +125
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setExperiment() wraps the created-at computation in a broad except Exception, which can silently hide real bugs (e.g., storage access problems) and make UI issues hard to diagnose. Since started_at appears to be a datetime, this can be handled without a blanket exception by filtering/handling empty iterables explicitly (e.g., compute min with a default, or guard on an empty list) and only catching the specific exceptions you expect.

Suggested change
try:
ensemble_start_times = [ens.started_at for ens in experiment.ensembles]
if ensemble_start_times:
created_text = min(ensemble_start_times).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
created_text = "Unknown"
ensemble_start_times = [
ens.started_at
for ens in experiment.ensembles
if ens.started_at is not None
]
if ensemble_start_times:
created_text = min(ensemble_start_times).strftime("%Y-%m-%d %H:%M:%S")

Copilot uses AI. Check for mistakes.
self._created_at_label.setText(f"Created at: {created_text}")

self._responses_text_edit.setText(yaml.dump(experiment.response_info, indent=4))
self._parameters_text_edit.setText(
Expand Down
12 changes: 11 additions & 1 deletion src/ert/gui/tools/manage_experiments/storage_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class _Column(IntEnum):
_NUM_COLUMNS = max(_Column).value + 1
_COLUMN_TEXT = {
0: "Name",
1: "Created at",
1: "Created",
}


Expand Down Expand Up @@ -83,6 +83,11 @@ def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any:
elif role == Qt.ItemDataRole.ToolTipRole:
if col == _Column.TIME:
return str(self._start_time)
elif role == Qt.ItemDataRole.UserRole:
if col == _Column.TIME:
return self._start_time
if col == _Column.NAME:
return self._name

return None

Expand Down Expand Up @@ -118,6 +123,11 @@ def data(
if self._children
else "None"
)
elif role == Qt.ItemDataRole.UserRole:
if col == _Column.NAME:
return self._name
if col == _Column.TIME:
return self._children[0]._start_time if self._children else None
Comment on lines +126 to +130
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExperimentModel uses self._children[0]._start_time as the value for the "Created" column when sorting (UserRole). Since LocalStorage._load_ensembles() inserts ensembles sorted by started_at descending, index 0 corresponds to the newest ensemble, not the experiment’s creation time. This makes the "Created" column (and sorting by it) misleading and inconsistent with the experiment details view which uses the earliest ensemble time. Consider computing the earliest (or otherwise explicitly defined) timestamp across all child ensembles for both DisplayRole and UserRole so the semantics match the column header.

Copilot uses AI. Check for mistakes.

return None

Expand Down
46 changes: 29 additions & 17 deletions src/ert/gui/tools/manage_experiments/storage_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
QAbstractItemModel,
QItemSelectionModel,
QModelIndex,
QSize,
QSortFilterProxyModel,
Qt,
)
Expand All @@ -15,7 +14,7 @@
from PyQt6.QtWidgets import (
QHBoxLayout,
QLineEdit,
QToolButton,
QPushButton,
QTreeView,
QVBoxLayout,
QWidget,
Expand All @@ -24,7 +23,6 @@
from ert.config import ErrorInfo, ErtConfig
from ert.gui.ertnotifier import ErtNotifier
from ert.gui.ertwidgets import CreateExperimentDialog, Suggestor
from ert.gui.icon_utils import load_icon
from ert.storage import Ensemble, Experiment
from ert.storage.local_experiment import ExperimentType

Expand All @@ -49,9 +47,7 @@ class AddWidget(QWidget):
def __init__(self, addFunction: Callable[[], None]) -> None:
super().__init__()

self.addButton = QToolButton(self)
self.addButton.setIcon(load_icon("add_circle_outlined.svg"))
self.addButton.setIconSize(QSize(16, 16))
self.addButton = QPushButton("Add new experiment", self)
self.addButton.clicked.connect(addFunction)

self.removeButton = None
Expand All @@ -71,21 +67,30 @@ def __init__(self, model: QAbstractItemModel) -> None:
self.setSourceModel(model)

def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
left_data = left.data()
right_data = right.data()

left_display = left.data(Qt.ItemDataRole.DisplayRole)
right_display = right.data(Qt.ItemDataRole.DisplayRole)
if (
isinstance(left_data, str)
and "Realization" in left_data
and isinstance(right_data, str)
and "Realization" in right_data
isinstance(left_display, str)
and left_display.startswith("Realization ")
and isinstance(right_display, str)
and right_display.startswith("Realization ")
):
left_realization_number = int(left_data.split(" ")[1])
right_realization_number = int(right_data.split(" ")[1])
left_num = int(left_display.split(" ")[1])
right_num = int(right_display.split(" ")[1])
return left_num < right_num

role = int(self.sortRole())
left_data = left.data(role)
right_data = right.data(role)

return left_realization_number < right_realization_number
if left_data is None and right_data is None:
return False
if left_data is None:
return False
if right_data is None:
return True

return super().lessThan(left, right)
return left_data < right_data


class StorageWidget(QWidget):
Expand Down Expand Up @@ -114,9 +119,16 @@ def __init__(
proxy_model = _SortingProxyModel(storage_model)
proxy_model.setFilterKeyColumn(-1) # Search all columns.
proxy_model.setSourceModel(storage_model)
proxy_model.setSortRole(Qt.ItemDataRole.UserRole)
proxy_model.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
proxy_model.setDynamicSortFilter(True)
proxy_model.sort(0, Qt.SortOrder.AscendingOrder)

self._tree_view.setModel(proxy_model)
self._tree_view.setSortingEnabled(True)
header = self._tree_view.header()
if header is not None:
header.setSortIndicatorShown(True)
search_bar.textChanged.connect(proxy_model.setFilterFixedString)

self._sel_model = QItemSelectionModel(proxy_model)
Expand Down
16 changes: 12 additions & 4 deletions tests/ert/ui_tests/gui/test_manage_experiments_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +441,18 @@ def _evaluate(coeffs, x):
# select the ensemble
storage_widget = tool.findChild(StorageWidget)
storage_widget._tree_view.expandAll()
model_index = storage_widget._tree_view.model().index(
0, 0, storage_widget._tree_view.model().index(0, 0)
)
storage_widget._tree_view.setCurrentIndex(model_index)

model = storage_widget._tree_view.model()
experiment_index = model.index(0, 0)
# Search all child rows under the first experiment for the name "iter-0"
target_index = None
for r in range(model.rowCount(experiment_index)):
idx = model.index(r, 0, experiment_index)
if model.data(idx, Qt.ItemDataRole.DisplayRole) == "iter-0":
target_index = idx
break
assert target_index is not None
storage_widget._tree_view.setCurrentIndex(target_index)
assert (
tool._storage_info_widget._content_layout.currentIndex()
== _WidgetType.ENSEMBLE_WIDGET
Expand Down
Loading