Commit 2025b0ac authored by Duggan, John's avatar Duggan, John
Browse files

Merge branch...

Merge branch '90-add-flag-to-allow-dataselector-to-browse-user-directories-if-available' into 'main'

Add flag to allow DataSelector to browse user directories if available

Closes #90

See merge request ndip/public-packages/nova-trame!65
parents efca40d7 a0f393c4
Loading
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
### nova-trame, 0.20.1

* DataSelector now supports a `show_user_directories` flag that will allow users to choose datafiles from user directories (thanks to John Duggan).

### nova-trame, 0.20.0

* Three new components are available: ExecutionButtons, ProgressBar, and ToolOutputWindows. These components allow you to quickly add widgets to your UI for running and monitoring jobs (thanks to Sergey Yakubov).
+1 −1
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ Changelog = "https://code.ornl.gov/ndip/public-packages/nova-trame/blob/main/CHA

[tool.poetry]
name = "nova-trame"
version = "0.20.0"
version = "0.20.1"
description = "A Python Package for injecting curated themes and custom components into Trame applications"
authors = ["Duggan, John <dugganjw@ornl.gov>"]
readme = "README.md"
+39 −18
Original line number Diff line number Diff line
@@ -54,23 +54,17 @@ INSTRUMENTS = {
}


def get_facilities() -> List[str]:
    return list(INSTRUMENTS.keys())


def get_instruments(facility: str) -> List[str]:
    return list(INSTRUMENTS.get(facility, {}).keys())


class DataSelectorState(BaseModel, validate_assignment=True):
    """Selection state for identifying datafiles."""

    facility: str = Field(default="", title="Facility")
    instrument: str = Field(default="", title="Instrument")
    experiment: str = Field(default="", title="Experiment")
    user_directory: str = Field(default="", title="User Directory")
    directory: str = Field(default="")
    extensions: List[str] = Field(default=[])
    prefix: str = Field(default="")
    show_user_directories: bool = Field(default=False)

    @field_validator("experiment", mode="after")
    @classmethod
