-
Notifications
You must be signed in to change notification settings - Fork 9
Control page: home page icon and panel placeholders #232
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ab9ea25
Control page: home page icon and panel placeholders
yakutovicha 4ee07b0
Import datetime fix
yakutovicha 4b84f07
Improve _do_refresh docstring
yakutovicha 6817df4
Add tests
yakutovicha c02f9ee
Update control page title and description
yakutovicha e3e30ef
Refresh section on first tab open (not just revisit)
yakutovicha b3b6174
Put load_profile() to a separate cell.
yakutovicha 01af425
Make refresh button optional
yakutovicha 28ffa6c
Swap refresh button and info message
yakutovicha c13a4b3
Move last updated timestamp to the else section
yakutovicha ded93ed
Update home/control.py
yakutovicha 0e801f9
control.ipynb: javascript code cell
yakutovicha c9c0e41
AiiDAlab logo: keep only logo reading inside try section
yakutovicha ddef5f6
DangerZoneWidget: do not specify show_refresh_button argument.
yakutovicha File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| { | ||
| "cells": [ | ||
| { | ||
| "cell_type": "code", | ||
| "execution_count": null, | ||
| "id": "0", | ||
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "%%javascript\n", | ||
| "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", | ||
| " return false;\n", | ||
| "}\n", | ||
| "if (document.getElementById('appmode-busy')) {\n", | ||
| " window.onbeforeunload = function() {return}\n", | ||
| "}" | ||
| ] | ||
| }, | ||
| { | ||
| "cell_type": "code", | ||
| "execution_count": null, | ||
| "id": "1", | ||
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [] | ||
| }, | ||
| { | ||
| "cell_type": "code", | ||
| "execution_count": null, | ||
| "id": "2", | ||
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "from aiida import load_profile\n", | ||
| "\n", | ||
| "_ = load_profile()" | ||
| ] | ||
| }, | ||
| { | ||
| "cell_type": "code", | ||
| "execution_count": null, | ||
| "id": "3", | ||
| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "import re\n", | ||
| "from pathlib import Path\n", | ||
| "\n", | ||
| "import ipywidgets as ipw\n", | ||
| "\n", | ||
| "from home import control" | ||
| ] | ||
| }, | ||
| { | ||
| "cell_type": "code", | ||
| "execution_count": null, | ||
| "id": "4", | ||
| "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", | ||
|
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", | ||
| "except OSError:\n", | ||
| " 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", | ||
| "else:\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", | ||
| "\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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| 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, show_refresh_button=True): | ||
| self._refreshing = False | ||
|
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 = None | ||
| self._last_updated = None | ||
| footer_children = [] | ||
| if show_refresh_button: | ||
| self.refresh_button = ipw.Button(description="Refresh", icon="refresh") | ||
| self.refresh_button.on_click(self.refresh) | ||
| footer_children.append(self.refresh_button) | ||
| self._last_updated = ipw.HTML() | ||
| footer_children.append(self._last_updated) | ||
| self.info = ipw.HTML() | ||
| footer_children.append(self.info) | ||
| footer = ipw.HBox(footer_children) | ||
|
|
||
| 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 | ||
|
danielhollas marked this conversation as resolved.
|
||
| if self.refresh_button is not None: | ||
| self.refresh_button.disabled = True | ||
| self.info.value = "Refreshing... <i class='fa fa-spinner fa-spin'></i>" | ||
|
|
||
| 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 = "" | ||
| if self._last_updated is not None: | ||
| self._last_updated.value = ( | ||
| f"Last updated: {datetime.now().strftime('%H:%M:%S')}" | ||
| ) | ||
| 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._refreshing = False | ||
| if self.refresh_button is not None: | ||
| self.refresh_button.disabled = False | ||
|
|
||
| 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.")]) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.