Skip to content

Control page: home page icon and panel placeholders - #232

Merged
yakutovicha merged 14 commits into
mainfrom
control-page/init
Jul 30, 2026
Merged

Control page: home page icon and panel placeholders#232
yakutovicha merged 14 commits into
mainfrom
control-page/init

Conversation

@yakutovicha

@yakutovicha yakutovicha commented Jul 16, 2026

Copy link
Copy Markdown
Member

fixes #155

Basic parts of the control section, extracted from #156.

Introduce a new "AiiDAlab control" page: a home-page entry point for
managing the AiiDA/AiiDAlab installation, with tab-based sections for
status, resources, storage, daemon, processes, profiles, and a danger
zone. Section bodies are placeholders in this PR; real logic for each
section lands in follow-up PRs.

  • start.py: add a "Control" icon tile linking to control.ipynb
  • control.ipynb: new notebook entry point with a lazily-loaded tab per
    section; each tab probes fresh data both on first open and on every
    revisit, and shows a retryable inline error if construction fails
  • home/control.py: add ControlSectionWidget, the shared base class for
    every section , plus placeholder subclasses for all seven sections
  • tests/test_control.py: cover refresh()'s re-entry guard, success
    path, and error path

Comment thread control.ipynb Outdated
Comment thread home/control.py Outdated

@edan-bainglass edan-bainglass left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some comments

Comment thread control.ipynb
Comment thread home/control.py Outdated
Comment thread home/control.py
Comment thread home/control.py
@danielhollas

Copy link
Copy Markdown
Contributor

Thanks for working on this @yakutovicha! Left some comments, most notably regarding thread safety of the refresh method.

@yakutovicha

yakutovicha commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Thanks for working on this @yakutovicha! Left some comments, most notably regarding thread safety of the refresh method.

It seems that the locking mechanism you propose makes things slightly more fragile.

A few observations:

  1. The refresh() method can't be run more than once at the same time, since everything that calls it lives inside the same thread (Jupyter kernel).
  2. For the lock to guard the execution, it needs to live inside the worker:
def refresh(self, _=None):
    if self._refresh_lock.locked():
        return
    def worker():
        with self._refresh_lock:
            self._do_refresh()
    threading.Thread(target=worker, daemon=True).start()
  1. The worker, in turn, is only launched when the thread actually gets CPU time, so there might be delays (can be milliseconds).

This leaves us with a possibility that two fast clicks can spawn 2 workers at the same time, which the current mechanism prevents entirely.

@edan-bainglass edan-bainglass left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yakutovicha. LGTM w.r.t my comments.

@danielhollas danielhollas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yakutovicha. I've found a couple other small things (some found by Claude), but happy for you to merge after addressing (or dismissing) them.

I'll reply to the threading issue separately.

Comment thread home/control.py Outdated
Comment thread home/control.py Outdated
Comment thread control.ipynb
Comment thread control.ipynb Outdated
Comment thread tests/test_control.py


@pytest.fixture
def run_threads_synchronously(monkeypatch):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This fixture feels very invasive, it would be nicer to write the tests in a robust way that would use an actual thread. But that can be done in a separate PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do it in a different PR, then. After this base PR is merged, many things can be implemented/discussed in parallel. Do you mind opening issues? 🙏

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #235, thanks!

@danielhollas

Copy link
Copy Markdown
Contributor

It seems that the locking mechanism you propose makes things slightly more fragile.

Sorry, my original suggestion indeed did not work, since the lock needs to be released from the worker thread. Here's a patch that I actually tested and seems to work. But I am happy to submit it as a separate PR since this would also require updating the tests and I don't want to block you.

diff --git a/home/control.py b/home/control.py
index 19bd92e..7f9e756 100644
--- a/home/control.py
+++ b/home/control.py
@@ -28,7 +28,7 @@ class ControlSectionWidget(ipw.VBox):
     description = ""
 
     def __init__(self, children, show_refresh_button=True):
-        self._refreshing = False
+        self._refresh_lock = threading.Lock()
 
         header_children = []
         if self.description:
@@ -70,9 +70,8 @@ class ControlSectionWidget(ipw.VBox):
         self.info.value = html.escape(text)
 
     def refresh(self, _=None):
-        if self._refreshing:
+        if not self._refresh_lock.acquire(blocking=False):
             return
-        self._refreshing = True
         if self.refresh_button is not None:
             self.refresh_button.disabled = True
         self.info.value = "Refreshing... <i class='fa fa-spinner fa-spin'></i>"
@@ -92,7 +91,7 @@ class ControlSectionWidget(ipw.VBox):
                 # button permanently disabled.
                 if self.refresh_button is not None:
                     self.refresh_button.disabled = False
-                self._refreshing = False
+                self._refresh_lock.release()
                 if self._last_updated is not None:
                     self._last_updated.value = (
                         f"Last updated: {datetime.now().strftime('%H:%M:%S')}"

The refresh() method can't be run more than once at the same time, since everything that calls it lives inside the same thread (Jupyter kernel).

So I actually thought the the kernel uses a separate thread to handle UI interactions, but was apparently wrong about that (and verified by printing threading.current_thread() in the refresh method). But that seems a bit like an implementation detail. Interestingly, there's a very recent work in Jupyter ecosystem to enable multiple threads (they call it subshells) so going forward a single-threaded assumption might not hold, but I guess it's fine for now. To enable subshells, one would need to run ipykernel>7.0 (we're running 6.5 now) and jupyterlab>4.4. See following for details (really interesting!)

https://ianthomas23.github.io/jupytercon2025-subshells/#1
ipython/ipykernel#1438

yakutovicha and others added 4 commits July 30, 2026 09:08
Co-authored-by: Daniel Hollas <daniel.hollas@bristol.ac.uk>
- always display full content of the page
- prevent the page asking a confirmation before closing
@yakutovicha
yakutovicha merged commit 20e3033 into main Jul 30, 2026
4 checks passed
@yakutovicha
yakutovicha deleted the control-page/init branch July 30, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement AiiDA-control page

4 participants