add connect and login interface to the napery-tiled GUI#30
add connect and login interface to the napery-tiled GUI#30Xiaogang Yang (XYangXRay) wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a connection + authentication flow to the napari-tiled browser widget, including a dedicated login UI and persistence of connection details across sessions.
Changes:
- Introduces
QTiledLoginWidgetto support API key, password, and device-code authentication flows. - Updates
QTiledBrowserandTiledSelectorto probe server auth requirements, drive login/logout UI, and handle device-code polling. - Adds YAML-backed persistent config helpers for saved URL/username.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
src/napari_tiled_browser/qt/tiled_widget.py |
Embeds the login widget, wires new auth signals/slots, and adds device-code polling timer. |
src/napari_tiled_browser/qt/tiled_login.py |
New Qt login widget with method selector + stacked auth forms. |
src/napari_tiled_browser/models/tiled_selector.py |
Adds Context-based connection/auth flows and new auth-related Qt signals. |
src/napari_tiled_browser/config.py |
New config load/save helpers using platformdirs + YAML. |
src/napari_tiled_browser/config.yaml |
Bundled default config values for initial startup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1,20 +1,26 @@ | |||
| import json | |||
| import logging | |||
| import time | |||
There was a problem hiding this comment.
time is imported but never used, which will fail ruff (F401) under this repo’s lint settings. Remove the unused import.
| import time |
| if not auth_is_required and not providers: | ||
| # No auth needed, connect directly | ||
| self._finalize_connection() | ||
| return |
There was a problem hiding this comment.
When no authentication is required/available, the connection succeeds but the URL is never persisted via save_login_info, which doesn’t match the PR description (“URL and username are saved to config.yaml for next session”). Consider saving at least the URL on any successful connection (e.g., in _finalize_connection()), with an empty username if unknown.
| self.login_widget.setVisible(False) | ||
|
|
||
| # Device code polling timer | ||
| self._device_code_timer = QTimer() |
There was a problem hiding this comment.
The device-code polling timer is created without a Qt parent (QTimer()), which can make lifecycle management harder and risks it surviving longer than the widget. Prefer QTimer(self) (or QTimer(parent=self)) so it is cleaned up automatically when the widget is destroyed.
| self._device_code_timer = QTimer() | |
| self._device_code_timer = QTimer(self) |
| identity_id = tokens.get("identity", {}).get("id", username) | ||
| save_login_info(self.url, str(identity_id)) | ||
| self.auth_success.emit(str(identity_id)) |
There was a problem hiding this comment.
The value saved to the username config key here is identity_id (potentially a UUID from the token payload), not the actual username the user entered. Since the UI uses this config value to prefill the username field on next start, consider saving the entered username (and, if needed, store identity/UUID under a separate key).
| identity_id = tokens.get("identity", {}).get("id", username) | |
| save_login_info(self.url, str(identity_id)) | |
| self.auth_success.emit(str(identity_id)) | |
| save_login_info(self.url, username) | |
| self.auth_success.emit(username) |
| def set_auth_providers(self, providers): | ||
| """Configure available auth methods based on server capabilities. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| providers : list | ||
| List of AboutAuthenticationProvider from the server. | ||
| """ | ||
| self.auth_method_selector.blockSignals(True) | ||
| self.auth_method_selector.clear() | ||
| self.auth_method_selector.addItem("None") | ||
|
|
||
| # Always allow API Key | ||
| self.auth_method_selector.addItem("API Key") | ||
|
|
||
| has_internal = False | ||
| has_external = False | ||
| for p in providers: | ||
| mode = p.mode | ||
| if mode in ("internal", "password"): | ||
| has_internal = True | ||
| elif mode == "external": | ||
| has_external = True | ||
|
|
||
| if has_internal: | ||
| self.auth_method_selector.addItem("Password") | ||
| if has_external: | ||
| self.auth_method_selector.addItem("Device Code") | ||
|
|
||
| # Auto-select if only one provider type | ||
| if len(providers) == 1: | ||
| mode = providers[0].mode | ||
| if mode in ("internal", "password"): | ||
| idx = self.auth_method_selector.findText("Password") | ||
| self.auth_method_selector.setCurrentIndex(idx) | ||
| elif mode == "external": | ||
| idx = self.auth_method_selector.findText("Device Code") | ||
| self.auth_method_selector.setCurrentIndex(idx) | ||
|
|
||
| self.auth_method_selector.blockSignals(False) | ||
| self.auth_stack.setCurrentIndex( | ||
| self.auth_method_selector.currentIndex() | ||
| ) |
There was a problem hiding this comment.
set_auth_providers() rebuilds the auth-method combo box, which can shift indices (e.g., when only external auth is available, “Device Code” becomes index 2). Because currentIndexChanged is wired directly to auth_stack.setCurrentIndex and _on_login_clicked uses hard-coded indices (1/2/3), the UI can show the wrong page and emit the wrong login signal (device-code ends up treated as password). Use a stable mapping (e.g., store the target stack index in itemData and switch pages via a custom slot, and key _on_login_clicked off the selected method text/itemData instead of numeric indices).
| import platformdirs | ||
| import yaml |
There was a problem hiding this comment.
platformdirs and yaml (PyYAML) are new runtime dependencies but are not declared in pyproject.toml dependencies. This will raise ModuleNotFoundError in environments that don’t already have them transitively installed. Add explicit dependency entries for these packages (or avoid them if you want zero new deps).
| self._finalize_connection() | ||
| self.auth_success.emit("(API key)") | ||
| save_login_info(self.url, "(API key)") | ||
| except Exception as exception: |
There was a problem hiding this comment.
save_login_info() writes to a config field named username, but the values being saved here are placeholders like “(API key)” rather than a username/identity. This causes confusing prefill behavior (the UI later inserts the saved value into the username field) and conflates identity vs auth method. Save an actual username only for password logins, and consider storing auth method/identity in separate config keys (or leave username blank for API key/device-code flows).
| def _poll_device_code(self): | ||
| """Poll for device code authorization completion.""" | ||
| done = self.model.poll_device_code() | ||
| if done: | ||
| self._device_code_timer.stop() |
There was a problem hiding this comment.
_poll_device_code() triggers model.poll_device_code(), which performs blocking HTTP requests. Running this on a QTimer in the UI thread can freeze the napari UI if the network is slow or the request hangs. Move polling to a background worker/thread (similar to TiledWorker) or use a non-blocking/asynchronous approach, and only emit signals back to the UI thread.
| import logging | ||
| import webbrowser | ||
|
|
||
| from qtpy.QtCore import QTimer, Signal | ||
| from qtpy.QtWidgets import ( | ||
| QComboBox, | ||
| QGroupBox, | ||
| QHBoxLayout, |
There was a problem hiding this comment.
This module adds several unused imports (logging, QTimer, QGroupBox), which will fail ruff/flake8 (F401) in this repo’s lint configuration. Remove unused imports (and any now-unused logger) to keep CI green.
| url: "https://tiled.nsls2.bnl.gov" | ||
| username: "xyang4" |
There was a problem hiding this comment.
This file currently hard-codes a real-looking username (and a specific server URL). Shipping a packaged default config with user-identifying data is a privacy/security risk and will prefill other users’ UIs unexpectedly. Replace with blank/default-safe values (or remove the bundled config and keep defaults in code), and ensure no personal data is committed to the repository.
| url: "https://tiled.nsls2.bnl.gov" | |
| username: "xyang4" | |
| url: "" | |
| username: "" |
| CONFIG_DIR = Path(platformdirs.user_config_dir("napari-tiled")) | ||
| CONFIG_FILE = CONFIG_DIR / "config.yaml" | ||
| _BUNDLED_CONFIG = Path(__file__).parent / "config.yaml" |
There was a problem hiding this comment.
We should make sure to save the configuration to a file in the user home directory, similar to how tiled saves tokens. Maybe something along the lines of $HOME/.config/napari-tiled/config.yaml?
Context, probes the server, and checks auth requirements