Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
136 changes: 136 additions & 0 deletions control.ipynb
Comment thread
yakutovicha marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "0",
"metadata": {},
"outputs": [],
"source": [
"import re\n",
"from pathlib import Path\n",
"\n",
"import ipywidgets as ipw\n",
"from aiida import load_profile\n",
"\n",
"load_profile()\n",
Comment thread
yakutovicha marked this conversation as resolved.
Outdated
"from home import control"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"sections = {\n",
" \"Status\": control.StatusOverviewWidget,\n",
" \"Resources\": control.SystemResourcesWidget,\n",
" \"Storage\": control.StorageWidget,\n",
" \"Daemon\": control.DaemonControlWidget,\n",
" \"Processes\": control.ProcessControlWidget,\n",
" \"Profiles\": control.ProfileControlWidget,\n",
" \"Danger zone\": control.DangerZoneWidget,\n",
"}\n",
"\n",
"placeholders = [\n",
" ipw.VBox([ipw.HTML(\"Loading... <i class='fa fa-spinner fa-spin'></i>\")])\n",
" for _ in sections\n",
"]\n",
"section_names = list(sections)\n",
"section_instances = {}\n",
"\n",
"tabs = ipw.Tab(children=placeholders, titles=section_names)\n",
"\n",
"\n",
"def open_section(index):\n",
" name = section_names[index]\n",
" placeholder = placeholders[index]\n",
" widget = section_instances.get(name)\n",
" if widget is None:\n",
" try:\n",
" widget = sections[name]()\n",
" except Exception as exc:\n",
" # Show the error but do NOT cache it: revisiting the tab retries.\n",
" placeholder.children = [\n",
" ipw.HTML(\n",
" f'<div class=\"alert alert-danger\" role=\"alert\">'\n",
" f'Failed to open the \"{name}\" section: {exc}</div>'\n",
" )\n",
Comment thread
danielhollas marked this conversation as resolved.
" ]\n",
" return\n",
" section_instances[name] = widget\n",
" placeholder.children = [widget]\n",
"\n",
" # First open and every revisit both probe for fresh data.\n",
" refresher = getattr(widget, \"refresh\", None)\n",
" if callable(refresher):\n",
" refresher()\n",
"\n",
"\n",
"def on_tab_change(change):\n",
" open_section(change[\"new\"])\n",
"\n",
"\n",
"tabs.observe(on_tab_change, names=\"selected_index\")\n",
"\n",
"try:\n",
" _logo_svg = Path(\"aiidalab_logo_v4.svg\").read_text()\n",
" _logo_svg = _logo_svg.split(\"?>\", 1)[-1].strip()\n",
" _logo_svg = re.sub(\n",
" r\"<svg\\b[^>]*>\",\n",
" lambda m: re.sub(r'\\s(width|height)=\"[^\"]*\"', \"\", m.group(0)),\n",
" _logo_svg,\n",
" count=1,\n",
" )\n",
" header = ipw.HTML(\n",
" \"<div style='display:flex;align-items:center;gap:16px;margin:4px 0 12px 0;'>\"\n",
" f\"<div style='width:56px;flex:none;'>{_logo_svg}</div>\"\n",
" \"<div>\"\n",
" \"<h1 style='margin:0;font-size:26px;'>AiiDAlab control</h1>\"\n",
" \"<div style='color:gray;'>Manage your AiiDAlab installation.</div>\"\n",
" \"</div></div>\"\n",
" )\n",
"except OSError:\n",
Comment thread
danielhollas marked this conversation as resolved.
Outdated
" header = ipw.HTML(\n",
" \"<div style='margin:4px 0 12px 0;'>\"\n",
" \"<h1 style='margin:0;font-size:26px;'>AiiDAlab control</h1>\"\n",
" \"<div style='color:gray;'>Manage your AiiDAlab installation.</div>\"\n",
" \"</div>\"\n",
" )\n",
"\n",
"tabs.layout = ipw.Layout(max_width=\"1100px\")\n",
"header.layout = ipw.Layout(max_width=\"1100px\")\n",
"\n",
"display(header)\n",
"display(tabs)\n",
"# selected_index is already 0, so the observer never fired for the landing\n",
"# tab; fill it explicitly AFTER display so the page shell (tab bar +\n",
"# \"Loading...\") paints before the first, potentially slow, section probe runs.\n",
"open_section(0)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
147 changes: 147 additions & 0 deletions home/control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import html
import threading
from datetime import datetime

import ipywidgets as ipw

from home.themes import ThemeDefault as Theme

_STATE_COLORS = {
"ok": Theme.COLORS.CHECK,
"warning": Theme.COLORS.AIIDALAB_ORANGE,
"error": Theme.COLORS.DANGER,
}


def _state_span(state, text) -> str:
return f"<span style='color:{_STATE_COLORS[state]}'>{html.escape(text)}</span>"


class ControlSectionWidget(ipw.VBox):
"""Shared anatomy for a control-page tab: description, body, footer.

The footer (refresh button, "Last updated" label, transient feedback) and
the threaded refresh-with-guard pattern are common to every section, so
they live here; subclasses only provide a body and a `_do_refresh()`.
"""

description = ""

def __init__(self, children):
self._refreshing = False
Comment thread
danielhollas marked this conversation as resolved.

header_children = []
if self.description:
header_children.append(
ipw.HTML(
f"<div style='color:{Theme.COLORS.GRAY};font-size:13px;"
f"margin:2px 0 6px 0;'>{html.escape(self.description)}</div>"
)
)

self.refresh_button = ipw.Button(description="Refresh", icon="refresh")
self.refresh_button.on_click(self.refresh)
Comment thread
yakutovicha marked this conversation as resolved.
Outdated
self._last_updated = ipw.HTML()
self.info = ipw.HTML()
footer = ipw.HBox([self.refresh_button, self._last_updated, self.info])

super().__init__(
children=[*header_children, *children, footer],
)
self.layout.padding = "8px 0 0 0"

def show_success(self, text):
self.info.value = _state_span("ok", text)

def show_warning(self, text):
self.info.value = _state_span("warning", text)

def show_error(self, text):
self.info.value = _state_span("error", text)

def show_plain(self, text):
self.info.value = html.escape(text)

def refresh(self, _=None):
if self._refreshing:
return
self._refreshing = True
Comment thread
danielhollas marked this conversation as resolved.
self.info.value = "Refreshing... <i class='fa fa-spinner fa-spin'></i>"
self.refresh_button.disabled = True
Comment thread
yakutovicha marked this conversation as resolved.
Outdated

def worker():
try:
self._do_refresh()
except Exception as exc:
self.show_error(f"Failed to refresh: {exc}")
else:
# Silent success: the "Last updated" timestamp below already
# signals success, so no "refreshed" message is shown.
self.info.value = ""
finally:
# Re-enable the button before touching anything else: if a
# later step raises, the page must not be left with the
# button permanently disabled.
self.refresh_button.disabled = False
self._refreshing = False
self._last_updated.value = (
f"Last updated: {datetime.now().strftime('%H:%M:%S')}"
)

threading.Thread(target=worker, daemon=True).start()

def _do_refresh(self):
"""Synchronous probe/update; subclasses override.

Runs on a background thread — avoid direct ipywidgets state
mutation here except via the thread-safe show_*/info hooks.
"""


class DaemonControlWidget(ControlSectionWidget):
description = "The daemon runs your AiiDA processes in the background."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class StatusOverviewWidget(ControlSectionWidget):
description = "Health of the services behind AiiDA."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class SystemResourcesWidget(ControlSectionWidget):
description = "Memory, CPU and disk usage of this container."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class StorageWidget(ControlSectionWidget):
description = "Disk usage of profile data and apps; storage maintenance."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class ProcessControlWidget(ControlSectionWidget):
description = "Inspect, pause, resume or kill AiiDA processes."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class ProfileControlWidget(ControlSectionWidget):
description = "Manage AiiDA profiles: default profile and deletion."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])


