Skip to content

Commit

Permalink
in-progress
Browse files Browse the repository at this point in the history
  • Loading branch information
edan-bainglass committed Nov 20, 2024
1 parent 97915df commit a5b7a3b
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 11 deletions.
19 changes: 19 additions & 0 deletions src/aiidalab_qe/app/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.utils import get_entry_items
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.panel import SettingsModel, SettingsPanel
from aiidalab_widgets_base import WizardAppWidgetStep

Expand Down Expand Up @@ -77,6 +78,23 @@ def render(self):
if self.rendered:
return

self.generic_guide = InAppGuide(
children=[
ipw.HTML("""
<div>
In this step, you can define the workflow tasks, such as the
level of structure optimization and/or the desired properties
to compute (in step 2.1). You can also customize the calculation
parameters (in step 2.2).
When you are ready, click "Confirm" to proceed to the next step.
<br>
<strong>Note:</strong> Changes after confirmation will unconfirm
the step and reset the following steps.
</div>
""")
],
)

# RelaxType: degrees of freedom in geometry optimization
self.relax_type_help = ipw.HTML()
ipw.dlink(
Expand Down Expand Up @@ -133,6 +151,7 @@ def render(self):
self.confirm_button.on_click(self.confirm)

self.children = [
self.generic_guide,
self.structure_set_message,
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 0px">
Expand Down
17 changes: 17 additions & 0 deletions src/aiidalab_qe/app/configuration/basic/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ipywidgets as ipw

from aiidalab_qe.app.configuration.basic.model import WorkChainModel
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.panel import SettingsPanel


Expand All @@ -24,6 +25,21 @@ def render(self):
if self.rendered:
return

self.generic_guide = InAppGuide(
guide_class="basic",
children=[
ipw.HTML("""
<div>
In the basic settings panel, you can define the type of material
(metal or insulator), and specify if spin polarization is to be
considered. You can also select the protocol to be used in the
calculation, which will modify a set of parameters controlling
the accuracy/speed trade-off.
</div>
""")
],
)

# SpinType: magnetic properties of material
self.spin_type = ipw.ToggleButtons(style={"description_width": "initial"})
ipw.dlink(
Expand Down Expand Up @@ -58,6 +74,7 @@ def render(self):
)

self.children = [
self.generic_guide,
ipw.HTML("""
<div style="line-height: 140%; padding-top: 10px; padding-bottom: 10px">
Below you can indicate both if the material should be treated as an
Expand Down
14 changes: 14 additions & 0 deletions src/aiidalab_qe/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from aiidalab_qe.app.structure.model import StructureStepModel
from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep
from aiidalab_qe.app.submission.model import SubmissionStepModel
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.widgets import LoadingWidget
from aiidalab_widgets_base import WizardAppWidget

Expand Down Expand Up @@ -107,8 +108,21 @@ def __init__(self, qe_auto_setup=True):
layout=ipw.Layout(display="none"),
)

self.generic_guide = InAppGuide(
children=[
ipw.HTML("""
<div>
You have activated an in-app guide. In the following sections,
you will see a step-by-step guide dedicated to running a Quantum
ESPRESSO calculation specific to the chosen guide.
</div>
""")
],
)

super().__init__(
children=[
self.generic_guide,
self.new_workchain_button,
self._process_loading_message,
self._wizard_app_widget,
Expand Down
3 changes: 3 additions & 0 deletions src/aiidalab_qe/app/static/styles/infobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
.info-box p {
line-height: 24px;
}
.info-box.in-app-guide.show {
display: flex !important;
}
3 changes: 3 additions & 0 deletions src/aiidalab_qe/app/static/templates/guide.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@
For a more in-depth dive into the app's features, please refer to the
<a href="https://aiidalab-qe.readthedocs.io/howto/index.html" target="_blank">how-to guides</a>.
</p>

<p>
Alternatively, you can select one of our in-app guides below to walk through an example use-case.
</p>
</div>
43 changes: 43 additions & 0 deletions src/aiidalab_qe/app/structure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
LazyLoadedOptimade,
LazyLoadedStructureBrowser,
)
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_widgets_base import (
BasicCellEditor,
BasicStructureEditor,
Expand Down Expand Up @@ -72,6 +73,47 @@ def render(self):
if self.rendered:
return

self.generic_guide = InAppGuide(
children=[
ipw.HTML("""
<div>
In this step, you can select a structure as follows:
<ul>
<li>
<strong>Upload file</strong>:
upload a structure file from your computer.
</li>
<li>
<strong>OPTIMADE</strong>:
search for structures in the OPTIMADE database.
</li>
<li>
<strong>AiiDA database</strong>:
search for structures in your AiiDA database.
</li>
<li>
<strong>From Examples</strong>:
select a structure from a list of example structures.
</li>
</ul>
Once selected, you may inspect the structure. You can also edit
the structure using the available structure editors. When done,
you can choose to modify the structure label and/or provide a
description. These will be attached to the input structure node
in your AiiDA database. When you are ready, click "Confirm" to
proceed to the next step.
<br>
<strong>Note:</strong> If the confirmed structure is not yet
stored in the AiiDA database, it will be stored automatically
when you proceed to the next step.
<br>
<strong>Note:</strong> Changes after confirmation will unconfirm
the step and reset the following steps.
</div>
""")
],
)

importers = [
StructureUploadWidget(title="Upload file"),
LazyLoadedOptimade(title="OPTIMADE"),
Expand Down Expand Up @@ -151,6 +193,7 @@ def render(self):
)

self.children = [
self.generic_guide,
ipw.HTML("""
<p>
Select a structure from one of the following sources and then
Expand Down
25 changes: 21 additions & 4 deletions src/aiidalab_qe/app/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import ipywidgets as ipw
import traitlets
import traitlets as tl
from IPython.display import display

from aiidalab_qe.common.widgets import LoadingWidget

Expand Down Expand Up @@ -56,7 +57,10 @@ def enable_toggles(self) -> None:
def _on_guide_toggle(self, change: dict):
"""Toggle the guide section."""
if change["new"]:
self._view.info_container.children = [self._view.guide]
self._view.info_container.children = [
self._view.guide,
self._view.guide_selection,
]
self._view.info_container.layout.display = "flex"
self._view.job_history_toggle.value = False
else:
Expand Down Expand Up @@ -89,14 +93,21 @@ def _on_job_history_toggle(self, change: dict):
else:
self._view.main.children = self._old_view

def _on_guide_select(self, change: dict):
"""Sets the current active guide."""
from aiidalab_qe.common.infobox import guide_manager

guide_manager.active_guide = change["new"]

def _set_event_handlers(self) -> None:
"""Set up event handlers."""
self._view.guide_toggle.observe(self._on_guide_toggle, "value")
self._view.about_toggle.observe(self._on_about_toggle, "value")
self._view.job_history_toggle.observe(self._on_job_history_toggle, "value")
self._view.guide_selection.observe(self._on_guide_select, "value")


class AppWrapperModel(traitlets.HasTraits):
class AppWrapperModel(tl.HasTraits):
"""An MVC model for `AppWrapper`."""

def __init__(self):
Expand All @@ -114,7 +125,7 @@ def __init__(self) -> None:
from datetime import datetime

from importlib_resources import files
from IPython.display import Image, display
from IPython.display import Image
from jinja2 import Environment

from aiidalab_qe.app.static import templates
Expand Down Expand Up @@ -184,6 +195,12 @@ def __init__(self) -> None:
self.guide = ipw.HTML(env.from_string(guide_template).render())
self.about = ipw.HTML(env.from_string(about_template).render())

self.guide_selection = ipw.RadioButtons(
options=["none", "basic", "advanced"],
description="Guides:",
value="none",
)

self.job_history = QueryInterface()

self.info_container = InfoBox()
Expand Down
60 changes: 60 additions & 0 deletions src/aiidalab_qe/common/infobox.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import ipywidgets as ipw
import traitlets as tl


class InfoBox(ipw.VBox):
Expand All @@ -20,3 +21,62 @@ def __init__(self, classes: list[str] | None = None, **kwargs):
for custom_class in custom_classes.split(" "):
if custom_class:
self.add_class(custom_class)


class GuideManager(tl.HasTraits):
active_guide = tl.Unicode("none")


guide_manager = GuideManager()


class InAppGuide(InfoBox):
"""The `InfoAppGuide` is used to set up in-app guides that may be toggle in unison."""

def __init__(
self,
guide_class: str = "qe-app",
classes: list[str] | None = None,
**kwargs,
):
"""`InAppGuide` constructor.
Parameters
----------
`guide_class` : `str`, optional
The identifier used to toggle the guide.
The default `qe-app` identifies built-in guide sections.
`classes` : `list[str]`, optional
One or more CSS classes.
"""

self.guide_class = guide_class

super().__init__(
classes=[
"in-app-guide",
*(classes or []),
],
**kwargs,
)

guide_manager.observe(
self._on_active_guide_change,
"active_guide",
)

# This manual toggle call is necessary because the guide
# may be contained in a component that was not yet rendered
# when a guide was selected.
self._toggle_guide()

def _on_active_guide_change(self, _):
self._toggle_guide()

def _toggle_guide(self):
active_guide = guide_manager.active_guide
not_generic = self.guide_class != "qe-app"
if active_guide == "none" or (not_generic and active_guide != self.guide_class):
self.layout.display = "none"
else:
self.layout.display = "flex"
16 changes: 15 additions & 1 deletion tests/test_infobox.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from aiidalab_qe.common.infobox import InfoBox
from aiidalab_qe.common.infobox import InAppGuide, InfoBox


def test_infobox_classes():
Expand All @@ -14,3 +14,17 @@ def test_infobox_classes():
"custom-3",
)
)


def test_in_app_guide():
"""Test `InAppGuide` class."""
guide_class = "some_guide"
in_app_guide = InAppGuide(guide_class=guide_class)
assert all(
css_class in in_app_guide._dom_classes
for css_class in (
"info-box",
"in-app-guide",
f"{guide_class}-guide-identifier",
)
)
12 changes: 6 additions & 6 deletions tests/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_guide_toggle(self):
self.controller._on_guide_toggle({"new": True})
self._assert_guide_is_on()
self.controller._on_guide_toggle({"new": False})
self._assert_no_guide_info()
self._assert_no_info()

def test_about_toggle(self):
"""Test about_toggle method."""
Expand All @@ -27,33 +27,33 @@ def test_about_toggle(self):
self.controller._on_about_toggle({"new": True})
self._assert_about_is_on()
self.controller._on_about_toggle({"new": False})
self._assert_no_guide_info()
self._assert_no_info()

def test_toggle_switch(self):
"""Test toggle_switch method."""
self._instansiate_mvc_components()
self.controller.enable_toggles()
self._assert_no_guide_info()
self._assert_no_info()
self.controller._on_guide_toggle({"new": True})
self._assert_guide_is_on()
self.controller._on_about_toggle({"new": True})
self._assert_about_is_on()
self.controller._on_guide_toggle({"new": True})
self._assert_guide_is_on()
self.controller._on_guide_toggle({"new": False})
self._assert_no_guide_info()
self._assert_no_info()

def _assert_guide_is_on(self):
"""Assert guide is on."""
assert len(self.view.info_container.children) == 1
assert len(self.view.info_container.children) == 2
assert self.view.guide in self.view.info_container.children

def _assert_about_is_on(self):
"""Assert about is on."""
assert len(self.view.info_container.children) == 1
assert self.view.about in self.view.info_container.children

def _assert_no_guide_info(self):
def _assert_no_info(self):
"""Assert no info is shown."""
assert len(self.view.info_container.children) == 0

Expand Down

0 comments on commit a5b7a3b

Please sign in to comment.