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

Add auto-detected home warning message mechanism #183

Merged
merged 11 commits into from
Dec 5, 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
run: echo "JUPYTER_TOKEN=$(openssl rand -hex 32)" >> $GITHUB_ENV

- name: Run pytest
run: pytest -v --driver ${{ matrix.browser }} tests_notebooks
run: pytest -v --driver ${{ matrix.browser }} tests_notebooks/
env:
TAG: edge

Expand Down
58 changes: 42 additions & 16 deletions home/start_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
from glob import glob
from os import path
from pathlib import Path

import ipywidgets as ipw
import traitlets
Expand All @@ -29,7 +30,7 @@ def create_app_widget_move_buttons(name):


class AiidaLabHome:
"""Class that mananges the appearance of the AiiDAlab home page."""
"""Class that manages the appearance of the AiiDAlab home page."""

def __init__(self):
self.config_fn = ".launcher.json"
Expand All @@ -40,16 +41,16 @@ def _create_app_widget(self, name):
"""Create the widget representing the app on the home screen."""
config = self.read_config()
app = AiidaLabApp(name, None, AIIDALAB_APPS)

if name == "home":
app_widget = AppWidget(app, allow_move=False, allow_manage=False)
else:
app_widget = CollapsableAppWidget(app, allow_move=True)
app_widget.hidden = name in config["hidden"]
app_widget.observe(self._on_app_widget_change_hidden, names=["hidden"])

app_widget = CollapsableAppWidget(app, allow_move=True)
app_widget.hidden = name in config["hidden"]
app_widget.observe(self._on_app_widget_change_hidden, names=["hidden"])
return app_widget

def _create_home_widget(self):
edan-bainglass marked this conversation as resolved.
Show resolved Hide resolved
"""Create the home app widget."""
app = AiidaLabApp("home", None, AIIDALAB_APPS)
return AppWidget(app, allow_move=False, allow_manage=False)

def _on_app_widget_change_hidden(self, change):
"""Record whether a app widget is hidden on the home screen in the config file."""
config = self.read_config()
Expand All @@ -72,16 +73,28 @@ def read_config(self):
def render(self):
"""Rendering all apps."""

displayed_apps = []
home = self._create_home_widget()
children = [home]

config_dir = Path.home() / ".aiidalab"
warning_file = config_dir / "home_app_warning.md"

if warning_file.exists():
content = warning_file.read_text()
notification = self._create_notification(content)
children.append(notification)

apps = self.load_apps()

for name in apps:
# Create app widget if it has not been created yet.
if name not in self._app_widgets:
self._app_widgets[name] = self._create_app_widget(name)

displayed_apps.append(self._app_widgets[name])
self.output.children = displayed_apps
children.append(self._app_widgets[name])

self.output.children = children

return self.output

def load_apps(self):
Expand All @@ -98,7 +111,7 @@ def load_apps(self):
apps.sort(key=lambda x: order.index(x) if x in order else -1)
config["order"] = apps
self.write_config(config)
return ["home", *apps]
return apps

def move_updown(self, name, delta):
"""Move the app up/down on the start page."""
Expand All @@ -111,6 +124,19 @@ def move_updown(self, name, delta):
config["order"] = order
self.write_config(config)

def _create_notification(self, content):
from IPython.display import Markdown, display
from jinja2 import Environment

env = Environment()
notification = env.from_string(content).render()
output = ipw.Output()
notification_widget = ipw.VBox(children=[output])
notification_widget.add_class("home-notification")
with output:
display(Markdown(notification))
return notification_widget


class AppWidget(ipw.VBox):
"""Widget that represents an app as part of the home page."""
Expand All @@ -119,7 +145,7 @@ def __init__(self, app, allow_move=False, allow_manage=True):
self.app = app

launcher = load_widget(app.name)
launcher.layout = ipw.Layout(width="900px")
launcher.layout.flex = "1" # fill available space

header_items = []
footer_items = []
Expand All @@ -128,7 +154,7 @@ def __init__(self, app, allow_move=False, allow_manage=True):
app_status_info = AppStatusInfoWidget()
for trait in ("detached", "compatible", "remote_update_status"):
ipw.dlink((app, trait), (app_status_info, trait))
app_status_info.layout.margin = "0px 0px 0px 800px"
app_status_info.layout.margin = "0px 0px 0px auto"
header_items.append(app_status_info)