class DangerZoneWidget(ControlSectionWidget):
description = "Irreversible actions that can lead to data loss."

def __init__(self):
super().__init__([ipw.HTML("To be implemented.")])
24 changes: 19 additions & 5 deletions start.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

def get_start_widget(appbase, jupbase):
# http://fontawesome.io/icons/

width = "70px"

template = """
<center>
<table>
Expand All @@ -15,7 +18,7 @@ def get_start_widget(appbase, jupbase):
File Manager
</td>

<td style="width:70px"></td>
<td style="width:{width}"></td>

<td style="text-align:center">
<a style="{style}" href="{appbase}/terminal.ipynb" title="Open Terminal" target="_blank">
Expand All @@ -24,7 +27,7 @@ def get_start_widget(appbase, jupbase):
Terminal
</td>

<td style="width:70px"></td>
<td style="width:{width}"></td>

<td style="text-align:center">
<a style="{style}" href="{jupbase}/tree/#running" title="Task Manager" target="_blank">
Expand All @@ -33,7 +36,7 @@ def get_start_widget(appbase, jupbase):
Tasks
</td>

<td style="width:70px"></td>
<td style="width:{width}"></td>

<td style="text-align:center">
<a style="{style}" href="{appbase}/appstore.ipynb" title="Install New Apps" target="_blank">
Expand All @@ -42,7 +45,16 @@ def get_start_widget(appbase, jupbase):
App Store
</td>

<td style="width:70px"></td>
<td style="width:{width}"></td>

<td style="text-align:center">
<a style="{style}" href="{appbase}/control.ipynb" title="Manage AiiDA and AiiDAlab" target="_blank">
<i class="fa fa-cogs fa-3x" aria-hidden="true"></i>
</a><br>
Control
</td>

<td style="width:{width}"></td>

<td style="text-align:center">
<a style="{style}" href="https://aiidalab.readthedocs.io" title="Learn about AiiDAlab" target="_blank">
Expand All @@ -58,5 +70,7 @@ def get_start_widget(appbase, jupbase):
</center>
"""

html = template.format(appbase=appbase, jupbase=jupbase, style="margin:20px")
html = template.format(
appbase=appbase, jupbase=jupbase, style="margin:20px", width=width
)
return ipw.HTML(html)
Loading