Skip to content

Commit

Permalink
Redesign status and results step (#978)
Browse files Browse the repository at this point in the history
This PR redesigns step 4 as follows:

- Split step into three "tabs":
  - Parameters summary and data download controls
  - Result panels
  - Process status views
    - "simplified" view (to be implemented in a later PR), and
    - "advanced" view (the current process tree)
- Smoothly handle transition from submission to results - the app now proceeds to step 4 while the builder is created and submitted. Step 4 is updated once the process is ready (Submit button doesn't get immediately disabled and it might take 2-3 seconds to go to next step #994)
  - Also resolves the issue where the submit button can be repeatedly pressed
- Discard redundant "Update results" button
- Disable raw-data/archive download buttons when downloading (Disable download button when clicked, until the download starts #977)
- Inject process pk to url on submission (Change URL when calc is submitted (and PK is assigned) #990)
  • Loading branch information
edan-bainglass authored Dec 19, 2024
1 parent 9e2f4b5 commit aad3410
Show file tree
Hide file tree
Showing 40 changed files with 605 additions and 400 deletions.
2 changes: 0 additions & 2 deletions qe.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import display\n",
"\n",
"from aiidalab_qe.app.wrapper import AppWrapperContoller, AppWrapperModel, AppWrapperView\n",
"\n",
"model = AppWrapperModel()\n",
Expand Down
18 changes: 8 additions & 10 deletions src/aiidalab_qe/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ def __init__(self, qe_auto_setup=True):
self._on_configuration_confirmation_change,
"confirmed",
)
self.submit_model.observe(
self._on_submission,
"confirmed",
ipw.dlink(
(self.submit_step, "state"),
(self.results_step, "previous_step_state"),
)
ipw.dlink(
(self.submit_model, "process_node"),
(self.results_model, "process_uuid"),
lambda node: node.uuid if node is not None else None,
)

# Add the application steps to the application
Expand Down Expand Up @@ -145,9 +150,6 @@ def _on_configuration_confirmation_change(self, _):
self._update_submission_step()
self._update_blockers()

def _on_submission(self, _):
self._update_results_step()

def _render_step(self, step_index):
step = self.steps[step_index][1]
step.render()
Expand All @@ -166,10 +168,6 @@ def _update_submission_step(self):
self.submit_model.input_structure = None
self.submit_model.input_parameters = {}

def _update_results_step(self):
node = self.submit_model.process_node
self.results_model.process_uuid = node.uuid if node is not None else None

def _update_blockers(self):
self.submit_model.external_submission_blockers = [
f"Unsaved changes in the <b>{title}</b> step. Please confirm the changes before submitting."
Expand Down
199 changes: 101 additions & 98 deletions src/aiidalab_qe/app/result/__init__.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
import ipywidgets as ipw
import traitlets as tl

from aiida import orm
from aiida.engine import ProcessState
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import LoadingWidget
from aiidalab_widgets_base import (
ProcessMonitor,
ProcessNodesTreeWidget,
WizardAppWidgetStep,
)
from aiidalab_widgets_base.viewers import viewer as node_viewer
from aiidalab_widgets_base import ProcessMonitor, WizardAppWidgetStep

from .components import ResultsComponent
from .components.status import WorkChainStatusModel, WorkChainStatusPanel
from .components.summary import WorkChainSummary, WorkChainSummaryModel
from .components.viewer import WorkChainResultsViewer, WorkChainResultsViewerModel
from .model import ResultsStepModel
from .viewer import WorkChainViewer, WorkChainViewerModel

PROCESS_COMPLETED = "<h4>Workflow completed successfully!</h4>"
PROCESS_EXCEPTED = "<h4>Workflow is excepted!</h4>"
PROCESS_RUNNING = "<h4>Workflow is running!</h4>"


class ViewQeAppWorkChainStatusAndResultsStep(ipw.VBox, WizardAppWidgetStep):
previous_step_state = tl.UseEnum(WizardAppWidgetStep.State)

def __init__(self, model: ResultsStepModel, **kwargs):
super().__init__(
children=[LoadingWidget("Loading results step")],
**kwargs,
)

self._model = model
self.observe(
self._on_previous_step_state_change,
"previous_step_state",
)
self._model.observe(
self._on_process_change,
"process_uuid",
)

self.rendered = False

self.node_views = {} # node-view cache

self.node_view_loading_message = LoadingWidget("Loading node view")

def render(self):
if self.rendered:
return
Expand All @@ -57,15 +56,6 @@ def render(self):
)
self.kill_button.on_click(self._on_kill_button_click)

self.update_results_button = ipw.Button(
description="Update results",
tooltip="Trigger the update of the results.",
button_style="success",
icon="refresh",
layout=ipw.Layout(width="auto", display="block"),
)
self.update_results_button.on_click(self._on_update_results_button_click)

self.clean_scratch_button = ipw.Button(
description="Clean remote data",
tooltip="Clean the remote folders of the workchain.",
Expand All @@ -85,48 +75,75 @@ def render(self):
(self.process_info, "value"),
)

self.process_tree = ProcessNodesTreeWidget()
self.process_tree.observe(
self._on_node_selection_change,
"selected_nodes",
summary_model = WorkChainSummaryModel()
self.summary_panel = WorkChainSummary(model=summary_model)
self._model.add_model("summary", summary_model)

results_model = WorkChainResultsViewerModel()
self.results_panel = WorkChainResultsViewer(model=results_model)
self._model.add_model("results", results_model)

status_model = WorkChainStatusModel()
self.status_panel = WorkChainStatusPanel(model=status_model)
self._model.add_model("status", status_model)

self.panels = {
"Summary": self.summary_panel,
"Results": self.results_panel,
"Status": self.status_panel,
}

self.toggle_controls = ipw.ToggleButtons(
options=[*self.panels.keys()],
tooltips=[
"A summary of calculation parameters",
"The calculation results",
"A detailed progress status of the workflow",
],
icons=[
"file-text-o",
"bar-chart",
"tasks",
],
value=None,
)
ipw.dlink(
(self._model, "process_uuid"),
(self.process_tree, "value"),
self.toggle_controls.add_class("results-step-toggles")
self.toggle_controls.observe(
self._on_toggle_change,
"value",
)

self.node_view_container = ipw.VBox()

self.process_monitor = ProcessMonitor(
timeout=0.2,
callbacks=[
self.process_tree.update,
self._update_status,
self._update_state,
self.container = ipw.VBox(
children=[
self.status_panel,
],
)

self.children = [
InAppGuide(identifier="results-step"),
self.process_info,
ipw.HBox(
children=[
self.kill_button,
self.update_results_button,
self.clean_scratch_button,
],
layout=ipw.Layout(margin="0 3px"),
),
self.process_tree,
self.node_view_container,
]
if self._model.has_process:
self._update_children()
elif self.previous_step_state is not WizardAppWidgetStep.State.SUCCESS:
self.children = [
ipw.HTML("""
<div class="alert alert-danger" style="text-align: center;">
No process detected. Please submit a calculation.
</div>
"""),
]

self.rendered = True

self._update_kill_button_layout()
self._update_clean_scratch_button_layout()

# This triggers the start of the monitor on a separate threadF
self.toggle_controls.value = "Summary"

self.process_monitor = ProcessMonitor(
timeout=0.2,
callbacks=[
self._update_status,
self._update_state,
],
)
ipw.dlink(
(self._model, "process_uuid"),
(self.process_monitor, "value"),
Expand All @@ -143,68 +160,54 @@ def reset(self):
def _on_state_change(self, _):
self._update_kill_button_layout()

def _on_previous_step_state_change(self, _):
if self.previous_step_state is WizardAppWidgetStep.State.SUCCESS:
process_node = self._model.fetch_process_node()
message = (
"Loading results"
if process_node and process_node.is_finished
else "Submitting calculation"
)
self.children = [LoadingWidget(message)]

def _on_toggle_change(self, change):
panel = self.panels[change["new"]]
self._toggle_view(panel)

def _on_process_change(self, _):
if self.rendered:
self._update_children()
self._model.update()
self._update_state()
self._update_kill_button_layout()
self._update_clean_scratch_button_layout()

def _on_node_selection_change(self, change):
self._update_node_view(change["new"])

def _on_kill_button_click(self, _):
self._model.kill_process()
self._update_kill_button_layout()

def _on_update_results_button_click(self, _):
self._update_node_view(self.process_tree.selected_nodes, refresh=True)

def _on_clean_scratch_button_click(self, _):
self._model.clean_remote_data()
self._update_clean_scratch_button_layout()

def _update_node_view(self, nodes, refresh=False):
"""Update the node view based on the selected nodes.
parameters
----------
`nodes`: `list`
List of selected nodes.
`refresh`: `bool`, optional
If True, the viewer will be refreshed.
Occurs when user presses the "Update results" button.
"""

if not nodes:
return
# only show the first selected node
node = nodes[0]

# check if the viewer is already added
if node.uuid in self.node_views and not refresh:
self.node_view = self.node_views[node.uuid]
elif not isinstance(node, orm.WorkChainNode):
self.node_view_container.children = [self.node_view_loading_message]
self.node_view = node_viewer(node)
self.node_views[node.uuid] = self.node_view
elif node.process_label == "QeAppWorkChain":
self.node_view_container.children = [self.node_view_loading_message]
self.node_view = self._create_workchain_viewer(node)
self.node_views[node.uuid] = self.node_view
else:
self.node_view = ipw.HTML("No viewer available for this node.")

self.node_view_container.children = [self.node_view]
def _update_children(self):
self.children = [
InAppGuide(identifier="results-step"),
self.process_info,
ipw.HBox(
children=[
self.kill_button,
self.clean_scratch_button,
],
layout=ipw.Layout(margin="0 3px"),
),
self.toggle_controls,
self.container,
]

def _create_workchain_viewer(self, node: orm.WorkChainNode):
model = WorkChainViewerModel()
ipw.dlink(
(self._model, "monitor_counter"),
(model, "monitor_counter"),
)
node_view: WorkChainViewer = node_viewer(node, model=model) # type: ignore
node_view.render()
return node_view
def _toggle_view(self, panel: ResultsComponent):
self.container.children = [panel]
panel.render()

def _update_kill_button_layout(self):
if not self.rendered:
Expand Down
55 changes: 55 additions & 0 deletions src/aiidalab_qe/app/result/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import typing as t

import ipywidgets as ipw

from aiidalab_qe.common.mixins import HasProcess
from aiidalab_qe.common.mvc import Model
from aiidalab_qe.common.widgets import LoadingWidget


class ResultsComponentModel(Model, HasProcess):
identifier = "results"


RCM = t.TypeVar("RCM", bound=ResultsComponentModel)


class ResultsComponent(ipw.VBox, t.Generic[RCM]):
def __init__(self, model: RCM, **kwargs):
self.loading_message = LoadingWidget(f"Loading {model.identifier}")

super().__init__(
children=[self.loading_message],
**kwargs,
)

self._model = model
self._model.observe(
self._on_process_change,
"process_uuid",
)
self._model.observe(
self._on_monitor_counter_change,
"monitor_counter",
)

self.rendered = False

def render(self):
if self.rendered:
return
self._render()
self.rendered = True
self._post_render()

def _on_process_change(self, _):
pass

def _on_monitor_counter_change(self, _):
pass

def _render(self):
raise NotImplementedError

def _post_render(self):
pass
7 changes: 7 additions & 0 deletions src/aiidalab_qe/app/result/components/status/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .model import WorkChainStatusModel
from .status import WorkChainStatusPanel

__all__ = [
"WorkChainStatusModel",
"WorkChainStatusPanel",
]
5 changes: 5 additions & 0 deletions src/aiidalab_qe/app/result/components/status/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from aiidalab_qe.app.result.components import ResultsComponentModel


class WorkChainStatusModel(ResultsComponentModel):
identifier = "workflow process status monitors"
Loading

0 comments on commit aad3410

Please sign in to comment.