@@ -81,11 +75,11 @@ class DataSelectorState(BaseModel, validate_assignment=True):

    @model_validator(mode="after")
    def validate_state(self) -> Self:
        valid_facilities = get_facilities()
        valid_facilities = self.get_facilities()
        if self.facility and self.facility not in valid_facilities:
            warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)

        valid_instruments = get_instruments(self.facility)
        valid_instruments = self.get_instruments()
        if self.instrument and self.instrument not in valid_instruments:
            warn(
                (
@@ -98,25 +92,37 @@ class DataSelectorState(BaseModel, validate_assignment=True):

        return self

    def get_facilities(self) -> List[str]:
        facilities = list(INSTRUMENTS.keys())
        if self.show_user_directories:
            facilities.append("User Directory")
        return facilities

    def get_instruments(self) -> List[str]:
        return list(INSTRUMENTS.get(self.facility, {}).keys())


class DataSelectorModel:
    """Manages file system interactions for the DataSelector widget."""

    def __init__(self, facility: str, instrument: str, extensions: List[str], prefix: str) -> None:
    def __init__(
        self, facility: str, instrument: str, extensions: List[str], prefix: str, show_user_directories: bool
    ) -> None:
        self.state = DataSelectorState()
        self.state.facility = facility
        self.state.instrument = instrument
        self.state.extensions = extensions
        self.state.prefix = prefix
        self.state.show_user_directories = show_user_directories

    def get_facilities(self) -> List[str]:
        return sorted(get_facilities())
        return sorted(self.state.get_facilities())

    def get_instrument_dir(self) -> str:
        return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")

    def get_instruments(self) -> List[str]:
        return sorted(get_instruments(self.state.facility))
        return sorted(self.state.get_instruments())

    def get_experiments(self) -> List[str]:
        experiments = []
@@ -142,17 +148,32 @@ class DataSelectorModel:

        return sorted_dirs

    def get_directories(self) -> List[Any]:
    def get_experiment_directory_path(self) -> Optional[Path]:
        if not self.state.experiment:
            return None

        return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment

    def get_user_directory_path(self) -> Optional[Path]:
        if not self.state.user_directory:
            return None

        return Path("/SNS/users") / self.state.user_directory

    def get_directories(self) -> List[str]:
        if self.state.facility == "User Directory":
            base_path = self.get_user_directory_path()
        else:
            base_path = self.get_experiment_directory_path()

        if not base_path:
            return []

        directories = []

        experiment_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
        try:
            for dirpath, _, _ in os.walk(experiment_path):
            for dirpath, _, _ in os.walk(base_path):
                # Get the relative path from the start path
                path_parts = os.path.relpath(dirpath, experiment_path).split(os.sep)
                path_parts = os.path.relpath(dirpath, base_path).split(os.sep)

                # Only create a new entry for top-level directories
                if len(path_parts) == 1 and path_parts[0] != ".":  # This indicates a top-level directory
+20 −3
Original line number Diff line number Diff line
"""View Implementation for DataSelector."""

from typing import Any, List, Optional, cast
from warnings import warn

from trame.app import get_server
from trame.widgets import client, html
@@ -27,6 +28,7 @@ class DataSelector(vuetify.VDataTableVirtual):
        extensions: Optional[List[str]] = None,
        prefix: str = "",
        select_strategy: str = "all",
        show_user_directories: bool = False,
        **kwargs: Any,
    ) -> None:
        """Constructor for DataSelector.
@@ -48,6 +50,9 @@ class DataSelector(vuetify.VDataTableVirtual):
        select_strategy : str, optional
            The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
            If unset, the `all` strategy will be used.
        show_user_directories : bool, optional
            Whether or not to allow users to select data files from user directories. Ignored if the facility parameter
            is set.
        **kwargs
            All other arguments will be passed to the underlying
            `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
@@ -64,10 +69,15 @@ class DataSelector(vuetify.VDataTableVirtual):
        else:
            self._label = None

        if facility and show_user_directories:
            warn("show_user_directories will be ignored since the facility parameter is set.", stacklevel=1)

        self._v_model = v_model
        self._v_model_name_in_state = v_model.split(".")[0]
        self._extensions = extensions if extensions is not None else []
        self._prefix = prefix
        self._select_strategy = select_strategy
        self._show_user_directories = show_user_directories

        self._state_name = f"nova__dataselector_{self._next_id}_state"
        self._facilities_name = f"nova__dataselector_{self._next_id}_facilities"
@@ -76,7 +86,7 @@ class DataSelector(vuetify.VDataTableVirtual):
        self._directories_name = f"nova__dataselector_{self._next_id}_directories"
        self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"

        self._flush_state = f"flushState('{self._v_model.split('.')[0]}');"
        self._flush_state = f"flushState('{self._v_model_name_in_state}');"
        self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec

        self.create_model(facility, instrument)
@@ -96,14 +106,19 @@ class DataSelector(vuetify.VDataTableVirtual):
                if instrument == "":
                    columns -= 1
                    InputField(
                        v_model=f"{self._state_name}.instrument", items=(self._instruments_name,), type="autocomplete"
                        v_if=f"{self._state_name}.facility !== 'User Directory'",
                        v_model=f"{self._state_name}.instrument",
                        items=(self._instruments_name,),
                        type="autocomplete",
                    )
                InputField(
                    v_if=f"{self._state_name}.facility !== 'User Directory'",
                    v_model=f"{self._state_name}.experiment",
                    column_span=columns,
                    items=(self._experiments_name,),
                    type="autocomplete",
                )
                InputField(v_else=True, v_model=f"{self._state_name}.user_directory", column_span=2)

            with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
                if not self._prefix:
@@ -155,7 +170,9 @@ class DataSelector(vuetify.VDataTableVirtual):
                    )

    def create_model(self, facility: str, instrument: str) -> None:
        self._model = DataSelectorModel(facility, instrument, self._extensions, self._prefix)
        self._model = DataSelectorModel(
            facility, instrument, self._extensions, self._prefix, self._show_user_directories
        )

    def create_viewmodel(self) -> None:
        server = get_server(None, client_type="vue3")
+10 −4
Original line number Diff line number Diff line
@@ -29,17 +29,23 @@ class DataSelectorViewModel:
        self.model.set_state(facility, instrument, experiment)
        self.update_view()

    def reset(self) -> None:
        self.model.set_directory("")
        self.reset_bind.update_in_view(None)

    def on_state_updated(self, results: Dict[str, Any]) -> None:
        for update in results.get("updated", []):
            match update:
                case "facility":
                    self.model.set_state(facility=None, instrument="", experiment="")
                    self.model.set_directory("")
                    self.reset_bind.update_in_view(None)
                    self.reset()
                case "instrument":
                    self.model.set_state(facility=None, instrument=None, experiment="")
                    self.model.set_directory("")
                    self.reset_bind.update_in_view(None)
                    self.reset()
                case "experiment":
                    self.reset()
                case "user_directory":
                    self.reset()
        self.update_view()

    def update_view(self) -> None: