Skip to content
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- **Navigator: DAQ system browser, cloud dataset id, Assign to workspace,
and faster DAQ system loading** (mirroring VH-Lab/NDI-matlab#1058).
"Session > DAQ Systems..." opens the new `ndi.gui.nav.DaqSystemBrowser`, a
read-only list of a session's DAQ systems with, for the selected one, its
reader, file navigator and file patterns, epoch probe map class and
patterns, metadata readers, and -- on "Load epochs" -- an epoch's actual
files, the pattern each matches and which is the epoch probe map. "Check
Cloud status" now opens `ndi.gui.nav.CloudStatusDialog`, which shows the
NDI Cloud dataset id with a Copy button (disabled when the dataset is not
in the cloud). Dataset and session nodes gain "Assign to workspace...",
which binds the object to a variable in `__main__` and relabels the tree in
place. `session.daqsystem_load(name='(.*)')` (and any match-everything
name) no longer sends a regexp to the database -- on a ~100,000-document
database that halved the load time -- via the new
`ndi_session.isMatchAllRegexp`; probe epoch tables, `getprobes` and the sync
graph, which all load DAQ systems that way, benefit too.

- **Bulk document upload, and waiting for it to be ingested** (mirroring
VH-Lab/NDI-matlab#1047). `ndi.cloud.upload.uploadDocumentCollection` now
uploads documents in bulk, as NDI-matlab does: each chunk
Expand Down Expand Up @@ -118,6 +135,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Fixed

- **`session.daqsystem_add` raised AttributeError for every DAQ system**: it
called `dev.setsession`, but `ndi_daq_system`'s method is `set_session`.

- **Deleting a document left its dependents behind with current DID-python.**
`session.database_rm`'s cascade asked for `depends_on("", id)`, relying on
an empty name meaning "any"; DID's SQL `depends_on` search (DID-python#101)
treats only `'*'` as any, so nothing was found and, e.g., deleting neurons
in the spike sorter importer left them in the database. It now asks for
`'*'`, as MATLAB's `findalldependencies` does.

- **Lightsheet viewer: slow tiles from the local cache.** Each pyramid level was
built as one dask graph layer per chunk (`from_delayed` + `da.block`), and
dask walks every layer each time napari computes a slice, so the cost grew
Expand Down
6 changes: 5 additions & 1 deletion src/ndi/daq/ndi_matlab_python_bridge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -756,10 +756,14 @@ classes:
- name: system
type: class
matlab_path: "+ndi/+daq/system.m"
matlab_last_sync_hash: "fe64a9f5"
matlab_last_sync_hash: "e066e8d"
python_path: "ndi/daq/system.py"
python_class: "ndi_daq_system"
inherits: "ndi.ido"
decision_log: >
2026-10-24: NDI-matlab 989a620 added an optional prefetched-documents
input to the constructor and e066e8d reverted it (VH-Lab/NDI-matlab#1058);
the file is unchanged from fe64a9f5 in net, so nothing to follow.

properties:
- name: name
Expand Down
4 changes: 4 additions & 0 deletions src/ndi/gui/nav/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

from . import datasets_cloud, datasets_model, datasets_text
from .cloud_pane import CloudPane
from .cloud_status_dialog import CloudStatusDialog
from .daq_system_browser import DaqSystemBrowser
from .datasets_pane import DatasetsPane
from .ndi_pane import NdiPane
from .pane import NavPane
Expand All @@ -35,6 +37,8 @@
"datasets_model",
"datasets_text",
"CloudPane",
"CloudStatusDialog",
"DaqSystemBrowser",
"DatasetsPane",
"NavPane",
"NdiPane",
Expand Down
151 changes: 151 additions & 0 deletions src/ndi/gui/nav/cloud_status_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""ndi.gui.nav.cloud_status_dialog - the result window of "Check Cloud status".

MATLAB counterpart: ``src/ndi/+ndi/+gui/+nav/cloudStatusDialog.m``

Reports whether a dataset is linked to NDI Cloud. It shows the status
sentence, the NDI Cloud dataset id in a read-only (selectable) field, and a
"Copy" button that puts the id on the clipboard. When the dataset is not in
the cloud there is no id: the field reads "(not in NDI Cloud)" and Copy is
disabled.

What the window SAYS -- the sentence, the id text, whether Copy is enabled --
is decided by the static methods, which need no display and are tested on
their own; :meth:`CloudStatusDialog.build` only lays the widgets out.
"""

from __future__ import annotations

from typing import Any

from ..cloud_colors import cloud_colors, rgb_to_hex

__all__ = ["CloudStatusDialog"]

#: Shown in the id field when there is no id to show.
NOT_IN_CLOUD_TEXT = "(not in NDI Cloud)"


class CloudStatusDialog:
"""The "Check Cloud status" result window for one dataset."""

def __init__(
self,
in_cloud: bool,
cloud_dataset_id: str = "",
*,
parent: Any = None,
build: bool = True,
):
self.in_cloud = bool(in_cloud)
self.cloud_dataset_id = str(cloud_dataset_id or "")
self.figure: Any = None
self.id_field: Any = None
self.copy_button: Any = None
if build:
self.build(parent)

# ------------------------------------------------------------------
# what the window says -- no Qt
# ------------------------------------------------------------------
@staticmethod
def status_message(in_cloud: bool) -> str:
"""The status sentence shown at the top of the window."""
if in_cloud:
return "This dataset is linked to NDI Cloud."
return 'This dataset is not in NDI Cloud. Use "Upload to Cloud" to add it.'

@staticmethod
def can_copy(in_cloud: bool, cloud_dataset_id: str) -> bool:
"""True when there is a cloud dataset id to copy."""
return bool(in_cloud) and bool(cloud_dataset_id)

@staticmethod
def copy_enable(in_cloud: bool, cloud_dataset_id: str) -> bool:
"""Whether the Copy button is enabled."""
return CloudStatusDialog.can_copy(in_cloud, cloud_dataset_id)

@staticmethod
def id_text(in_cloud: bool, cloud_dataset_id: str) -> str:
"""Text shown in the id field."""
if CloudStatusDialog.can_copy(in_cloud, cloud_dataset_id):
return str(cloud_dataset_id)
return NOT_IN_CLOUD_TEXT

# ------------------------------------------------------------------
# Qt
# ------------------------------------------------------------------
def build(self, parent: Any = None) -> Any:
"""Create the window: message, id row with Copy, and OK."""
from .._qt_helpers import get_or_create_app, require_qt

require_qt()
get_or_create_app()
from PySide6 import QtWidgets

c = cloud_colors()
navy = rgb_to_hex(c.dark_blue)
white = rgb_to_hex(c.white)
light = rgb_to_hex(c.light_blue)

self.figure = QtWidgets.QDialog(parent)
self.figure.setWindowTitle("Check Cloud Status")
self.figure.setObjectName("ndiNavigatorCloudStatus")
self.figure.setStyleSheet(f"QDialog {{ background-color: {navy}; }}")
self.figure.resize(460, 170)

layout = QtWidgets.QGridLayout(self.figure)
layout.setContentsMargins(12, 12, 12, 12)
layout.setVerticalSpacing(10)

message = QtWidgets.QLabel(self.status_message(self.in_cloud))
message.setWordWrap(True)
message.setStyleSheet(f"color: {white};")
layout.addWidget(message, 0, 0, 1, 3)

label = QtWidgets.QLabel("Cloud dataset ID:")
label.setStyleSheet(f"color: {white}; font-weight: bold;")
layout.addWidget(label, 1, 0)

# A read-only line edit rather than a label, so the id can also be
# selected and copied by hand.
self.id_field = QtWidgets.QLineEdit(self.id_text(self.in_cloud, self.cloud_dataset_id))
self.id_field.setReadOnly(True)
self.id_field.setObjectName("ndiCloudDatasetId")
self.id_field.setStyleSheet(f"background-color: {white}; color: {navy};")
layout.addWidget(self.id_field, 1, 1)

self.copy_button = QtWidgets.QPushButton("Copy")
self.copy_button.setObjectName("ndiCloudDatasetIdCopy")
self.copy_button.setToolTip("Copy the cloud dataset ID to the clipboard")
self.copy_button.setEnabled(self.copy_enable(self.in_cloud, self.cloud_dataset_id))
self.copy_button.setStyleSheet(f"background-color: {light}; color: {navy};")
self.copy_button.clicked.connect(self.copy_id)
layout.addWidget(self.copy_button, 1, 2)

ok = QtWidgets.QPushButton("OK")
ok.setStyleSheet(f"background-color: {white}; color: {navy};")
ok.clicked.connect(self.figure.accept)
layout.addWidget(ok, 2, 2)
layout.setRowStretch(0, 1)
return self.figure

def copy_id(self) -> bool:
"""Copy the cloud dataset id to the clipboard. False when there is none."""
if not self.can_copy(self.in_cloud, self.cloud_dataset_id):
return False
from PySide6 import QtGui

QtGui.QGuiApplication.clipboard().setText(self.cloud_dataset_id)
if self.copy_button is not None:
self.copy_button.setText("Copied")
return True

def show(self) -> None:
if self.figure is not None:
self.figure.show()

def __repr__(self) -> str:
return (
f"CloudStatusDialog(in_cloud={self.in_cloud!r}, "
f"cloud_dataset_id={self.cloud_dataset_id!r})"
)
Loading
Loading