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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 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,6 +110,7 @@ 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,
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
39 changes: 32 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,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;
}
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
98 changes: 98 additions & 0 deletions src/aiidalab_qe/common/guide_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from __future__ import annotations

from pathlib import Path

import traitlets as tl
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 = {
"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()

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

@property
def has_guide(self) -> bool:
return self.active_guide != "none"

def get_guide_categories(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_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.

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."""
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 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()
Loading
Loading