Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement path for in-app guides #763

Merged
merged 6 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/source/development/guides.rst
Original file line number Diff line number Diff line change
@@ -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/<guide-name>`` guides and external ``<plugin-identifier>/<guide-name>`` 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

<div id="structure-step">
<p>Some basic information regarding the structure.</p>
</div>

<div id="configuration-step">
<p>Some basic information regarding the configuration.</p>
</div>

...

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=<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.
1 change: 1 addition & 0 deletions docs/source/development/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ This guide explains the architecture of the application and how to extend the fu
architecture
plugin
plugin_registry
guides
10 changes: 9 additions & 1 deletion 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 (
ConfigurationSettingsModel,
ConfigurationSettingsPanel,
Expand Down Expand Up @@ -114,10 +115,16 @@ def render(self):
children=[
ipw.VBox(
children=[
InAppGuide(identifier="properties-selection"),
*self.property_children,
]
),
self.tabs,
ipw.VBox(
children=[
InAppGuide(identifier="calculation-settings"),
self.tabs,
],
),
],
layout=ipw.Layout(margin="10px 2px"),
selected_index=None,
Expand All @@ -141,6 +148,7 @@ def render(self):
self.confirm_button.on_click(self.confirm)

self.children = [
InAppGuide(identifier="configuration-step"),
self.structure_set_message,
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 0px">
Expand Down
7 changes: 2 additions & 5 deletions src/aiidalab_qe/app/configuration/advanced/advanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -274,11 +275,7 @@ def render(self):
self.pseudos.render()

self.children = [
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 10px">
<h4>Advanced Settings</h4>
</div>
"""),
InAppGuide(identifier="advanced-settings"),
ipw.HBox(
children=[
self.clean_workdir,
Expand Down
2 changes: 2 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 BasicConfigurationSettingsModel
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.panel import ConfigurationSettingsPanel


Expand Down Expand Up @@ -60,6 +61,7 @@ def render(self):
)

self.children = [
InAppGuide(identifier="basic-settings"),
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
4 changes: 4 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 @@ -109,9 +110,12 @@ def __init__(self, qe_auto_setup=True):

super().__init__(
children=[
InAppGuide(identifier="guide-warning", classes=["guide-warning"]),
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"),
]
)

Expand Down
2 changes: 2 additions & 0 deletions src/aiidalab_qe/app/result/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -106,6 +107,7 @@ def render(self):
)

self.children = [
InAppGuide(identifier="results-step"),
self.process_info,
ipw.HBox(
children=[
Expand Down
7 changes: 0 additions & 7 deletions src/aiidalab_qe/app/static/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 35 additions & 7 deletions src/aiidalab_qe/app/static/styles/infobox.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@
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;
}
.post-guide {
margin: 1em 0;
}
.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;
}
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>
2 changes: 2 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 @@ -151,6 +152,7 @@ def render(self):
)

self.children = [
InAppGuide(identifier="structure-step"),
ipw.HTML("""
<p>
Select a structure from one of the following sources and then
Expand Down
2 changes: 2 additions & 0 deletions src/aiidalab_qe/app/submission/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -158,6 +159,7 @@ def render(self):
)

self.children = [
InAppGuide(identifier="submission-step"),
ipw.HTML("""
<div style="padding-top: 0px; padding-bottom: 0px">
<h4>Codes</h4>
Expand Down
72 changes: 65 additions & 7 deletions src/aiidalab_qe/app/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import ipywidgets as ipw
import traitlets
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


Expand Down Expand Up @@ -56,7 +58,16 @@ 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,
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
else:
Expand Down Expand Up @@ -89,14 +100,53 @@ def _on_job_history_toggle(self, change: dict):
else:
self._view.main.children = self._old_view

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."""
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_category_options(self, _):
"""Fetch the available 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_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(traitlets.HasTraits):
class AppWrapperModel(tl.HasTraits):
"""An MVC model for `AppWrapper`."""

def __init__(self):
Expand All @@ -114,7 +164,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 +234,14 @@ 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_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()

self.info_container = InfoBox()
Expand Down
Loading
Loading