From 026d47fe0b8707492349bc321be584f61459fa7e Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Mon, 9 Dec 2024 07:24:03 +0000 Subject: [PATCH 1/6] Implement in-app guide mechanism --- src/aiidalab_qe/app/configuration/__init__.py | 56 ++++++++++++++++- .../app/configuration/advanced/advanced.py | 19 ++++-- .../app/configuration/basic/workflow.py | 22 +++++++ src/aiidalab_qe/app/main.py | 12 ++++ src/aiidalab_qe/app/static/styles/custom.css | 7 --- src/aiidalab_qe/app/static/styles/infobox.css | 39 +++++++++--- .../app/static/templates/guide.jinja | 3 + src/aiidalab_qe/app/structure/__init__.py | 52 ++++++++++++++++ src/aiidalab_qe/app/submission/__init__.py | 20 +++++++ src/aiidalab_qe/app/wrapper.py | 25 ++++++-- src/aiidalab_qe/common/infobox.py | 60 +++++++++++++++++++ src/aiidalab_qe/plugins/bands/setting.py | 14 +++++ src/aiidalab_qe/plugins/pdos/setting.py | 14 +++++ tests/test_infobox.py | 16 ++++- tests/test_wrapper.py | 12 ++-- 15 files changed, 340 insertions(+), 31 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/__init__.py b/src/aiidalab_qe/app/configuration/__init__.py index b9775ea0b..2adb18c3c 100644 --- a/src/aiidalab_qe/app/configuration/__init__.py +++ b/src/aiidalab_qe/app/configuration/__init__.py @@ -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 ( ConfigurationSettingsModel, ConfigurationSettingsPanel, @@ -114,10 +115,39 @@ def render(self): children=[ ipw.VBox( children=[ + InAppGuide( + children=[ + ipw.HTML(""" +
+ Here we select the properties to calculate. +
+ Select Electronic band structure and + Projected density of states (PDOS) +
+
+ """) + ], + ), *self.property_children, ] ), - self.tabs, + ipw.VBox( + children=[ + InAppGuide( + children=[ + ipw.HTML(""" +
+ Here we can customize the calculation parameters. +
+ Click on each tab to customize its settings. +
+
+ """) + ], + ), + self.tabs, + ], + ), ], layout=ipw.Layout(margin="10px 2px"), selected_index=None, @@ -141,6 +171,30 @@ def render(self): self.confirm_button.on_click(self.confirm) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +
+ In this step, we define the workflow tasks, including structure + relaxation and which properties to compute, and select the + parameters of the calculations. +
+

Tasks

+
    +
  1. Select Full geometry relaxation
  2. +
  3. Open Step 2.1 to select properties
  4. +
  5. Open Step 2.2 to customize parameters
  6. +
  7. Click Confirm to proceed
  8. +
+
+
+ Note: Changes after confirmation will unconfirm this + step and reset the following steps. +
+
+ """) + ], + ), self.structure_set_message, ipw.HTML("""
diff --git a/src/aiidalab_qe/app/configuration/advanced/advanced.py b/src/aiidalab_qe/app/configuration/advanced/advanced.py index 5779d707d..e2b7e8a96 100644 --- a/src/aiidalab_qe/app/configuration/advanced/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced/advanced.py @@ -5,6 +5,7 @@ import ipywidgets as ipw +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.panel import ConfigurationSettingsPanel from .hubbard import ( @@ -274,11 +275,19 @@ def render(self): self.pseudos.render() self.children = [ - ipw.HTML(""" -
-

Advanced Settings

-
- """), + InAppGuide( + children=[ + ipw.HTML(""" +
+ The Advanced settings allow you to finely tune the calculation. +
+ In this walkthrough, we will not modify advanced settings + and proceed with the defaults. +
+
+ """) + ], + ), ipw.HBox( children=[ self.clean_workdir, diff --git a/src/aiidalab_qe/app/configuration/basic/workflow.py b/src/aiidalab_qe/app/configuration/basic/workflow.py index 4e4147b25..75fdce849 100644 --- a/src/aiidalab_qe/app/configuration/basic/workflow.py +++ b/src/aiidalab_qe/app/configuration/basic/workflow.py @@ -6,6 +6,7 @@ import ipywidgets as ipw from aiidalab_qe.app.configuration.basic.model import BasicConfigurationSettingsModel +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.panel import ConfigurationSettingsPanel @@ -60,6 +61,27 @@ def render(self): ) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +
+ The basic settings panel provides top-level calculation + settings including the electronic and magnetic properties of + the material. It also provides a choice of three protocols + that pre-configure many calculation settings, balancing + speed with accuracy. +
+ Select the fast protocol +
+
+ Note: Due to the limited resources provided for the + demo server, we select the fast protocol to + reduce the cost of the calculation. +
+
+ """) + ], + ), ipw.HTML("""
Below you can indicate both if the material should be treated as an diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index 4dbd02f9e..2cc5fc12c 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -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 @@ -109,6 +110,17 @@ def __init__(self, qe_auto_setup=True): super().__init__( children=[ + InAppGuide( + children=[ + ipw.HTML(""" +
+ You've activated an in-app guide. Follow along below to learn + how to use the Quantum ESPRESSO app. +
+ """) + ], + classes=["guide-warning"], + ), self.new_workchain_button, self._process_loading_message, self._wizard_app_widget, diff --git a/src/aiidalab_qe/app/static/styles/custom.css b/src/aiidalab_qe/app/static/styles/custom.css index 053cf8b92..7cd2171b9 100644 --- a/src/aiidalab_qe/app/static/styles/custom.css +++ b/src/aiidalab_qe/app/static/styles/custom.css @@ -31,13 +31,6 @@ outline: none !important; } -.guide ol { - list-style: none; -} -.guide p:not(:last-of-type) { - margin-bottom: 0.5em; -} - .loading { margin: 0 auto; padding: 5px; diff --git a/src/aiidalab_qe/app/static/styles/infobox.css b/src/aiidalab_qe/app/static/styles/infobox.css index a25861e42..7d7b3ccc9 100644 --- a/src/aiidalab_qe/app/static/styles/infobox.css +++ b/src/aiidalab_qe/app/static/styles/infobox.css @@ -2,14 +2,39 @@ display: none; margin: 2px; padding: 1em; - border: 3px solid orangered; - background-color: #ffedcc; - border-radius: 1em; - -webkit-border-radius: 1em; - -moz-border-radius: 1em; - -ms-border-radius: 1em; - -o-border-radius: 1em; + border-left: 3px solid #add8e6; + background-color: #d9edf7; } .info-box p { line-height: 24px; } +.info-box.in-app-guide.show { + display: flex !important; +} +.guide ol { + list-style: none; +} +.guide p:not(:last-of-type) { + margin-bottom: 0.5em; +} +.guide-warning { + background-color: #fcf8e3; + border-color: #ffe4c4; + margin-bottom: 1em; +} +.in-app-guide .alert { + color: black; + margin: 10px 0; + padding: 10px 14px; + border-width: 0; + border-left-width: 3px; +} +.in-app-guide .alert.alert-warning { + border-color: #ffe4c4; +} +.in-app-guide .alert.alert-success { + border-color: #8fbc8f; +} +.in-app-guide h4 { + font-weight: bold; +} diff --git a/src/aiidalab_qe/app/static/templates/guide.jinja b/src/aiidalab_qe/app/static/templates/guide.jinja index d72d7beef..0a1a5bcf1 100644 --- a/src/aiidalab_qe/app/static/templates/guide.jinja +++ b/src/aiidalab_qe/app/static/templates/guide.jinja @@ -42,5 +42,8 @@ For a more in-depth dive into the app's features, please refer to the how-to guides.

+ +

+ Alternatively, you can select one of our in-app guides below to walk through an example use-case.

diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index 998be5cf2..ebc5bfd19 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -14,6 +14,7 @@ LazyLoadedOptimade, LazyLoadedStructureBrowser, ) +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_widgets_base import ( BasicCellEditor, BasicStructureEditor, @@ -151,6 +152,57 @@ def render(self): ) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +
+ In this step, you can select a structure as follows: +
    +
  • + Upload file: + upload a structure file from your computer. +
  • +
  • + OPTIMADE: + search for structures in the OPTIMADE database. +
  • +
  • + AiiDA database: + search for structures in your AiiDA database. +
  • +
  • + From Examples: + select a structure from a list of example structures. +
  • +
+ 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. +
+
+

Tasks

+
    +
  1. Click on the From examples tab
  2. +
  3. Select Gold from the dropdown list
  4. +
  5. Click the Confirm button to proceed.
  6. +
+
+
+ Warning: If the confirmed structure is not yet stored + in the AiiDA database, it will be stored automatically when + you proceed to the next step. +
+
+ Warning: Changes after confirmation will unconfirm + this step and reset the following steps. +
+
+ """), + ], + ), ipw.HTML("""

Select a structure from one of the following sources and then diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index 344535f3a..cb9e8420e 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -11,6 +11,7 @@ from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.app.utils import get_entry_items from aiidalab_qe.common.code import PluginCodes, PwCodeModel +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.panel import PluginResourceSettingsModel, ResourceSettingsPanel from aiidalab_qe.common.setup_codes import QESetupWidget from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget @@ -158,6 +159,25 @@ def render(self): ) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +

+ In this step, we define the resources to be used in the + calculation. The global resources are used to define resources + across all workflow calculations. Optionally, you can override + the resource settings for specific calculations. +
+

Tasks

+
    +
  1. Select resources
  2. +
  3. Click Submit to proceed
  4. +
+
+
+ """) + ], + ), ipw.HTML("""

Codes

diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index a22248fbb..14ce07d74 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -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 @@ -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: @@ -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): @@ -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 @@ -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"], + description="Guides:", + value="none", + ) + self.job_history = QueryInterface() self.info_container = InfoBox() diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index 86a7eb26b..a665991f3 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -1,6 +1,7 @@ from __future__ import annotations import ipywidgets as ipw +import traitlets as tl class InfoBox(ipw.VBox): @@ -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" diff --git a/src/aiidalab_qe/plugins/bands/setting.py b/src/aiidalab_qe/plugins/bands/setting.py index efbbdb37b..4b70cdff3 100644 --- a/src/aiidalab_qe/plugins/bands/setting.py +++ b/src/aiidalab_qe/plugins/bands/setting.py @@ -2,6 +2,7 @@ import ipywidgets as ipw +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.panel import ConfigurationSettingsPanel from aiidalab_qe.plugins.bands.model import BandsConfigurationSettingsModel @@ -26,6 +27,19 @@ def render(self): ) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +
+ Here we configure the settings for computing the band + structure. +
+ Check Fat bands calculation +
+
+ """) + ], + ), ipw.HTML("""

Settings

diff --git a/src/aiidalab_qe/plugins/pdos/setting.py b/src/aiidalab_qe/plugins/pdos/setting.py index 462f3aa66..e08c361a1 100644 --- a/src/aiidalab_qe/plugins/pdos/setting.py +++ b/src/aiidalab_qe/plugins/pdos/setting.py @@ -2,6 +2,7 @@ import ipywidgets as ipw +from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.panel import ConfigurationSettingsPanel from .model import PdosConfigurationSettingsModel @@ -107,6 +108,19 @@ def render(self): ) self.children = [ + InAppGuide( + children=[ + ipw.HTML(""" +
+ Here we configure the settings for computing the projected + density of states, or PDOS. +
+ ??? +
+
+ """) + ], + ), ipw.HTML("""

Settings

diff --git a/tests/test_infobox.py b/tests/test_infobox.py index 892335da9..314d7928d 100644 --- a/tests/test_infobox.py +++ b/tests/test_infobox.py @@ -1,4 +1,4 @@ -from aiidalab_qe.common.infobox import InfoBox +from aiidalab_qe.common.infobox import InAppGuide, InfoBox def test_infobox_classes(): @@ -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", + ) + ) diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 609fa40d8..e4eed6a1c 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -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.""" @@ -27,13 +27,13 @@ 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}) @@ -41,11 +41,11 @@ def test_toggle_switch(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 _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): @@ -53,7 +53,7 @@ def _assert_about_is_on(self): 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 From 42afcfd5af0277de87dcc994331fa1a1cf43432a Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Mon, 9 Dec 2024 19:28:35 +0000 Subject: [PATCH 2/6] Generalize and simplify --- src/aiidalab_qe/app/configuration/__init__.py | 52 +------- .../app/configuration/advanced/advanced.py | 14 +-- .../app/configuration/basic/workflow.py | 22 +--- src/aiidalab_qe/app/main.py | 12 +- src/aiidalab_qe/app/structure/__init__.py | 52 +------- src/aiidalab_qe/app/submission/__init__.py | 20 +-- src/aiidalab_qe/app/wrapper.py | 10 +- src/aiidalab_qe/common/guide_manager.py | 50 ++++++++ src/aiidalab_qe/common/infobox.py | 43 +++---- src/aiidalab_qe/guides/basic.html | 119 ++++++++++++++++++ src/aiidalab_qe/plugins/bands/__init__.py | 3 + .../plugins/bands/guides/bands.html | 119 ++++++++++++++++++ src/aiidalab_qe/plugins/bands/setting.py | 14 +-- src/aiidalab_qe/plugins/pdos/setting.py | 14 +-- tests/test_infobox.py | 19 ++- 15 files changed, 343 insertions(+), 220 deletions(-) create mode 100644 src/aiidalab_qe/common/guide_manager.py create mode 100644 src/aiidalab_qe/guides/basic.html create mode 100644 src/aiidalab_qe/plugins/bands/guides/bands.html diff --git a/src/aiidalab_qe/app/configuration/__init__.py b/src/aiidalab_qe/app/configuration/__init__.py index 2adb18c3c..00e549f46 100644 --- a/src/aiidalab_qe/app/configuration/__init__.py +++ b/src/aiidalab_qe/app/configuration/__init__.py @@ -115,36 +115,13 @@ def render(self): children=[ ipw.VBox( children=[ - InAppGuide( - children=[ - ipw.HTML(""" -
- Here we select the properties to calculate. -
- Select Electronic band structure and - Projected density of states (PDOS) -
-
- """) - ], - ), + InAppGuide(identifier="properties-selection"), *self.property_children, ] ), ipw.VBox( children=[ - InAppGuide( - children=[ - ipw.HTML(""" -
- Here we can customize the calculation parameters. -
- Click on each tab to customize its settings. -
-
- """) - ], - ), + InAppGuide(identifier="calculation-settings"), self.tabs, ], ), @@ -171,30 +148,7 @@ def render(self): self.confirm_button.on_click(self.confirm) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- In this step, we define the workflow tasks, including structure - relaxation and which properties to compute, and select the - parameters of the calculations. -
-

Tasks

-
    -
  1. Select Full geometry relaxation
  2. -
  3. Open Step 2.1 to select properties
  4. -
  5. Open Step 2.2 to customize parameters
  6. -
  7. Click Confirm to proceed
  8. -
-
-
- Note: Changes after confirmation will unconfirm this - step and reset the following steps. -
-
- """) - ], - ), + InAppGuide(identifier="configuration-step"), self.structure_set_message, ipw.HTML("""
diff --git a/src/aiidalab_qe/app/configuration/advanced/advanced.py b/src/aiidalab_qe/app/configuration/advanced/advanced.py index e2b7e8a96..c675b4d83 100644 --- a/src/aiidalab_qe/app/configuration/advanced/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced/advanced.py @@ -275,19 +275,7 @@ def render(self): self.pseudos.render() self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- The Advanced settings allow you to finely tune the calculation. -
- In this walkthrough, we will not modify advanced settings - and proceed with the defaults. -
-
- """) - ], - ), + InAppGuide(identifier="advanced-settings"), ipw.HBox( children=[ self.clean_workdir, diff --git a/src/aiidalab_qe/app/configuration/basic/workflow.py b/src/aiidalab_qe/app/configuration/basic/workflow.py index 75fdce849..c58084957 100644 --- a/src/aiidalab_qe/app/configuration/basic/workflow.py +++ b/src/aiidalab_qe/app/configuration/basic/workflow.py @@ -61,27 +61,7 @@ def render(self): ) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- The basic settings panel provides top-level calculation - settings including the electronic and magnetic properties of - the material. It also provides a choice of three protocols - that pre-configure many calculation settings, balancing - speed with accuracy. -
- Select the fast protocol -
-
- Note: Due to the limited resources provided for the - demo server, we select the fast protocol to - reduce the cost of the calculation. -
-
- """) - ], - ), + InAppGuide(identifier="basic-settings"), ipw.HTML("""
Below you can indicate both if the material should be treated as an diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index 2cc5fc12c..b1aad16c8 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -110,17 +110,7 @@ def __init__(self, qe_auto_setup=True): super().__init__( children=[ - InAppGuide( - children=[ - ipw.HTML(""" -
- You've activated an in-app guide. Follow along below to learn - how to use the Quantum ESPRESSO app. -
- """) - ], - classes=["guide-warning"], - ), + InAppGuide(identifier="guide-warning", classes=["guide-warning"]), self.new_workchain_button, self._process_loading_message, self._wizard_app_widget, diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index ebc5bfd19..e32e47ecc 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -152,57 +152,7 @@ def render(self): ) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- In this step, you can select a structure as follows: -
    -
  • - Upload file: - upload a structure file from your computer. -
  • -
  • - OPTIMADE: - search for structures in the OPTIMADE database. -
  • -
  • - AiiDA database: - search for structures in your AiiDA database. -
  • -
  • - From Examples: - select a structure from a list of example structures. -
  • -
- 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. -
-
-

Tasks

-
    -
  1. Click on the From examples tab
  2. -
  3. Select Gold from the dropdown list
  4. -
  5. Click the Confirm button to proceed.
  6. -
-
-
- Warning: If the confirmed structure is not yet stored - in the AiiDA database, it will be stored automatically when - you proceed to the next step. -
-
- Warning: Changes after confirmation will unconfirm - this step and reset the following steps. -
-
- """), - ], - ), + InAppGuide(identifier="structure-step"), ipw.HTML("""

Select a structure from one of the following sources and then diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index cb9e8420e..e4f745abe 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -159,25 +159,7 @@ def render(self): ) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -

- In this step, we define the resources to be used in the - calculation. The global resources are used to define resources - across all workflow calculations. Optionally, you can override - the resource settings for specific calculations. -
-

Tasks

-
    -
  1. Select resources
  2. -
  3. Click Submit to proceed
  4. -
-
-
- """) - ], - ), + InAppGuide(identifier="submission-step"), ipw.HTML("""

Codes

diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index 14ce07d74..59cc530a1 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -4,6 +4,7 @@ import traitlets as tl from IPython.display import display +from aiidalab_qe.common.guide_manager import guide_manager from aiidalab_qe.common.widgets import LoadingWidget @@ -95,16 +96,19 @@ def _on_job_history_toggle(self, change: dict): 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_guide_options(self, _): + """Fetch the available guides.""" + self._view.guide_selection.options = ["none", *guide_manager.get_guides()] + 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") + self._view.on_displayed(self._set_guide_options) class AppWrapperModel(tl.HasTraits): @@ -196,7 +200,7 @@ def __init__(self) -> None: self.about = ipw.HTML(env.from_string(about_template).render()) self.guide_selection = ipw.RadioButtons( - options=["none", "basic"], + options=["none"], description="Guides:", value="none", ) diff --git a/src/aiidalab_qe/common/guide_manager.py b/src/aiidalab_qe/common/guide_manager.py new file mode 100644 index 000000000..9e4e8a755 --- /dev/null +++ b/src/aiidalab_qe/common/guide_manager.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from pathlib import Path + +import traitlets as tl +from bs4 import BeautifulSoup + +import aiidalab_qe +from aiidalab_qe.app.utils import get_entry_items + + +class GuideManager(tl.HasTraits): + active_guide = tl.Unicode("none", allow_none=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + guides = Path(aiidalab_qe.__file__).parent.joinpath("guides").glob("*") + self._guides = {guide.stem: guide.absolute() for guide in guides} + self._fetch_plugin_guides() + + self.content = BeautifulSoup() + + self.observe( + self._on_active_guide_change, + "active_guide", + ) + + @property + def has_guide(self): + return self.active_guide != "none" + + def get_guides(self): + return [*self._guides.keys()] + + def get_guide_section_by_id(self, content_id: str): + return self.content.find(attrs={"id": content_id}) + + def _on_active_guide_change(self, _): + guide_path = self._guides.get(self.active_guide) + html = Path(guide_path).read_text() if guide_path else "" + self.content = BeautifulSoup(html, "html.parser") + + def _fetch_plugin_guides(self): + entries: dict[str, Path] = get_entry_items("aiidalab_qe.properties", "guides") + for guides in entries.values(): + for guide in guides.glob("*"): + self._guides[guide.stem] = guide.absolute() + + +guide_manager = GuideManager() diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index a665991f3..841dc7622 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -1,7 +1,6 @@ from __future__ import annotations import ipywidgets as ipw -import traitlets as tl class InfoBox(ipw.VBox): @@ -23,19 +22,13 @@ def __init__(self, classes: list[str] | None = None, **kwargs): 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.""" + """The `InfoAppGuide` is used to set up toggleable in-app guides.""" def __init__( self, - guide_class: str = "qe-app", + children: list | None = None, + identifier: str = "", classes: list[str] | None = None, **kwargs, ): @@ -43,14 +36,16 @@ def __init__( Parameters ---------- - `guide_class` : `str`, optional - The identifier used to toggle the guide. - The default `qe-app` identifies built-in guide sections. + children : `list`, optional + The children of the guide. + `identifier` : `str` + The identifier used to load the guide file. `classes` : `list[str]`, optional One or more CSS classes. """ + from aiidalab_qe.common.guide_manager import guide_manager - self.guide_class = guide_class + self.manager = guide_manager super().__init__( classes=[ @@ -60,7 +55,15 @@ def __init__( **kwargs, ) - guide_manager.observe( + if children: + self.children = children + elif identifier: + self.children = [] + self.identifier = identifier + else: + raise ValueError("No content or path identifier provided") + + self.manager.observe( self._on_active_guide_change, "active_guide", ) @@ -74,9 +77,7 @@ 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" + if hasattr(self, "identifier"): + html = self.manager.get_guide_section_by_id(self.identifier) + self.children = [ipw.HTML(str(html))] if html else [] + self.layout.display = "flex" if self.manager.has_guide else "none" diff --git a/src/aiidalab_qe/guides/basic.html b/src/aiidalab_qe/guides/basic.html new file mode 100644 index 000000000..bd28dd655 --- /dev/null +++ b/src/aiidalab_qe/guides/basic.html @@ -0,0 +1,119 @@ +
+ You've activated an in-app guide. Follow along below to learn how to use the + Quantum ESPRESSO app. +
+ +
+ In this step, you can select a structure as follows: +
    +
  • Upload file: upload a structure file from your computer.
  • +
  • OPTIMADE: search for structures in the OPTIMADE database.
  • +
  • + AiiDA database: search for structures in your AiiDA database. +
  • +
  • + From Examples: select a structure from a list of example + structures. +
  • +
+ 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. +
+
+

Tasks

+
    +
  1. Click on the From examples tab
  2. +
  3. Select Gold from the dropdown list
  4. +
  5. Click the Confirm button to proceed.
  6. +
+
+
+ Warning: If the confirmed structure is not yet stored in the AiiDA + database, it will be stored automatically when you proceed to the next step. +
+
+ Warning: Changes after confirmation will unconfirm this step and + reset the following steps. +
+
+ +
+ In this step, we define the workflow tasks, including structure relaxation and + which properties to compute, and select the parameters of the calculations. +
+

Tasks

+
    +
  1. Select Full geometry relaxation
  2. +
  3. Open Step 2.1 to select properties
  4. +
  5. Open Step 2.2 to customize parameters
  6. +
  7. Click Confirm to proceed
  8. +
+
+
+ Note: Changes after confirmation will unconfirm this step and reset + the following steps. +
+
+ +
+ Here we select the properties to calculate. +
+ Select Electronic band structure and + Projected density of states (PDOS) +
+
+ +
+ Here we can customize the calculation parameters. +
+ Click on each tab to customize its settings. +
+
+ +
+ The basic settings panel provides top-level calculation settings including the + electronic and magnetic properties of the material. It also provides a choice + of three protocols that pre-configure many calculation settings, balancing + speed with accuracy. +
Select the fast protocol
+
+ Note: Due to the limited resources provided for the demo server, we select + the fast protocol to reduce the cost of the calculation. +
+
+ +
+ The Advanced settings allow you to finely tune the calculation. +
+ In this walkthrough, we will not modify advanced settings and proceed with + the defaults. +
+
+ +
+ Here we configure the settings for computing the band structure. +
Check Fat bands calculation
+
+ +
+ Here we configure the settings for computing the projected density of states, + or PDOS. +
???
+
+ +
+ In this step, we define the resources to be used in the calculation. The + global resources are used to define resources across all workflow + calculations. Optionally, you can override the resource settings for specific + calculations. +
+

Tasks

+
    +
  1. Select resources
  2. +
  3. Click Submit to proceed
  4. +
+
+
diff --git a/src/aiidalab_qe/plugins/bands/__init__.py b/src/aiidalab_qe/plugins/bands/__init__.py index 2cc72a868..3c2a97aca 100644 --- a/src/aiidalab_qe/plugins/bands/__init__.py +++ b/src/aiidalab_qe/plugins/bands/__init__.py @@ -1,4 +1,6 @@ # from aiidalab_qe.bands.result import Result +from pathlib import Path + from aiidalab_qe.common.panel import PluginOutline from .model import BandsConfigurationSettingsModel @@ -27,4 +29,5 @@ class BandsPluginOutline(PluginOutline): "model": BandsResultsModel, }, "workchain": workchain_and_builder, + "guides": Path(__file__).parent / "guides", } diff --git a/src/aiidalab_qe/plugins/bands/guides/bands.html b/src/aiidalab_qe/plugins/bands/guides/bands.html new file mode 100644 index 000000000..bd28dd655 --- /dev/null +++ b/src/aiidalab_qe/plugins/bands/guides/bands.html @@ -0,0 +1,119 @@ +
+ You've activated an in-app guide. Follow along below to learn how to use the + Quantum ESPRESSO app. +
+ +
+ In this step, you can select a structure as follows: +
    +
  • Upload file: upload a structure file from your computer.
  • +
  • OPTIMADE: search for structures in the OPTIMADE database.
  • +
  • + AiiDA database: search for structures in your AiiDA database. +
  • +
  • + From Examples: select a structure from a list of example + structures. +
  • +
+ 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. +
+
+

Tasks

+
    +
  1. Click on the From examples tab
  2. +
  3. Select Gold from the dropdown list
  4. +
  5. Click the Confirm button to proceed.
  6. +
+
+
+ Warning: If the confirmed structure is not yet stored in the AiiDA + database, it will be stored automatically when you proceed to the next step. +
+
+ Warning: Changes after confirmation will unconfirm this step and + reset the following steps. +
+
+ +
+ In this step, we define the workflow tasks, including structure relaxation and + which properties to compute, and select the parameters of the calculations. +
+

Tasks

+
    +
  1. Select Full geometry relaxation
  2. +
  3. Open Step 2.1 to select properties
  4. +
  5. Open Step 2.2 to customize parameters
  6. +
  7. Click Confirm to proceed
  8. +
+
+
+ Note: Changes after confirmation will unconfirm this step and reset + the following steps. +
+
+ +
+ Here we select the properties to calculate. +
+ Select Electronic band structure and + Projected density of states (PDOS) +
+
+ +
+ Here we can customize the calculation parameters. +
+ Click on each tab to customize its settings. +
+
+ +
+ The basic settings panel provides top-level calculation settings including the + electronic and magnetic properties of the material. It also provides a choice + of three protocols that pre-configure many calculation settings, balancing + speed with accuracy. +
Select the fast protocol
+
+ Note: Due to the limited resources provided for the demo server, we select + the fast protocol to reduce the cost of the calculation. +
+
+ +
+ The Advanced settings allow you to finely tune the calculation. +
+ In this walkthrough, we will not modify advanced settings and proceed with + the defaults. +
+
+ +
+ Here we configure the settings for computing the band structure. +
Check Fat bands calculation
+
+ +
+ Here we configure the settings for computing the projected density of states, + or PDOS. +
???
+
+ +
+ In this step, we define the resources to be used in the calculation. The + global resources are used to define resources across all workflow + calculations. Optionally, you can override the resource settings for specific + calculations. +
+

Tasks

+
    +
  1. Select resources
  2. +
  3. Click Submit to proceed
  4. +
+
+
diff --git a/src/aiidalab_qe/plugins/bands/setting.py b/src/aiidalab_qe/plugins/bands/setting.py index 4b70cdff3..225aac3ef 100644 --- a/src/aiidalab_qe/plugins/bands/setting.py +++ b/src/aiidalab_qe/plugins/bands/setting.py @@ -27,19 +27,7 @@ def render(self): ) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- Here we configure the settings for computing the band - structure. -
- Check Fat bands calculation -
-
- """) - ], - ), + InAppGuide(identifier="bands-settings"), ipw.HTML("""

Settings

diff --git a/src/aiidalab_qe/plugins/pdos/setting.py b/src/aiidalab_qe/plugins/pdos/setting.py index e08c361a1..c4cf0f301 100644 --- a/src/aiidalab_qe/plugins/pdos/setting.py +++ b/src/aiidalab_qe/plugins/pdos/setting.py @@ -108,19 +108,7 @@ def render(self): ) self.children = [ - InAppGuide( - children=[ - ipw.HTML(""" -
- Here we configure the settings for computing the projected - density of states, or PDOS. -
- ??? -
-
- """) - ], - ), + InAppGuide(identifier="pdos-settings"), ipw.HTML("""

Settings

diff --git a/tests/test_infobox.py b/tests/test_infobox.py index 314d7928d..871232db4 100644 --- a/tests/test_infobox.py +++ b/tests/test_infobox.py @@ -1,8 +1,7 @@ -from aiidalab_qe.common.infobox import InAppGuide, InfoBox - - def test_infobox_classes(): """Test `InfoBox` classes.""" + from aiidalab_qe.common.infobox import InfoBox + custom_classes = ["custom-1", "custom-2 custom-3"] infobox = InfoBox(classes=custom_classes) assert all( @@ -18,13 +17,21 @@ def test_infobox_classes(): def test_in_app_guide(): """Test `InAppGuide` class.""" - guide_class = "some_guide" - in_app_guide = InAppGuide(guide_class=guide_class) + import ipywidgets as ipw + + from aiidalab_qe.common.guide_manager import guide_manager + from aiidalab_qe.common.infobox import InAppGuide + + in_app_guide = InAppGuide(children=[ipw.HTML("Hello, World!")]) assert all( css_class in in_app_guide._dom_classes for css_class in ( "info-box", "in-app-guide", - f"{guide_class}-guide-identifier", ) ) + assert in_app_guide.children[0].value == "Hello, World!" + + guide_manager.active_guide = "basic" + in_app_guide = InAppGuide(identifier="guide-warning") + assert "You've activated an in-app guide" in in_app_guide.children[0].value From bb57f5c7ef81785a0de17c6a067402370e7883ec Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 10 Dec 2024 05:50:51 +0000 Subject: [PATCH 3/6] Improve readability --- src/aiidalab_qe/common/guide_manager.py | 35 +++++++++++++++++++++---- src/aiidalab_qe/common/infobox.py | 34 +++++++++++++++++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/aiidalab_qe/common/guide_manager.py b/src/aiidalab_qe/common/guide_manager.py index 9e4e8a755..2bcc6e72a 100644 --- a/src/aiidalab_qe/common/guide_manager.py +++ b/src/aiidalab_qe/common/guide_manager.py @@ -3,16 +3,20 @@ from pathlib import Path import traitlets as tl -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, PageElement import aiidalab_qe from aiidalab_qe.app.utils import get_entry_items class GuideManager(tl.HasTraits): + """A global guide manager that loads and manages guide sections.""" + active_guide = tl.Unicode("none", allow_none=True) def __init__(self, *args, **kwargs): + """`GuideManager` constructor.""" + super().__init__(*args, **kwargs) guides = Path(aiidalab_qe.__file__).parent.joinpath("guides").glob("*") self._guides = {guide.stem: guide.absolute() for guide in guides} @@ -26,21 +30,42 @@ def __init__(self, *args, **kwargs): ) @property - def has_guide(self): + def has_guide(self) -> bool: return self.active_guide != "none" - def get_guides(self): + def get_guides(self) -> list[str]: + """Return a list of available guides. + + Returns + ------- + `list[str]` + A list of the names of available guides. + """ return [*self._guides.keys()] - def get_guide_section_by_id(self, content_id: str): - return self.content.find(attrs={"id": content_id}) + def get_guide_section_by_id(self, content_id: str) -> PageElement | None: + """Return a guide section by its HTML `id` attribute. + + Parameters + ---------- + `content_id` : `str` + The HTML `id` attribute of the guide section. + + Returns + ------- + `PageElement` | `None` + The guide section or `None` if not found. + """ + return self.content.find(attrs={"id": content_id}) # type: ignore def _on_active_guide_change(self, _): + """Load the contents of the active guide.""" guide_path = self._guides.get(self.active_guide) html = Path(guide_path).read_text() if guide_path else "" self.content = BeautifulSoup(html, "html.parser") def _fetch_plugin_guides(self): + """Fetch guides from plugins.""" entries: dict[str, Path] = get_entry_items("aiidalab_qe.properties", "guides") for guides in entries.values(): for guide in guides.glob("*"): diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index 841dc7622..bdf72e988 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -23,7 +23,22 @@ def __init__(self, classes: list[str] | None = None, **kwargs): class InAppGuide(InfoBox): - """The `InfoAppGuide` is used to set up toggleable in-app guides.""" + """The `InAppGuide` is used to set up toggleable in-app guides. + + Attributes + ---------- + `manager` : `GuideManager` + A local reference to the global guide manager. + `identifier` : `str`, optional + If content `children` are not provided directly, the `identifier` + is used to fetch the corresponding guide section from the guide + currently loaded by the guide manager. + + Raises + ------ + `ValueError` + If neither content `children` or a guide section `identifier` are provided. + """ def __init__( self, @@ -37,9 +52,11 @@ def __init__( Parameters ---------- children : `list`, optional - The children of the guide. - `identifier` : `str` - The identifier used to load the guide file. + The content children of this guide section. + `identifier` : `str`, optional + If content `children` are not provided directly, the `identifier` + is used to fetch the corresponding guide section from the guide + currently loaded by the guide manager. `classes` : `list[str]`, optional One or more CSS classes. """ @@ -71,13 +88,18 @@ def __init__( # 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() + self._on_active_guide_change(None) def _on_active_guide_change(self, _): + self._update_contents() self._toggle_guide() - def _toggle_guide(self): + def _update_contents(self): + """Update the contents of the guide section.""" if hasattr(self, "identifier"): html = self.manager.get_guide_section_by_id(self.identifier) self.children = [ipw.HTML(str(html))] if html else [] + + def _toggle_guide(self): + """Toggle the visibility of the guide section.""" self.layout.display = "flex" if self.manager.has_guide else "none" From bfa4b022c5ad74fa967d663a9d72b36c79873652 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 11 Dec 2024 18:01:05 +0000 Subject: [PATCH 4/6] Implement guide uniqueness --- src/aiidalab_qe/app/wrapper.py | 59 +++++++-- src/aiidalab_qe/common/guide_manager.py | 35 +++++- .../guides/{basic.html => 0_basic.html} | 9 +- src/aiidalab_qe/guides/1_advanced.html | 10 ++ .../plugins/bands/guides/0_basic.html | 10 ++ .../plugins/bands/guides/bands.html | 119 ------------------ tests/test_infobox.py | 4 +- 7 files changed, 105 insertions(+), 141 deletions(-) rename src/aiidalab_qe/guides/{basic.html => 0_basic.html} (94%) create mode 100644 src/aiidalab_qe/guides/1_advanced.html create mode 100644 src/aiidalab_qe/plugins/bands/guides/0_basic.html delete mode 100644 src/aiidalab_qe/plugins/bands/guides/bands.html diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index 59cc530a1..8ecedb45a 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -60,7 +60,13 @@ def _on_guide_toggle(self, change: dict): if change["new"]: self._view.info_container.children = [ self._view.guide, - self._view.guide_selection, + ipw.HBox( + children=[ + self._view.guide_category_selection, + self._view.guide_selection, + ], + layout=ipw.Layout(align_items="baseline"), + ), ] self._view.info_container.layout.display = "flex" self._view.job_history_toggle.value = False @@ -94,21 +100,50 @@ def _on_job_history_toggle(self, change: dict): else: self._view.main.children = self._old_view - def _on_guide_select(self, change: dict): + def _on_guide_category_select(self, change: dict): + self._view.guide_selection.options = guide_manager.get_guides(change["new"]) + self._update_active_guide() + + def _on_guide_select(self, _): + self._update_active_guide() + + def _update_active_guide(self): """Sets the current active guide.""" - guide_manager.active_guide = change["new"] + category = self._view.guide_category_selection.value + guide = self._view.guide_selection.value + active_guide = f"{category}/{guide}" if category != "none" else category + guide_manager.active_guide = active_guide - def _set_guide_options(self, _): + def _set_guide_category_options(self, _): """Fetch the available guides.""" - self._view.guide_selection.options = ["none", *guide_manager.get_guides()] + self._view.guide_category_selection.options = [ + "none", + *guide_manager.get_guide_categories(), + ] 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") - self._view.on_displayed(self._set_guide_options) + 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_category_selection.observe( + self._on_guide_category_select, + "value", + ) + self._view.guide_selection.observe( + self._on_guide_select, + "value", + ) + self._view.on_displayed(self._set_guide_category_options) class AppWrapperModel(tl.HasTraits): @@ -199,11 +234,13 @@ 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( + self.guide_category_selection = ipw.RadioButtons( options=["none"], description="Guides:", value="none", + layout=ipw.Layout(width="max-content"), ) + self.guide_selection = ipw.RadioButtons(layout=ipw.Layout(margin="2px 20px")) self.job_history = QueryInterface() diff --git a/src/aiidalab_qe/common/guide_manager.py b/src/aiidalab_qe/common/guide_manager.py index 2bcc6e72a..15e313693 100644 --- a/src/aiidalab_qe/common/guide_manager.py +++ b/src/aiidalab_qe/common/guide_manager.py @@ -19,7 +19,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) guides = Path(aiidalab_qe.__file__).parent.joinpath("guides").glob("*") - self._guides = {guide.stem: guide.absolute() for guide in guides} + self._guides = { + "general": { + guide.stem.split("_", maxsplit=1)[1]: guide.absolute() + for guide in sorted(guides, key=lambda x: x.stem.split("_")[0]) + } + } + self._fetch_plugin_guides() self.content = BeautifulSoup() @@ -33,7 +39,7 @@ def __init__(self, *args, **kwargs): def has_guide(self) -> bool: return self.active_guide != "none" - def get_guides(self) -> list[str]: + def get_guide_categories(self) -> list[str]: """Return a list of available guides. Returns @@ -43,6 +49,16 @@ def get_guides(self) -> list[str]: """ return [*self._guides.keys()] + def get_guides(self, identifier: str) -> list[str]: + """Return a list of available sub-guides. + + Returns + ------- + `list[str]` + A list of the names of available sub-guides. + """ + return [*self._guides[identifier].keys()] if identifier != "none" else [] + def get_guide_section_by_id(self, content_id: str) -> PageElement | None: """Return a guide section by its HTML `id` attribute. @@ -60,16 +76,23 @@ def get_guide_section_by_id(self, content_id: str) -> PageElement | None: def _on_active_guide_change(self, _): """Load the contents of the active guide.""" - guide_path = self._guides.get(self.active_guide) + if self.active_guide == "none": + self.content = BeautifulSoup() + return + category, guide = self.active_guide.split("/") + guide_path = self._guides[category][guide] html = Path(guide_path).read_text() if guide_path else "" self.content = BeautifulSoup(html, "html.parser") def _fetch_plugin_guides(self): """Fetch guides from plugins.""" entries: dict[str, Path] = get_entry_items("aiidalab_qe.properties", "guides") - for guides in entries.values(): - for guide in guides.glob("*"): - self._guides[guide.stem] = guide.absolute() + for identifier, guides in entries.items(): + if identifier not in self._guides: + self._guides[identifier] = {} + for guide in sorted(guides.glob("*"), key=lambda x: x.stem.split("_")[0]): + stem = guide.stem.split("_", maxsplit=1)[1] + self._guides[identifier][stem] = guide.absolute() guide_manager = GuideManager() diff --git a/src/aiidalab_qe/guides/basic.html b/src/aiidalab_qe/guides/0_basic.html similarity index 94% rename from src/aiidalab_qe/guides/basic.html rename to src/aiidalab_qe/guides/0_basic.html index bd28dd655..e51f8763d 100644 --- a/src/aiidalab_qe/guides/basic.html +++ b/src/aiidalab_qe/guides/0_basic.html @@ -1,6 +1,6 @@
- You've activated an in-app guide. Follow along below to learn how to use the - Quantum ESPRESSO app. + You've activated the basic in-app guide. Follow along below to learn the basic + features of the Quantum ESPRESSO app.
@@ -101,7 +101,10 @@

Tasks

Here we configure the settings for computing the projected density of states, or PDOS. -
???
+
+ In this walkthrough, we will not modify pdos settings and proceed with the + defaults. +
diff --git a/src/aiidalab_qe/guides/1_advanced.html b/src/aiidalab_qe/guides/1_advanced.html new file mode 100644 index 000000000..90c25128d --- /dev/null +++ b/src/aiidalab_qe/guides/1_advanced.html @@ -0,0 +1,10 @@ +
+ You've activated the advanced in-app guide. Follow along below to learn how to + use the more advanced features of the Quantum ESPRESSO app. +
+ +
Advanced example
+ +
Advanced example
+ +
Advanced example
diff --git a/src/aiidalab_qe/plugins/bands/guides/0_basic.html b/src/aiidalab_qe/plugins/bands/guides/0_basic.html new file mode 100644 index 000000000..9af66d3e0 --- /dev/null +++ b/src/aiidalab_qe/plugins/bands/guides/0_basic.html @@ -0,0 +1,10 @@ +
+ You've activated the basic bands in-app guide. Follow along below to learn how + to use the Quantum ESPRESSO app to run a simple band structure calculation. +
+ +
Bands example
+ +
Bands example
+ +
Bands example
diff --git a/src/aiidalab_qe/plugins/bands/guides/bands.html b/src/aiidalab_qe/plugins/bands/guides/bands.html deleted file mode 100644 index bd28dd655..000000000 --- a/src/aiidalab_qe/plugins/bands/guides/bands.html +++ /dev/null @@ -1,119 +0,0 @@ -
- You've activated an in-app guide. Follow along below to learn how to use the - Quantum ESPRESSO app. -
- -
- In this step, you can select a structure as follows: -
    -
  • Upload file: upload a structure file from your computer.
  • -
  • OPTIMADE: search for structures in the OPTIMADE database.
  • -
  • - AiiDA database: search for structures in your AiiDA database. -
  • -
  • - From Examples: select a structure from a list of example - structures. -
  • -
- 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. -
-
-

Tasks

-
    -
  1. Click on the From examples tab
  2. -
  3. Select Gold from the dropdown list
  4. -
  5. Click the Confirm button to proceed.
  6. -
-
-
- Warning: If the confirmed structure is not yet stored in the AiiDA - database, it will be stored automatically when you proceed to the next step. -
-
- Warning: Changes after confirmation will unconfirm this step and - reset the following steps. -
-
- -
- In this step, we define the workflow tasks, including structure relaxation and - which properties to compute, and select the parameters of the calculations. -
-

Tasks

-
    -
  1. Select Full geometry relaxation
  2. -
  3. Open Step 2.1 to select properties
  4. -
  5. Open Step 2.2 to customize parameters
  6. -
  7. Click Confirm to proceed
  8. -
-
-
- Note: Changes after confirmation will unconfirm this step and reset - the following steps. -
-
- -
- Here we select the properties to calculate. -
- Select Electronic band structure and - Projected density of states (PDOS) -
-
- -
- Here we can customize the calculation parameters. -
- Click on each tab to customize its settings. -
-
- -
- The basic settings panel provides top-level calculation settings including the - electronic and magnetic properties of the material. It also provides a choice - of three protocols that pre-configure many calculation settings, balancing - speed with accuracy. -
Select the fast protocol
-
- Note: Due to the limited resources provided for the demo server, we select - the fast protocol to reduce the cost of the calculation. -
-
- -
- The Advanced settings allow you to finely tune the calculation. -
- In this walkthrough, we will not modify advanced settings and proceed with - the defaults. -
-
- -
- Here we configure the settings for computing the band structure. -
Check Fat bands calculation
-
- -
- Here we configure the settings for computing the projected density of states, - or PDOS. -
???
-
- -
- In this step, we define the resources to be used in the calculation. The - global resources are used to define resources across all workflow - calculations. Optionally, you can override the resource settings for specific - calculations. -
-

Tasks

-
    -
  1. Select resources
  2. -
  3. Click Submit to proceed
  4. -
-
-
diff --git a/tests/test_infobox.py b/tests/test_infobox.py index 871232db4..65651a4b4 100644 --- a/tests/test_infobox.py +++ b/tests/test_infobox.py @@ -32,6 +32,6 @@ def test_in_app_guide(): ) assert in_app_guide.children[0].value == "Hello, World!" - guide_manager.active_guide = "basic" + guide_manager.active_guide = "general/basic" in_app_guide = InAppGuide(identifier="guide-warning") - assert "You've activated an in-app guide" in in_app_guide.children[0].value + assert "You've activated the basic in-app guide" in in_app_guide.children[0].value From 1b8176929820c2f786ead976b26c11b04a04a0b7 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Mon, 16 Dec 2024 14:01:28 +0000 Subject: [PATCH 5/6] Add step4 and post-wizard guides --- src/aiidalab_qe/app/main.py | 2 + src/aiidalab_qe/app/result/__init__.py | 2 + src/aiidalab_qe/app/static/styles/infobox.css | 3 ++ src/aiidalab_qe/common/infobox.py | 33 +++++++++++--- src/aiidalab_qe/guides/0_basic.html | 44 ++++++++++++++++++- src/aiidalab_qe/guides/1_advanced.html | 6 +++ tests/test_infobox.py | 11 +++++ 7 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index b1aad16c8..d24bfcbf9 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -114,6 +114,8 @@ def __init__(self, qe_auto_setup=True): self.new_workchain_button, self._process_loading_message, self._wizard_app_widget, + InAppGuide(identifier="post-guide", classes=["post-guide"]), + InAppGuide(children=[ipw.HTML("hello")], guide_id="general/basic"), ] ) diff --git a/src/aiidalab_qe/app/result/__init__.py b/src/aiidalab_qe/app/result/__init__.py index 1f07f1fcc..386f7cd0a 100644 --- a/src/aiidalab_qe/app/result/__init__.py +++ b/src/aiidalab_qe/app/result/__init__.py @@ -3,6 +3,7 @@ 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, @@ -106,6 +107,7 @@ def render(self): ) self.children = [ + InAppGuide(identifier="results-step"), self.process_info, ipw.HBox( children=[ diff --git a/src/aiidalab_qe/app/static/styles/infobox.css b/src/aiidalab_qe/app/static/styles/infobox.css index 7d7b3ccc9..a66f0c773 100644 --- a/src/aiidalab_qe/app/static/styles/infobox.css +++ b/src/aiidalab_qe/app/static/styles/infobox.css @@ -22,6 +22,9 @@ border-color: #ffe4c4; margin-bottom: 1em; } +.post-guide { + margin: 1em 0; +} .in-app-guide .alert { color: black; margin: 10px 0; diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index bdf72e988..5d7b3b79a 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -43,6 +43,7 @@ class InAppGuide(InfoBox): def __init__( self, children: list | None = None, + guide_id: str = "", identifier: str = "", classes: list[str] | None = None, **kwargs, @@ -51,8 +52,12 @@ def __init__( Parameters ---------- - children : `list`, optional + `children` : `list`, optional The content children of this guide section. + `guide_id` : `str`, optional + The associated guide id to be used in conjunction with content children. + If none provided, the widget-based guide section will be shown for all + guides. `identifier` : `str`, optional If content `children` are not provided directly, the `identifier` is used to fetch the corresponding guide section from the guide @@ -73,12 +78,15 @@ def __init__( ) if children: + self.guide_id = guide_id self.children = children + self.identifier = None elif identifier: + self.guide_id = None self.children = [] self.identifier = identifier else: - raise ValueError("No content or path identifier provided") + raise ValueError("No widgets or path identifier provided") self.manager.observe( self._on_active_guide_change, @@ -96,10 +104,23 @@ def _on_active_guide_change(self, _): def _update_contents(self): """Update the contents of the guide section.""" - if hasattr(self, "identifier"): - html = self.manager.get_guide_section_by_id(self.identifier) - self.children = [ipw.HTML(str(html))] if html else [] + if not self.identifier: + return + html = self.manager.get_guide_section_by_id(self.identifier) + self.children = [ipw.HTML(str(html))] if html else [] def _toggle_guide(self): """Toggle the visibility of the guide section.""" - self.layout.display = "flex" if self.manager.has_guide else "none" + self.layout.display = ( + "flex" + if self.children + and ( + # file-based guide section + (self.identifier and self.manager.has_guide) + # widget-based guide section shown for all guides + or (not self.guide_id and self.manager.has_guide) + # widget-based guide section shown for a specific guide + or self.guide_id == self.manager.active_guide + ) + else "none" + ) diff --git a/src/aiidalab_qe/guides/0_basic.html b/src/aiidalab_qe/guides/0_basic.html index e51f8763d..b2edb75b1 100644 --- a/src/aiidalab_qe/guides/0_basic.html +++ b/src/aiidalab_qe/guides/0_basic.html @@ -115,8 +115,50 @@

Tasks

Tasks

    -
  1. Select resources
  2. +
  3. Select computational resources
  4. +
  5. Add a description of the calculation
  6. Click Submit to proceed
+ +
+ In this step, we can monitor the calculation and view its results. A summary + of calculation parameters is also provided. Raw input and output files may be + downloaded, as well as an AiiDA archive containing the full calculation + provenance. +
+

Tasks

+
    +
  1. + In the process tree, click on the root QeAppWorkChain node +
  2. +
  3. (optional) review the calculation parameters
  4. +
  5. Click on the Final Geometry tab
  6. +
  7. + When the structure relaxation is finished, click on + Load results to view the final structure geometry +
  8. +
  9. + Repeat the last two steps for the Bands and PDOS tabs +
  10. +
+
+
+ +
+ This concludes the basic Quantum ESPRESSO in-app guide. +
+

Exercises

+
    +
  1. + Download the raw input and output files for further analysis +
  2. +
  3. + Download the AiiDA archive to store the full calculation + provenance +
  4. +
  5. Explore the AiiDA database to view the calculation history
  6. +
+
+
diff --git a/src/aiidalab_qe/guides/1_advanced.html b/src/aiidalab_qe/guides/1_advanced.html index 90c25128d..234d1fa55 100644 --- a/src/aiidalab_qe/guides/1_advanced.html +++ b/src/aiidalab_qe/guides/1_advanced.html @@ -8,3 +8,9 @@
Advanced example
Advanced example
+ +
Advanced example
+ +
+ This concludes the advanced Quantum ESPRESSO in-app guide. +
diff --git a/tests/test_infobox.py b/tests/test_infobox.py index 65651a4b4..84289bdbb 100644 --- a/tests/test_infobox.py +++ b/tests/test_infobox.py @@ -32,6 +32,17 @@ def test_in_app_guide(): ) assert in_app_guide.children[0].value == "Hello, World!" + assert in_app_guide.layout.display == "none" + guide_manager.active_guide = "general/basic" + assert in_app_guide.layout.display == "flex" + guide_manager.active_guide = "general/advanced" + assert in_app_guide.layout.display == "flex" + in_app_guide.guide_id = "general/advanced" + guide_manager.active_guide = "general/basic" + assert in_app_guide.layout.display == "none" + guide_manager.active_guide = "general/advanced" + assert in_app_guide.layout.display == "flex" + guide_manager.active_guide = "general/basic" in_app_guide = InAppGuide(identifier="guide-warning") assert "You've activated the basic in-app guide" in in_app_guide.children[0].value From dfa9884c01340ee1fe3ffcdf0370c6216bae2c16 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Tue, 17 Dec 2024 17:26:59 +0000 Subject: [PATCH 6/6] Add documentation regarding guide usage --- docs/source/development/guides.rst | 51 ++++++++++++++++++++++++++++++ docs/source/development/index.rst | 1 + 2 files changed, 52 insertions(+) create mode 100644 docs/source/development/guides.rst diff --git a/docs/source/development/guides.rst b/docs/source/development/guides.rst new file mode 100644 index 000000000..eae1225b0 --- /dev/null +++ b/docs/source/development/guides.rst @@ -0,0 +1,51 @@ +.. _develop:guides: + +****** +Guides +****** + +Throughout the app, instances of ``aiidalab_qe.common.infobox.InAppGuide`` have been dropped to provide users with helpful information in the form of toggleable tutorials. +These guide section widgets observe a global instance of ``aiidalab_qe.common.guide_manager.GuideManager`` to obtain their content and toggle their visibility. +The guide manager is initialized at app startup and scans the local codebase, as well as entry-point-based plugins, for defined guides. +The discovered guides are then made available as a nested radio button widget of the form ``category`` -> ``guide``. +This allows both built-in ``general/`` guides and external ``/`` guides, ensuring guide uniqueness. + +Developers have two options when using the ``InAppGuide`` widget: + +#. **Widget-based guide sections**: Developers can define the guide content directly in the widget's constructor using ``ipywidgets`` widgets. + When using this approach, the developer may provide a ``guide_id`` in the constructor to associate the guide section with a specific guide. + Otherwise, if no ``guide_id`` is provided, the guide section will be displayed for any active guide. + + .. code:: python + + InAppGuide( + guide_id="general/basic", + children=[ + ipw.HTML("Some basic information."), + ], + ) + +#. **HTML-based guide sections**: Developers can define the guide content in an HTML document, tagging the various sections using the HTML ``id`` attribute. + The guide manager will load the HTML document associated with the selected guide and parse it using ``BeautifulSoup``. + + .. code:: html + +
+

Some basic information regarding the structure.

+
+ +
+

Some basic information regarding the configuration.

+
+ + ... + + HTML-based ``InAppGuide`` widgets are instantiated without children (or a guide id), but instead with an ``identifier`` corresponding to the relevant HTML ``id`` attribute. + + .. code:: python + + InAppGuide(identifier="structure-step") + +A decent amount of ``InAppGuide(identifier=)`` instances have been placed strategically throughout the app. +Developers may suggest additional core guide sections via GitHub pull requests. +For plugin developers, additional instances of either flavor are recommended to be added in any component of the plugin in conjunction with dedicated plugin-specific guides. diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index 712b0d37e..c3c37a02a 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -12,3 +12,4 @@ This guide explains the architecture of the application and how to extend the fu architecture plugin plugin_registry + guides