nova-trame 0.23.0__py3-none-any.whl → 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nova/trame/_internal/utils.py +30 -0
- nova/trame/model/data_selector.py +14 -10
- nova/trame/model/ornl/neutron_data_selector.py +13 -23
- nova/trame/view/components/data_selector.py +148 -25
- nova/trame/view/components/ornl/neutron_data_selector.py +152 -53
- nova/trame/view_model/data_selector.py +16 -4
- nova/trame/view_model/ornl/neutron_data_selector.py +5 -17
- {nova_trame-0.23.0.dist-info → nova_trame-0.24.0.dist-info}/METADATA +1 -1
- {nova_trame-0.23.0.dist-info → nova_trame-0.24.0.dist-info}/RECORD +12 -11
- {nova_trame-0.23.0.dist-info → nova_trame-0.24.0.dist-info}/LICENSE +0 -0
- {nova_trame-0.23.0.dist-info → nova_trame-0.24.0.dist-info}/WHEEL +0 -0
- {nova_trame-0.23.0.dist-info → nova_trame-0.24.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
"""Internal utilities for nova-trame."""
|
2
|
+
|
3
|
+
from typing import Any, Tuple, Union
|
4
|
+
|
5
|
+
from trame_server.core import State
|
6
|
+
|
7
|
+
from nova.mvvm._internal.utils import rgetdictvalue, rsetdictvalue
|
8
|
+
|
9
|
+
|
10
|
+
# Reads a state parameter from Trame. For internal use only, if you're using this in your application you're violating
|
11
|
+
# the MVVM framework. :)
|
12
|
+
def get_state_param(state: State, value: Union[Any, Tuple]) -> Any:
|
13
|
+
if isinstance(value, tuple):
|
14
|
+
return rgetdictvalue(state, value[0])
|
15
|
+
|
16
|
+
return value
|
17
|
+
|
18
|
+
|
19
|
+
# Writes a state parameter to Trame. For internal use only, if you're using this in your application you're violating
|
20
|
+
# the MVVM framework. :)
|
21
|
+
def set_state_param(state: State, value: Union[Any, Tuple], new_value: Any = None) -> Any:
|
22
|
+
with state:
|
23
|
+
if isinstance(value, tuple):
|
24
|
+
if new_value is not None:
|
25
|
+
rsetdictvalue(state, value[0], new_value)
|
26
|
+
elif len(value) > 1:
|
27
|
+
rsetdictvalue(state, value[0], value[1])
|
28
|
+
state.dirty(value[0].split(".")[0])
|
29
|
+
|
30
|
+
return get_state_param(state, value)
|
@@ -12,20 +12,23 @@ class DataSelectorState(BaseModel, validate_assignment=True):
|
|
12
12
|
"""Selection state for identifying datafiles."""
|
13
13
|
|
14
14
|
directory: str = Field(default="")
|
15
|
-
subdirectory: str = Field(default="")
|
16
15
|
extensions: List[str] = Field(default=[])
|
17
|
-
|
16
|
+
subdirectory: str = Field(default="")
|
18
17
|
|
19
18
|
|
20
19
|
class DataSelectorModel:
|
21
20
|
"""Manages file system interactions for the DataSelector widget."""
|
22
21
|
|
23
|
-
def __init__(self, state: DataSelectorState
|
22
|
+
def __init__(self, state: DataSelectorState) -> None:
|
24
23
|
self.state: DataSelectorState = state
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
26
|
+
if "directory" in kwargs:
|
27
|
+
self.state.directory = kwargs["directory"]
|
28
|
+
if "extensions" in kwargs:
|
29
|
+
self.state.extensions = kwargs["extensions"]
|
30
|
+
if "subdirectory" in kwargs:
|
31
|
+
self.state.subdirectory = kwargs["subdirectory"]
|
29
32
|
|
30
33
|
def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
31
34
|
# Sort the current level of dictionaries
|
@@ -47,6 +50,10 @@ class DataSelectorModel:
|
|
47
50
|
|
48
51
|
if len(path_parts) > 1:
|
49
52
|
dirs.clear()
|
53
|
+
elif path_parts != ["."]:
|
54
|
+
# Subdirectories are fully queried upon being opened, so we only need to query one item to determine
|
55
|
+
# if the target directory has any children.
|
56
|
+
dirs[:] = dirs[:1]
|
50
57
|
|
51
58
|
# Only create a new entry for top-level directories
|
52
59
|
if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
|
@@ -85,10 +92,7 @@ class DataSelectorModel:
|
|
85
92
|
def get_datafiles_from_path(self, base_path: Path) -> List[str]:
|
86
93
|
datafiles = []
|
87
94
|
try:
|
88
|
-
|
89
|
-
datafile_path = base_path / self.state.prefix
|
90
|
-
else:
|
91
|
-
datafile_path = base_path / self.state.subdirectory
|
95
|
+
datafile_path = base_path / self.state.subdirectory
|
92
96
|
|
93
97
|
for entry in os.scandir(datafile_path):
|
94
98
|
if entry.is_file():
|
@@ -107,23 +107,21 @@ class NeutronDataSelectorState(DataSelectorState):
|
|
107
107
|
class NeutronDataSelectorModel(DataSelectorModel):
|
108
108
|
"""Manages file system interactions for the DataSelector widget."""
|
109
109
|
|
110
|
-
def __init__(
|
111
|
-
|
112
|
-
state: NeutronDataSelectorState,
|
113
|
-
facility: str,
|
114
|
-
instrument: str,
|
115
|
-
extensions: List[str],
|
116
|
-
prefix: str,
|
117
|
-
allow_custom_directories: bool,
|
118
|
-
) -> None:
|
119
|
-
super().__init__(state, "", extensions, prefix)
|
110
|
+
def __init__(self, state: NeutronDataSelectorState) -> None:
|
111
|
+
super().__init__(state)
|
120
112
|
self.state: NeutronDataSelectorState = state
|
121
113
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
114
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
115
|
+
super().set_binding_parameters(**kwargs)
|
116
|
+
|
117
|
+
if "facility" in kwargs:
|
118
|
+
self.state.facility = kwargs["facility"]
|
119
|
+
if "instrument" in kwargs:
|
120
|
+
self.state.instrument = kwargs["instrument"]
|
121
|
+
if "experiment" in kwargs:
|
122
|
+
self.state.experiment = kwargs["experiment"]
|
123
|
+
if "allow_custom_directories" in kwargs:
|
124
|
+
self.state.allow_custom_directories = kwargs["allow_custom_directories"]
|
127
125
|
|
128
126
|
def get_facilities(self) -> List[str]:
|
129
127
|
return natsorted(self.state.get_facilities())
|
@@ -183,11 +181,3 @@ class NeutronDataSelectorModel(DataSelectorModel):
|
|
183
181
|
return []
|
184
182
|
|
185
183
|
return self.get_datafiles_from_path(base_path)
|
186
|
-
|
187
|
-
def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
|
188
|
-
if facility is not None:
|
189
|
-
self.state.facility = facility
|
190
|
-
if instrument is not None:
|
191
|
-
self.state.instrument = instrument
|
192
|
-
if experiment is not None:
|
193
|
-
self.state.experiment = experiment
|
@@ -1,14 +1,19 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from asyncio import ensure_future, sleep
|
4
|
+
from typing import Any, List, Tuple, Union, cast
|
5
|
+
from warnings import warn
|
4
6
|
|
5
7
|
from trame.app import get_server
|
6
8
|
from trame.widgets import client, datagrid, html
|
7
9
|
from trame.widgets import vuetify3 as vuetify
|
10
|
+
from trame_server.core import State
|
8
11
|
|
12
|
+
from nova.mvvm._internal.utils import rgetdictvalue
|
9
13
|
from nova.mvvm.trame_binding import TrameBinding
|
14
|
+
from nova.trame._internal.utils import get_state_param, set_state_param
|
10
15
|
from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
|
11
|
-
from nova.trame.view.layouts import GridLayout, VBoxLayout
|
16
|
+
from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
|
12
17
|
from nova.trame.view_model.data_selector import DataSelectorViewModel
|
13
18
|
|
14
19
|
from .input_field import InputField
|
@@ -21,29 +26,36 @@ class DataSelector(datagrid.VGrid):
|
|
21
26
|
|
22
27
|
def __init__(
|
23
28
|
self,
|
24
|
-
v_model: str,
|
25
|
-
directory: str,
|
26
|
-
extensions:
|
27
|
-
prefix: str = "",
|
28
|
-
|
29
|
+
v_model: Union[str, Tuple],
|
30
|
+
directory: Union[str, Tuple],
|
31
|
+
extensions: Union[List[str], Tuple, None] = None,
|
32
|
+
prefix: Union[str, Tuple] = "",
|
33
|
+
subdirectory: Union[str, Tuple] = "",
|
34
|
+
refresh_rate: Union[int, Tuple] = 30,
|
35
|
+
select_strategy: Union[str, Tuple] = "all",
|
29
36
|
**kwargs: Any,
|
30
37
|
) -> None:
|
31
38
|
"""Constructor for DataSelector.
|
32
39
|
|
33
40
|
Parameters
|
34
41
|
----------
|
35
|
-
v_model : str
|
42
|
+
v_model : Union[str, Tuple]
|
36
43
|
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
37
44
|
selected by the user.
|
38
|
-
directory : str
|
45
|
+
directory : Union[str, Tuple]
|
39
46
|
The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
|
40
47
|
users.
|
41
|
-
extensions : List[str], optional
|
48
|
+
extensions : Union[List[str], Tuple], optional
|
42
49
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
43
|
-
prefix : str, optional
|
44
|
-
|
45
|
-
|
46
|
-
|
50
|
+
prefix : Union[str, Tuple], optional
|
51
|
+
Deprecated. Please refer to the `subdirectory` parameter.
|
52
|
+
subdirectory : Union[str, Tuple], optional
|
53
|
+
A subdirectory within the selected top-level folder to show files. If not specified as a string, the user
|
54
|
+
will be shown a folder browser and will be able to see all files in the selected top-level folder.
|
55
|
+
refresh_rate : Union[int, Tuple], optional
|
56
|
+
The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
|
57
|
+
feature. Defaults to 30 seconds.
|
58
|
+
select_strategy : Union[str, Tuple], optional
|
47
59
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
48
60
|
If unset, the `all` strategy will be used.
|
49
61
|
**kwargs
|
@@ -68,11 +80,29 @@ class DataSelector(datagrid.VGrid):
|
|
68
80
|
else:
|
69
81
|
self._label = None
|
70
82
|
|
83
|
+
if prefix:
|
84
|
+
warn(
|
85
|
+
"The prefix parameter has been deprecated. Please switch to using the subdirectory parameter.",
|
86
|
+
category=DeprecationWarning,
|
87
|
+
stacklevel=1,
|
88
|
+
)
|
89
|
+
|
90
|
+
if not subdirectory:
|
91
|
+
subdirectory = prefix
|
92
|
+
|
71
93
|
self._v_model = v_model
|
72
|
-
|
94
|
+
if isinstance(v_model, str):
|
95
|
+
self._v_model_name_in_state = v_model.split(".")[0]
|
96
|
+
else:
|
97
|
+
self._v_model_name_in_state = v_model[0].split(".")[0]
|
98
|
+
|
73
99
|
self._directory = directory
|
100
|
+
self._last_directory = get_state_param(self.state, self._directory)
|
74
101
|
self._extensions = extensions if extensions is not None else []
|
75
|
-
self.
|
102
|
+
self._last_extensions = get_state_param(self.state, self._extensions)
|
103
|
+
self._subdirectory = subdirectory
|
104
|
+
self._last_subdirectory = get_state_param(self.state, self._subdirectory)
|
105
|
+
self._refresh_rate = refresh_rate
|
76
106
|
self._select_strategy = select_strategy
|
77
107
|
|
78
108
|
self._revogrid_id = f"nova__dataselector_{self._next_id}_rv"
|
@@ -86,17 +116,30 @@ class DataSelector(datagrid.VGrid):
|
|
86
116
|
).exec
|
87
117
|
self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
|
88
118
|
|
89
|
-
self.
|
90
|
-
self.
|
119
|
+
self._create_model()
|
120
|
+
self._create_viewmodel()
|
121
|
+
self._setup_bindings()
|
91
122
|
|
92
123
|
self.create_ui(**kwargs)
|
93
124
|
|
125
|
+
ensure_future(self._refresh_loop())
|
126
|
+
|
127
|
+
@property
|
128
|
+
def state(self) -> State:
|
129
|
+
return get_server(None, client_type="vue3").state
|
130
|
+
|
94
131
|
def create_ui(self, *args: Any, **kwargs: Any) -> None:
|
95
132
|
with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
|
96
|
-
|
133
|
+
with HBoxLayout(valign="center"):
|
134
|
+
self._layout.filter = html.Div(classes="flex-1-1")
|
135
|
+
with vuetify.VBtn(
|
136
|
+
classes="mx-1", density="compact", icon=True, variant="text", click=self.refresh_contents
|
137
|
+
):
|
138
|
+
vuetify.VIcon("mdi-refresh")
|
139
|
+
vuetify.VTooltip("Refresh Contents", activator="parent")
|
97
140
|
|
98
141
|
with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
|
99
|
-
if not self.
|
142
|
+
if isinstance(self._subdirectory, tuple) or not self._subdirectory:
|
100
143
|
with html.Div(classes="d-flex flex-column h-100 overflow-hidden"):
|
101
144
|
vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
|
102
145
|
vuetify.VTreeview(
|
@@ -108,7 +151,7 @@ class DataSelector(datagrid.VGrid):
|
|
108
151
|
item_value="path",
|
109
152
|
items=(self._directories_name,),
|
110
153
|
click_open=(self._vm.expand_directory, "[$event.path]"),
|
111
|
-
update_activated=(self.
|
154
|
+
update_activated=(self.set_subdirectory, "$event"),
|
112
155
|
)
|
113
156
|
vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
|
114
157
|
|
@@ -125,6 +168,7 @@ class DataSelector(datagrid.VGrid):
|
|
125
168
|
" prop: 'title',"
|
126
169
|
"}]",
|
127
170
|
),
|
171
|
+
column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
|
128
172
|
frame_size=10,
|
129
173
|
hide_attribution=True,
|
130
174
|
id=self._revogrid_id,
|
@@ -169,11 +213,11 @@ class DataSelector(datagrid.VGrid):
|
|
169
213
|
f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
|
170
214
|
)
|
171
215
|
|
172
|
-
def
|
216
|
+
def _create_model(self) -> None:
|
173
217
|
state = DataSelectorState()
|
174
|
-
self._model = DataSelectorModel(state
|
218
|
+
self._model = DataSelectorModel(state)
|
175
219
|
|
176
|
-
def
|
220
|
+
def _create_viewmodel(self) -> None:
|
177
221
|
server = get_server(None, client_type="vue3")
|
178
222
|
binding = TrameBinding(server.state)
|
179
223
|
|
@@ -183,14 +227,93 @@ class DataSelector(datagrid.VGrid):
|
|
183
227
|
self._vm.datafiles_bind.connect(self._datafiles_name)
|
184
228
|
self._vm.reset_bind.connect(self.reset)
|
185
229
|
|
186
|
-
|
230
|
+
def refresh_contents(self) -> None:
|
231
|
+
self._vm.update_view(refresh_directories=True)
|
187
232
|
|
188
233
|
def reset(self, _: Any = None) -> None:
|
189
234
|
self._reset_state()
|
190
235
|
self._reset_rv_grid()
|
191
236
|
|
237
|
+
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
238
|
+
set_state_param(self.state, self._subdirectory, subdirectory_path)
|
239
|
+
self._vm.set_subdirectory(subdirectory_path)
|
240
|
+
|
192
241
|
def set_state(self, *args: Any, **kwargs: Any) -> None:
|
193
242
|
raise TypeError(
|
194
243
|
"The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
|
195
244
|
"`nova.trame.view.components.ornl`."
|
196
245
|
)
|
246
|
+
|
247
|
+
# This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
|
248
|
+
# component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
|
249
|
+
# don't want bindings in the internal implementation as our callbacks could compete with the developer's.
|
250
|
+
def _setup_bindings(self) -> None:
|
251
|
+
# If the bindings were given initial values, write these to the state.
|
252
|
+
set_state_param(self.state, self._directory)
|
253
|
+
set_state_param(self.state, self._extensions)
|
254
|
+
set_state_param(self.state, self._subdirectory)
|
255
|
+
self._vm.set_binding_parameters(
|
256
|
+
directory=get_state_param(self.state, self._directory),
|
257
|
+
extensions=get_state_param(self.state, self._extensions),
|
258
|
+
subdirectory=get_state_param(self.state, self._subdirectory),
|
259
|
+
)
|
260
|
+
|
261
|
+
# The component used by this parameter will attempt to set the initial value itself, which will trigger the
|
262
|
+
# below change listeners causing unpredictable behavior.
|
263
|
+
if isinstance(self._subdirectory, tuple):
|
264
|
+
self._subdirectory = (self._subdirectory[0],)
|
265
|
+
|
266
|
+
# Now we set up the change listeners for all bound parameters. These are responsible for updating the component
|
267
|
+
# when other portions of the application manipulate these parameters.
|
268
|
+
if isinstance(self._directory, tuple):
|
269
|
+
|
270
|
+
@self.state.change(self._directory[0].split(".")[0])
|
271
|
+
def on_directory_change(**kwargs: Any) -> None:
|
272
|
+
directory = rgetdictvalue(kwargs, self._directory[0])
|
273
|
+
if directory != self._last_directory:
|
274
|
+
self._last_directory = directory
|
275
|
+
self._vm.set_binding_parameters(
|
276
|
+
directory=set_state_param(self.state, self._directory, directory),
|
277
|
+
)
|
278
|
+
|
279
|
+
if isinstance(self._extensions, tuple):
|
280
|
+
|
281
|
+
@self.state.change(self._extensions[0].split(".")[0])
|
282
|
+
def on_extensions_change(**kwargs: Any) -> None:
|
283
|
+
extensions = rgetdictvalue(kwargs, self._extensions[0])
|
284
|
+
if extensions != self._last_extensions:
|
285
|
+
self._last_extensions = extensions
|
286
|
+
self._vm.set_binding_parameters(
|
287
|
+
extensions=set_state_param(self.state, self._extensions, extensions),
|
288
|
+
)
|
289
|
+
|
290
|
+
if isinstance(self._subdirectory, tuple):
|
291
|
+
|
292
|
+
@self.state.change(self._subdirectory[0].split(".")[0])
|
293
|
+
def on_subdirectory_change(**kwargs: Any) -> None:
|
294
|
+
subdirectory = rgetdictvalue(kwargs, self._subdirectory[0])
|
295
|
+
if subdirectory != self._last_subdirectory:
|
296
|
+
self._last_subdirectory = subdirectory
|
297
|
+
self._vm.set_binding_parameters(
|
298
|
+
subdirectory=set_state_param(self.state, self._subdirectory, subdirectory),
|
299
|
+
)
|
300
|
+
|
301
|
+
async def _refresh_loop(self) -> None:
|
302
|
+
refresh_rate: int = set_state_param(self.state, self._refresh_rate)
|
303
|
+
skip = False
|
304
|
+
|
305
|
+
if refresh_rate > 0:
|
306
|
+
while True:
|
307
|
+
await sleep(refresh_rate)
|
308
|
+
if skip:
|
309
|
+
continue
|
310
|
+
|
311
|
+
self.refresh_contents()
|
312
|
+
self.state.dirty(self._datafiles_name)
|
313
|
+
|
314
|
+
try:
|
315
|
+
refresh_rate = int(get_state_param(self.state, self._refresh_rate))
|
316
|
+
skip = False
|
317
|
+
except TypeError:
|
318
|
+
refresh_rate = 1
|
319
|
+
skip = True
|
@@ -1,11 +1,12 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
|
-
from typing import Any, List,
|
3
|
+
from typing import Any, List, Tuple, Union
|
4
4
|
from warnings import warn
|
5
5
|
|
6
6
|
from trame.app import get_server
|
7
7
|
from trame.widgets import vuetify3 as vuetify
|
8
8
|
|
9
|
+
from nova.mvvm._internal.utils import rgetdictvalue
|
9
10
|
from nova.mvvm.trame_binding import TrameBinding
|
10
11
|
from nova.trame.model.ornl.neutron_data_selector import (
|
11
12
|
CUSTOM_DIRECTORIES_LABEL,
|
@@ -15,7 +16,7 @@ from nova.trame.model.ornl.neutron_data_selector import (
|
|
15
16
|
from nova.trame.view.layouts import GridLayout
|
16
17
|
from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
|
17
18
|
|
18
|
-
from ..data_selector import DataSelector
|
19
|
+
from ..data_selector import DataSelector, get_state_param, set_state_param
|
19
20
|
from ..input_field import InputField
|
20
21
|
|
21
22
|
vuetify.enable_lab()
|
@@ -26,35 +27,42 @@ class NeutronDataSelector(DataSelector):
|
|
26
27
|
|
27
28
|
def __init__(
|
28
29
|
self,
|
29
|
-
v_model: str,
|
30
|
-
allow_custom_directories: bool = False,
|
31
|
-
facility: str = "",
|
32
|
-
instrument: str = "",
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
v_model: Union[str, Tuple],
|
31
|
+
allow_custom_directories: Union[bool, Tuple] = False,
|
32
|
+
facility: Union[str, Tuple] = "",
|
33
|
+
instrument: Union[str, Tuple] = "",
|
34
|
+
experiment: Union[str, Tuple] = "",
|
35
|
+
extensions: Union[List[str], Tuple, None] = None,
|
36
|
+
subdirectory: Union[str, Tuple] = "",
|
37
|
+
refresh_rate: Union[int, Tuple] = 30,
|
38
|
+
select_strategy: Union[str, Tuple] = "all",
|
36
39
|
**kwargs: Any,
|
37
40
|
) -> None:
|
38
41
|
"""Constructor for DataSelector.
|
39
42
|
|
40
43
|
Parameters
|
41
44
|
----------
|
42
|
-
v_model : str
|
45
|
+
v_model : Union[str, Tuple]
|
43
46
|
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
44
47
|
selected by the user.
|
45
|
-
allow_custom_directories : bool, optional
|
48
|
+
allow_custom_directories : Union[bool, Tuple], optional
|
46
49
|
Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
|
47
50
|
facility parameter is set.
|
48
|
-
facility : str, optional
|
51
|
+
facility : Union[str, Tuple], optional
|
49
52
|
The facility to restrict data selection to. Options: HFIR, SNS
|
50
|
-
instrument : str, optional
|
53
|
+
instrument : Union[str, Tuple], optional
|
51
54
|
The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
|
52
|
-
|
55
|
+
experiment : Union[str, Tuple], optional
|
56
|
+
The experiment to restrict data selection to.
|
57
|
+
extensions : Union[List[str], Tuple], optional
|
53
58
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
54
|
-
|
55
|
-
A subdirectory within the user's chosen experiment to show files. If not specified, the user
|
56
|
-
folder browser and will be able to see all files in the experiment that they have access to.
|
57
|
-
|
59
|
+
subdirectory : Union[str, Tuple], optional
|
60
|
+
A subdirectory within the user's chosen experiment to show files. If not specified as a string, the user
|
61
|
+
will be shown a folder browser and will be able to see all files in the experiment that they have access to.
|
62
|
+
refresh_rate : Union[str, Tuple], optional
|
63
|
+
The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
|
64
|
+
feature. Defaults to 30 seconds.
|
65
|
+
select_strategy : Union[str, Tuple], optional
|
58
66
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
59
67
|
If unset, the `all` strategy will be used.
|
60
68
|
**kwargs
|
@@ -65,53 +73,76 @@ class NeutronDataSelector(DataSelector):
|
|
65
73
|
-------
|
66
74
|
None
|
67
75
|
"""
|
68
|
-
if facility and allow_custom_directories:
|
69
|
-
warn("allow_custom_directories will be ignored since the facility parameter is
|
76
|
+
if isinstance(facility, str) and allow_custom_directories:
|
77
|
+
warn("allow_custom_directories will be ignored since the facility parameter is fixed.", stacklevel=1)
|
70
78
|
|
71
79
|
self._facility = facility
|
72
80
|
self._instrument = instrument
|
81
|
+
self._experiment = experiment
|
73
82
|
self._allow_custom_directories = allow_custom_directories
|
83
|
+
self._last_allow_custom_directories = self._allow_custom_directories
|
74
84
|
|
85
|
+
self._state_name = f"nova__dataselector_{self._next_id}_state"
|
75
86
|
self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
|
87
|
+
self._selected_facility_name = (
|
88
|
+
self._facility[0] if isinstance(self._facility, tuple) else f"{self._state_name}.facility"
|
89
|
+
)
|
76
90
|
self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
|
91
|
+
self._selected_instrument_name = (
|
92
|
+
self._instrument[0] if isinstance(self._instrument, tuple) else f"{self._state_name}.instrument"
|
93
|
+
)
|
77
94
|
self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
|
95
|
+
self._selected_experiment_name = (
|
96
|
+
self._experiment[0] if isinstance(self._experiment, tuple) else f"{self._state_name}.experiment"
|
97
|
+
)
|
78
98
|
|
79
|
-
super().__init__(
|
99
|
+
super().__init__(
|
100
|
+
v_model,
|
101
|
+
"",
|
102
|
+
extensions=extensions,
|
103
|
+
subdirectory=subdirectory,
|
104
|
+
refresh_rate=refresh_rate,
|
105
|
+
select_strategy=select_strategy,
|
106
|
+
**kwargs,
|
107
|
+
)
|
80
108
|
|
81
109
|
def create_ui(self, **kwargs: Any) -> None:
|
82
110
|
super().create_ui(**kwargs)
|
83
111
|
with self._layout.filter:
|
84
112
|
with GridLayout(columns=3):
|
85
113
|
columns = 3
|
86
|
-
if self._facility
|
114
|
+
if isinstance(self._facility, tuple) or not self._facility:
|
87
115
|
columns -= 1
|
88
116
|
InputField(
|
89
|
-
v_model=
|
117
|
+
v_model=self._selected_facility_name,
|
118
|
+
items=(self._facilities_name,),
|
119
|
+
type="autocomplete",
|
120
|
+
update_modelValue=(self.update_facility, "[$event]"),
|
90
121
|
)
|
91
|
-
if self._instrument
|
122
|
+
if isinstance(self._instrument, tuple) or not self._instrument:
|
92
123
|
columns -= 1
|
93
124
|
InputField(
|
94
|
-
v_if=f"{self.
|
95
|
-
v_model=
|
125
|
+
v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
126
|
+
v_model=self._selected_instrument_name,
|
96
127
|
items=(self._instruments_name,),
|
97
128
|
type="autocomplete",
|
129
|
+
update_modelValue=(self.update_instrument, "[$event]"),
|
98
130
|
)
|
99
131
|
InputField(
|
100
|
-
v_if=f"{self.
|
101
|
-
v_model=
|
132
|
+
v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
133
|
+
v_model=self._selected_experiment_name,
|
102
134
|
column_span=columns,
|
103
135
|
items=(self._experiments_name,),
|
104
136
|
type="autocomplete",
|
137
|
+
update_modelValue=(self.update_experiment, "[$event]"),
|
105
138
|
)
|
106
139
|
InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
|
107
140
|
|
108
|
-
def
|
141
|
+
def _create_model(self) -> None:
|
109
142
|
state = NeutronDataSelectorState()
|
110
|
-
self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
|
111
|
-
state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
|
112
|
-
)
|
143
|
+
self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
|
113
144
|
|
114
|
-
def
|
145
|
+
def _create_viewmodel(self) -> None:
|
115
146
|
server = get_server(None, client_type="vue3")
|
116
147
|
binding = TrameBinding(server.state)
|
117
148
|
|
@@ -126,26 +157,94 @@ class NeutronDataSelector(DataSelector):
|
|
126
157
|
|
127
158
|
self._vm.update_view()
|
128
159
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
160
|
+
# This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
|
161
|
+
# component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
|
162
|
+
# don't want bindings in the internal implementation as our callbacks could compete with the developer's.
|
163
|
+
def _setup_bindings(self) -> None:
|
164
|
+
# If the bindings were given initial values, write these to the state.
|
165
|
+
set_state_param(self.state, self._facility)
|
166
|
+
set_state_param(self.state, self._instrument)
|
167
|
+
set_state_param(self.state, self._experiment)
|
168
|
+
set_state_param(self.state, self._allow_custom_directories)
|
169
|
+
self._last_facility = get_state_param(self.state, self._facility)
|
170
|
+
self._last_instrument = get_state_param(self.state, self._instrument)
|
171
|
+
self._last_experiment = get_state_param(self.state, self._experiment)
|
172
|
+
self._vm.set_binding_parameters(
|
173
|
+
facility=get_state_param(self.state, self._facility),
|
174
|
+
instrument=get_state_param(self.state, self._instrument),
|
175
|
+
experiment=get_state_param(self.state, self._experiment),
|
176
|
+
allow_custom_directories=get_state_param(self.state, self._allow_custom_directories),
|
177
|
+
)
|
133
178
|
|
134
|
-
|
179
|
+
# Now we set up the change listeners for all bound parameters. These are responsible for updating the component
|
180
|
+
# when other portions of the application manipulate these parameters.
|
181
|
+
if isinstance(self._facility, tuple):
|
182
|
+
|
183
|
+
@self.state.change(self._facility[0].split(".")[0])
|
184
|
+
def on_facility_change(**kwargs: Any) -> None:
|
185
|
+
facility = rgetdictvalue(kwargs, self._facility[0])
|
186
|
+
if facility != self._last_facility:
|
187
|
+
self._last_facility = facility
|
188
|
+
self._vm.set_binding_parameters(
|
189
|
+
facility=set_state_param(self.state, (self._selected_facility_name,), facility)
|
190
|
+
)
|
191
|
+
self._vm.reset()
|
135
192
|
|
136
|
-
|
137
|
-
----------
|
138
|
-
facility : str, optional
|
139
|
-
The facility to restrict data selection to. Options: HFIR, SNS
|
140
|
-
instrument : str, optional
|
141
|
-
The instrument to restrict data selection to. Must be at the selected facility.
|
142
|
-
experiment : str, optional
|
143
|
-
The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
|
144
|
-
that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
|
145
|
-
shown to the user.
|
193
|
+
if isinstance(self._instrument, tuple):
|
146
194
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
195
|
+
@self.state.change(self._instrument[0].split(".")[0])
|
196
|
+
def on_instrument_change(**kwargs: Any) -> None:
|
197
|
+
instrument = rgetdictvalue(kwargs, self._instrument[0])
|
198
|
+
if instrument != self._last_instrument:
|
199
|
+
self._last_instrument = instrument
|
200
|
+
self._vm.set_binding_parameters(
|
201
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument)
|
202
|
+
)
|
203
|
+
self._vm.reset()
|
204
|
+
|
205
|
+
if isinstance(self._experiment, tuple):
|
206
|
+
|
207
|
+
@self.state.change(self._experiment[0].split(".")[0])
|
208
|
+
def on_experiment_change(**kwargs: Any) -> None:
|
209
|
+
experiment = rgetdictvalue(kwargs, self._experiment[0])
|
210
|
+
if experiment != self._last_experiment:
|
211
|
+
self._last_experiment = experiment
|
212
|
+
self._vm.set_binding_parameters(
|
213
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment)
|
214
|
+
)
|
215
|
+
self._vm.reset()
|
216
|
+
|
217
|
+
if isinstance(self._allow_custom_directories, tuple):
|
218
|
+
|
219
|
+
@self.state.change(self._allow_custom_directories[0].split(".")[0])
|
220
|
+
def on_allow_custom_directories_change(**kwargs: Any) -> None:
|
221
|
+
allow_custom_directories = rgetdictvalue(kwargs, self._allow_custom_directories[0]) # type: ignore
|
222
|
+
if allow_custom_directories != self._last_allow_custom_directories:
|
223
|
+
self._last_allow_custom_directories = allow_custom_directories
|
224
|
+
self._vm.set_binding_parameters(
|
225
|
+
allow_custom_directories=set_state_param(
|
226
|
+
self.state, self._allow_custom_directories, allow_custom_directories
|
227
|
+
)
|
228
|
+
)
|
229
|
+
|
230
|
+
# These update methods notify the rest of the application when the component changes bound parameters.
|
231
|
+
def update_facility(self, facility: str) -> None:
|
232
|
+
self._vm.set_binding_parameters(
|
233
|
+
facility=set_state_param(self.state, (self._selected_facility_name,), facility),
|
234
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), ""), # Reset the instrument
|
235
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
|
236
|
+
)
|
237
|
+
self._vm.reset()
|
238
|
+
|
239
|
+
def update_instrument(self, instrument: str) -> None:
|
240
|
+
self._vm.set_binding_parameters(
|
241
|
+
instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument),
|
242
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
|
243
|
+
)
|
244
|
+
self._vm.reset()
|
245
|
+
|
246
|
+
def update_experiment(self, experiment: str) -> None:
|
247
|
+
self._vm.set_binding_parameters(
|
248
|
+
experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
|
249
|
+
)
|
250
|
+
self._vm.reset()
|
@@ -16,7 +16,7 @@ class DataSelectorViewModel:
|
|
16
16
|
|
17
17
|
self.datafiles: List[Dict[str, Any]] = []
|
18
18
|
self.directories: List[Dict[str, Any]] = []
|
19
|
-
self.expanded: List[str] =
|
19
|
+
self.expanded: Dict[str, List[str]] = {}
|
20
20
|
|
21
21
|
self.state_bind = binding.new_bind(self.model.state, callback_after_update=self.on_state_updated)
|
22
22
|
self.directories_bind = binding.new_bind()
|
@@ -44,20 +44,32 @@ class DataSelectorViewModel:
|
|
44
44
|
current_level["children"] = new_directories
|
45
45
|
|
46
46
|
# Mark this directory as expanded and display the new content
|
47
|
-
self.expanded
|
47
|
+
self.expanded[paths[-1]] = paths
|
48
48
|
self.directories_bind.update_in_view(self.directories)
|
49
49
|
|
50
|
+
def reexpand_directories(self) -> None:
|
51
|
+
paths_to_expand = self.expanded.values()
|
52
|
+
self.expanded = {}
|
53
|
+
|
54
|
+
for paths in paths_to_expand:
|
55
|
+
self.expand_directory(paths)
|
56
|
+
|
50
57
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
51
58
|
pass
|
52
59
|
|
60
|
+
def set_binding_parameters(self, **kwargs: Any) -> None:
|
61
|
+
self.model.set_binding_parameters(**kwargs)
|
62
|
+
self.update_view(refresh_directories=True)
|
63
|
+
|
53
64
|
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
54
65
|
self.model.set_subdirectory(subdirectory_path)
|
55
66
|
self.update_view()
|
56
67
|
|
57
|
-
def update_view(self) -> None:
|
68
|
+
def update_view(self, refresh_directories: bool = False) -> None:
|
58
69
|
self.state_bind.update_in_view(self.model.state)
|
59
|
-
if not self.directories:
|
70
|
+
if not self.directories or refresh_directories:
|
60
71
|
self.directories = self.model.get_directories()
|
72
|
+
self.reexpand_directories()
|
61
73
|
self.directories_bind.update_in_view(self.directories)
|
62
74
|
|
63
75
|
self.datafiles = [
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""View model implementation for the DataSelector widget."""
|
2
2
|
|
3
|
-
from typing import Any, Dict
|
3
|
+
from typing import Any, Dict
|
4
4
|
|
5
5
|
from nova.mvvm.interface import BindingInterface
|
6
6
|
from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
|
@@ -18,34 +18,22 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
|
|
18
18
|
self.instruments_bind = binding.new_bind()
|
19
19
|
self.experiments_bind = binding.new_bind()
|
20
20
|
|
21
|
-
def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
|
22
|
-
self.model.set_state(facility, instrument, experiment)
|
23
|
-
self.update_view()
|
24
|
-
|
25
21
|
def reset(self) -> None:
|
26
22
|
self.model.set_subdirectory("")
|
27
23
|
self.directories = self.model.get_directories()
|
28
|
-
self.expanded =
|
24
|
+
self.expanded = {}
|
29
25
|
self.reset_bind.update_in_view(None)
|
30
26
|
|
31
27
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
32
28
|
for update in results.get("updated", []):
|
33
29
|
match update:
|
34
|
-
case "facility":
|
35
|
-
self.model.set_state(facility=None, instrument="", experiment="")
|
36
|
-
self.reset()
|
37
|
-
case "instrument":
|
38
|
-
self.model.set_state(facility=None, instrument=None, experiment="")
|
39
|
-
self.reset()
|
40
|
-
case "experiment":
|
41
|
-
self.reset()
|
42
30
|
case "custom_directory":
|
43
31
|
self.reset()
|
44
|
-
|
32
|
+
self.update_view()
|
45
33
|
|
46
|
-
def update_view(self) -> None:
|
34
|
+
def update_view(self, refresh_directories: bool = False) -> None:
|
47
35
|
self.facilities_bind.update_in_view(self.model.get_facilities())
|
48
36
|
self.instruments_bind.update_in_view(self.model.get_instruments())
|
49
37
|
self.experiments_bind.update_in_view(self.model.get_experiments())
|
50
38
|
|
51
|
-
super().update_view()
|
39
|
+
super().update_view(refresh_directories)
|
@@ -1,15 +1,16 @@
|
|
1
1
|
nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
2
2
|
nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
|
3
|
-
nova/trame/
|
4
|
-
nova/trame/model/
|
3
|
+
nova/trame/_internal/utils.py,sha256=Yi6zdHfeIHE5dQXmxZ9x0Yyuwkjn2geFoXAYtZ_PO2s,1060
|
4
|
+
nova/trame/model/data_selector.py,sha256=rDmWDtHVGgi5e2fBEYvChM2vVKNu798m67Sq8MUN2UI,4463
|
5
|
+
nova/trame/model/ornl/neutron_data_selector.py,sha256=Eu-CnX4Gr_T-Kz_l3SsMhyGf61hlHhRCYpcn7em7TCk,6168
|
5
6
|
nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
|
6
7
|
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
7
|
-
nova/trame/view/components/data_selector.py,sha256=
|
8
|
+
nova/trame/view/components/data_selector.py,sha256=EJ8fYCrkhRgDExAcewiUb7jTs7Qzo-GKVYWx7oFlrT0,14592
|
8
9
|
nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
|
9
10
|
nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
|
10
11
|
nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
|
11
12
|
nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
|
12
|
-
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=
|
13
|
+
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=YTYuLD5eDn3AsKguT8ksPx2Yme2RxheuXQGlhuTLOOc,12527
|
13
14
|
nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
|
14
15
|
nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
|
15
16
|
nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
|
@@ -31,14 +32,14 @@ nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyD
|
|
31
32
|
nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
|
32
33
|
nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
|
33
34
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
34
|
-
nova/trame/view_model/data_selector.py,sha256=
|
35
|
+
nova/trame/view_model/data_selector.py,sha256=LeVbrBatzwffiOywI8M7F9ldZxlpi92rFRishHkFhmo,2975
|
35
36
|
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
36
|
-
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=
|
37
|
+
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=l1l_e0CFsVZ0h-9MPSjXTA2w1rgH3KoGAS67UyL8DvY,1551
|
37
38
|
nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
|
38
39
|
nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
|
39
40
|
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
40
|
-
nova_trame-0.
|
41
|
-
nova_trame-0.
|
42
|
-
nova_trame-0.
|
43
|
-
nova_trame-0.
|
44
|
-
nova_trame-0.
|
41
|
+
nova_trame-0.24.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
42
|
+
nova_trame-0.24.0.dist-info/METADATA,sha256=pSBLrSiLCUFV9pg6yj-m9ZlQjUYT7Ms2uef6QYr2gKI,1688
|
43
|
+
nova_trame-0.24.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
44
|
+
nova_trame-0.24.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
45
|
+
nova_trame-0.24.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|