footer_items.append(
Expand All @@ -150,7 +176,7 @@ def __init__(self, app, allow_move=False, allow_manage=True):

footer = ipw.HTML(" ".join(footer_items), layout={"width": "initial"})
footer.layout.margin = (
"0px 0px 0px 700px" if allow_manage else "0px 0px 20px 0px"
"0px 0px 0px auto" if allow_manage else "0px 0px 20px 0px"
)

super().__init__(children=[header, body, footer])
Expand Down
8 changes: 7 additions & 1 deletion start.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
" .output_subarea {\n",
" max-width: none !important;\n",
" }\n",
" .home-notification {\n",
" background-color: antiquewhite;\n",
" margin: 2px;\n",
" padding: 8px;\n",
" border: 1px solid red;\n",
" }\n",
"</style>\n"
]
},
Expand Down Expand Up @@ -64,7 +70,7 @@
" home.move_updown(parsed_url[\"move_up\"][0], -1)\n",
"elif \"move_down\" in parsed_url:\n",
" home.move_updown(parsed_url[\"move_down\"][0], +1)\n",
"display(home.render())"
"home.render()"
]
},
{
Expand Down
41 changes: 31 additions & 10 deletions tests_notebooks/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,39 @@ def docker_compose(docker_services):


@pytest.fixture(scope="session")
def aiidalab_exec(docker_compose):
def execute(command, user=None, **kwargs):
workdir = "/home/jovyan/apps/home"
if user:
command = f"exec --workdir {workdir} -T --user={user} aiidalab {command}"
else:
command = f"exec --workdir {workdir} -T aiidalab {command}"
def aiidalab_exec(docker_compose, nb_user):
"""Execute command inside the AiiDAlab test container"""

def execute(command, user=None, **kwargs):
workdir = f"/home/{nb_user}/apps/home"
if user is None:
user = nb_user
command = (
f"exec --workdir {workdir} -T --user={user} aiidalab bash -c '{command}'"
edan-bainglass marked this conversation as resolved.
Show resolved Hide resolved
)
return docker_compose.execute(command, **kwargs)

return execute


@pytest.fixture(scope="session")
def nb_user():
return "jovyan"


@pytest.fixture
def create_warning_file(nb_user, aiidalab_exec):
config_folder = f"/home/{nb_user}/.aiidalab"
aiidalab_exec(f"mkdir -p {config_folder}")
aiidalab_exec(f"echo Warning! > {config_folder}/home_app_warning.md")


@pytest.fixture(scope="session", autouse=True)
def notebook_service(docker_ip, docker_services, aiidalab_exec):
def notebook_service(docker_ip, docker_services, aiidalab_exec, nb_user):
"""Ensure that HTTP service is up and responsive."""
# Directory ~/apps/home/ is mounted by docker,
# make it writeable for jovyan user, needed for `pip install`
aiidalab_exec("chmod -R a+rw /home/jovyan/apps/home", user="root")
aiidalab_exec(f"chmod -R a+rw /home/{nb_user}/apps/home", user="root")

aiidalab_exec("pip install --no-cache-dir .")

Expand All @@ -75,6 +89,12 @@ def notebook_service(docker_ip, docker_services, aiidalab_exec):

@pytest.fixture(scope="function")
def selenium_driver(selenium, notebook_service):
"""This is the main fixture to be used in tests.

We're already guaranteed that the container is up and responding to HTTP requests.
(via `notebook_service` fixture).
"""

def _selenium_driver(nb_path, url_params=None):
url, token = notebook_service
url_with_token = urljoin(url, f"apps/apps/home/{nb_path}?token={token}")
Expand Down Expand Up @@ -109,8 +129,9 @@ def _selenium_driver(nb_path, url_params=None):
@pytest.fixture
def final_screenshot(request, screenshot_dir, selenium):
"""Take screenshot at the end of the test.

Screenshot name is generated from the test function name
by stripping the 'test_' prefix
by stripping the 'test_' prefix.
"""
screenshot_name = f"{request.function.__name__[5:]}.png"
screenshot_path = Path.joinpath(screenshot_dir, screenshot_name)
Expand Down
11 changes: 11 additions & 0 deletions tests_notebooks/test_notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from selenium.webdriver.common.by import By


def test_home_notification(selenium_driver, create_warning_file, final_screenshot):
selenium = selenium_driver("start.ipynb")
selenium.set_window_size(1000, 941)
notifications = selenium.find_elements(By.CLASS_NAME, "home-notification")
assert len(notifications) == 1
home_warning = notifications[0]
content_element = home_warning.find_element(By.TAG_NAME, "p")
assert content_element.text == "Warning!"
Loading