nova-trame 0.23.1__py3-none-any.whl → 0.25.3__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.
@@ -0,0 +1,37 @@
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
+ # Trame state handlers don't work on nested properties. When writing Trame state handlers (e.g. flushState, dirty, or
11
+ # change), we instead use the name of the top-level property. For example, "config.parameter_group_a.option_x" becomes
12
+ # "config".
13
+ def get_state_name(name: str) -> str:
14
+ return name.split(".")[0]
15
+
16
+
17
+ # Reads a state parameter from Trame. For internal use only, if you're using this in your application you're violating
18
+ # the MVVM framework. :)
19
+ def get_state_param(state: State, value: Union[Any, Tuple]) -> Any:
20
+ if isinstance(value, tuple):
21
+ return rgetdictvalue(state, value[0])
22
+
23
+ return value
24
+
25
+
26
+ # Writes a state parameter to Trame. For internal use only, if you're using this in your application you're violating
27
+ # the MVVM framework. :)
28
+ def set_state_param(state: State, value: Union[Any, Tuple], new_value: Any = None) -> Any:
29
+ with state:
30
+ if isinstance(value, tuple):
31
+ if new_value is not None:
32
+ rsetdictvalue(state, value[0], new_value)
33
+ elif len(value) > 1:
34
+ rsetdictvalue(state, value[0], value[1])
35
+ state.dirty(get_state_name(value[0]))
36
+
37
+ 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
- prefix: str = Field(default="")
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, directory: str, extensions: List[str], prefix: str) -> None:
22
+ def __init__(self, state: DataSelectorState) -> None:
24
23
  self.state: DataSelectorState = state
25
24
 
26
- self.state.directory = directory
27
- self.state.extensions = extensions
28
- self.state.prefix = prefix
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
@@ -89,10 +92,7 @@ class DataSelectorModel:
89
92
  def get_datafiles_from_path(self, base_path: Path) -> List[str]:
90
93
  datafiles = []
91
94
  try:
92
- if self.state.prefix:
93
- datafile_path = base_path / self.state.prefix
94
- else:
95
- datafile_path = base_path / self.state.subdirectory
95
+ datafile_path = base_path / self.state.subdirectory
96
96
 
97
97
  for entry in os.scandir(datafile_path):
98
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
- self,
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
- self.state.facility = facility
123
- self.state.instrument = instrument
124
- self.state.extensions = extensions
125
- self.state.prefix = prefix
126
- self.state.allow_custom_directories = allow_custom_directories
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())
@@ -175,19 +173,12 @@ class NeutronDataSelectorModel(DataSelectorModel):
175
173
  return self.get_directories_from_path(base_path)
176
174
 
177
175
  def get_datafiles(self) -> List[str]:
176
+ using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
178
177
  if self.state.experiment:
179
178
  base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
180
- elif self.state.custom_directory:
179
+ elif using_custom_directory and self.state.custom_directory:
181
180
  base_path = Path(self.state.custom_directory)
182
181
  else:
183
182
  return []
184
183
 
185
184
  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,13 +1,17 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
3
  from asyncio import ensure_future, sleep
4
- from typing import Any, List, Optional, cast
4
+ from typing import Any, List, Tuple, Union, cast
5
+ from warnings import warn
5
6
 
6
7
  from trame.app import get_server
7
8
  from trame.widgets import client, datagrid, html
8
9
  from trame.widgets import vuetify3 as vuetify
10
+ from trame_server.core import State
9
11
 
12
+ from nova.mvvm._internal.utils import rgetdictvalue
10
13
  from nova.mvvm.trame_binding import TrameBinding
14
+ from nova.trame._internal.utils import get_state_param, set_state_param
11
15
  from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
12
16
  from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
13
17
  from nova.trame.view_model.data_selector import DataSelectorViewModel
@@ -22,33 +26,36 @@ class DataSelector(datagrid.VGrid):
22
26
 
23
27
  def __init__(
24
28
  self,
25
- v_model: str,
26
- directory: str,
27
- extensions: Optional[List[str]] = None,
28
- prefix: str = "",
29
- refresh_rate: int = 30,
30
- select_strategy: str = "all",
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",
31
36
  **kwargs: Any,
32
37
  ) -> None:
33
38
  """Constructor for DataSelector.
34
39
 
35
40
  Parameters
36
41
  ----------
37
- v_model : str
42
+ v_model : Union[str, Tuple]
38
43
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
39
44
  selected by the user.
40
- directory : str
45
+ directory : Union[str, Tuple]
41
46
  The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
42
47
  users.
43
- extensions : List[str], optional
48
+ extensions : Union[List[str], Tuple], optional
44
49
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
45
- prefix : str, optional
46
- A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
47
- a folder browser and will be able to see all files in the selected top-level folder.
48
- refresh_rate : int, optional
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
49
56
  The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
50
57
  feature. Defaults to 30 seconds.
51
- select_strategy : str, optional
58
+ select_strategy : Union[str, Tuple], optional
52
59
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
53
60
  If unset, the `all` strategy will be used.
54
61
  **kwargs
@@ -73,11 +80,28 @@ class DataSelector(datagrid.VGrid):
73
80
  else:
74
81
  self._label = None
75
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
+
76
93
  self._v_model = v_model
77
- self._v_model_name_in_state = v_model.split(".")[0]
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
+
78
99
  self._directory = directory
100
+ self._last_directory = get_state_param(self.state, self._directory)
79
101
  self._extensions = extensions if extensions is not None else []
80
- self._prefix = prefix
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)
81
105
  self._refresh_rate = refresh_rate
82
106
  self._select_strategy = select_strategy
83
107
 
@@ -92,13 +116,18 @@ class DataSelector(datagrid.VGrid):
92
116
  ).exec
93
117
  self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
94
118
 
95
- self.create_model()
96
- self.create_viewmodel()
119
+ self._create_model()
120
+ self._create_viewmodel()
121
+ self._setup_bindings()
97
122
 
98
123
  self.create_ui(**kwargs)
99
124
 
100
125
  ensure_future(self._refresh_loop())
101
126
 
127
+ @property
128
+ def state(self) -> State:
129
+ return get_server(None, client_type="vue3").state
130
+
102
131
  def create_ui(self, *args: Any, **kwargs: Any) -> None:
103
132
  with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
104
133
  with HBoxLayout(valign="center"):
@@ -110,7 +139,7 @@ class DataSelector(datagrid.VGrid):
110
139
  vuetify.VTooltip("Refresh Contents", activator="parent")
111
140
 
112
141
  with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
113
- if not self._prefix:
142
+ if isinstance(self._subdirectory, tuple) or not self._subdirectory:
114
143
  with html.Div(classes="d-flex flex-column h-100 overflow-hidden"):
115
144
  vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
116
145
  vuetify.VTreeview(
@@ -122,7 +151,7 @@ class DataSelector(datagrid.VGrid):
122
151
  item_value="path",
123
152
  items=(self._directories_name,),
124
153
  click_open=(self._vm.expand_directory, "[$event.path]"),
125
- update_activated=(self._vm.set_subdirectory, "$event"),
154
+ update_activated=(self.set_subdirectory, "$event"),
126
155
  )
127
156
  vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
128
157
 
@@ -139,6 +168,7 @@ class DataSelector(datagrid.VGrid):
139
168
  " prop: 'title',"
140
169
  "}]",
141
170
  ),
171
+ column_span=1 if isinstance(self._subdirectory, tuple) or not self._subdirectory else 2,
142
172
  frame_size=10,
143
173
  hide_attribution=True,
144
174
  id=self._revogrid_id,
@@ -171,10 +201,8 @@ class DataSelector(datagrid.VGrid):
171
201
  InputField(
172
202
  v_model=self._v_model,
173
203
  classes="flex-0-1 nova-readonly",
174
- clearable=True,
175
204
  readonly=True,
176
205
  type="select",
177
- click_clear=self.reset,
178
206
  ),
179
207
  ):
180
208
  with vuetify.Template(raw_attrs=['v-slot:selection="{ item, index }"']):
@@ -183,11 +211,16 @@ class DataSelector(datagrid.VGrid):
183
211
  f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
184
212
  )
185
213
 
186
- def create_model(self) -> None:
214
+ with vuetify.Template(v_slot_append_inner=True):
215
+ vuetify.VIcon(
216
+ "mdi-close-box", v_if=f"{self._v_model}.length > 0", color="primary", size=20, click=self.reset
217
+ )
218
+
219
+ def _create_model(self) -> None:
187
220
  state = DataSelectorState()
188
- self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
221
+ self._model = DataSelectorModel(state)
189
222
 
190
- def create_viewmodel(self) -> None:
223
+ def _create_viewmodel(self) -> None:
191
224
  server = get_server(None, client_type="vue3")
192
225
  binding = TrameBinding(server.state)
193
226
 
@@ -196,8 +229,7 @@ class DataSelector(datagrid.VGrid):
196
229
  self._vm.directories_bind.connect(self._directories_name)
197
230
  self._vm.datafiles_bind.connect(self._datafiles_name)
198
231
  self._vm.reset_bind.connect(self.reset)
199
-
200
- self._vm.update_view()
232
+ self._vm.reset_grid_bind.connect(self._reset_rv_grid)
201
233
 
202
234
  def refresh_contents(self) -> None:
203
235
  self._vm.update_view(refresh_directories=True)
@@ -206,16 +238,86 @@ class DataSelector(datagrid.VGrid):
206
238
  self._reset_state()
207
239
  self._reset_rv_grid()
208
240
 
241
+ def set_subdirectory(self, subdirectory_path: str = "") -> None:
242
+ set_state_param(self.state, self._subdirectory, subdirectory_path)
243
+ self._vm.set_subdirectory(subdirectory_path)
244
+
209
245
  def set_state(self, *args: Any, **kwargs: Any) -> None:
210
246
  raise TypeError(
211
247
  "The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
212
248
  "`nova.trame.view.components.ornl`."
213
249
  )
214
250
 
251
+ # This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
252
+ # component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
253
+ # don't want bindings in the internal implementation as our callbacks could compete with the developer's.
254
+ def _setup_bindings(self) -> None:
255
+ # If the bindings were given initial values, write these to the state.
256
+ set_state_param(self.state, self._directory)
257
+ set_state_param(self.state, self._extensions)
258
+ set_state_param(self.state, self._subdirectory)
259
+ self._vm.set_binding_parameters(
260
+ directory=get_state_param(self.state, self._directory),
261
+ extensions=get_state_param(self.state, self._extensions),
262
+ subdirectory=get_state_param(self.state, self._subdirectory),
263
+ )
264
+
265
+ # The component used by this parameter will attempt to set the initial value itself, which will trigger the
266
+ # below change listeners causing unpredictable behavior.
267
+ if isinstance(self._subdirectory, tuple):
268
+ self._subdirectory = (self._subdirectory[0],)
269
+
270
+ # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
271
+ # when other portions of the application manipulate these parameters.
272
+ if isinstance(self._directory, tuple):
273
+
274
+ @self.state.change(self._directory[0].split(".")[0])
275
+ def on_directory_change(**kwargs: Any) -> None:
276
+ directory = rgetdictvalue(kwargs, self._directory[0])
277
+ if directory != self._last_directory:
278
+ self._last_directory = directory
279
+ self._vm.set_binding_parameters(
280
+ directory=set_state_param(self.state, self._directory, directory),
281
+ )
282
+
283
+ if isinstance(self._extensions, tuple):
284
+
285
+ @self.state.change(self._extensions[0].split(".")[0])
286
+ def on_extensions_change(**kwargs: Any) -> None:
287
+ extensions = rgetdictvalue(kwargs, self._extensions[0])
288
+ if extensions != self._last_extensions:
289
+ self._last_extensions = extensions
290
+ self._vm.set_binding_parameters(
291
+ extensions=set_state_param(self.state, self._extensions, extensions),
292
+ )
293
+
294
+ if isinstance(self._subdirectory, tuple):
295
+
296
+ @self.state.change(self._subdirectory[0].split(".")[0])
297
+ def on_subdirectory_change(**kwargs: Any) -> None:
298
+ subdirectory = rgetdictvalue(kwargs, self._subdirectory[0])
299
+ if subdirectory != self._last_subdirectory:
300
+ self._last_subdirectory = subdirectory
301
+ self._vm.set_binding_parameters(
302
+ subdirectory=set_state_param(self.state, self._subdirectory, subdirectory),
303
+ )
304
+
215
305
  async def _refresh_loop(self) -> None:
216
- if self._refresh_rate > 0:
306
+ refresh_rate: int = set_state_param(self.state, self._refresh_rate)
307
+ skip = False
308
+
309
+ if refresh_rate > 0:
217
310
  while True:
218
- await sleep(self._refresh_rate)
311
+ await sleep(refresh_rate)
312
+ if skip:
313
+ continue
219
314
 
220
315
  self.refresh_contents()
221
316
  self.state.dirty(self._datafiles_name)
317
+
318
+ try:
319
+ refresh_rate = int(get_state_param(self.state, self._refresh_rate))
320
+ skip = False
321
+ except TypeError:
322
+ refresh_rate = 1
323
+ skip = True
@@ -1,5 +1,7 @@
1
1
  """Module for the Progress Tab."""
2
2
 
3
+ from typing import Tuple, Union
4
+
3
5
  from trame.app import get_server
4
6
  from trame.widgets import client
5
7
  from trame.widgets import vuetify3 as vuetify
@@ -15,16 +17,17 @@ class ExecutionButtons:
15
17
  This is intended to be used with the `nova-galaxy ToolRunner <https://nova-application-development.readthedocs.io/projects/nova-galaxy/en/latest/core_concepts/tool_runner.html>`__.
16
18
  """
17
19
 
18
- def __init__(self, id: str, stop_btn: bool = False, download_btn: bool = False) -> None:
20
+ def __init__(self, id: str, stop_btn: Union[bool, Tuple] = False, download_btn: Union[bool, Tuple] = False) -> None:
19
21
  """Constructor for ExecutionButtons.
20
22
 
21
23
  Parameters
22
24
  ----------
23
25
  id : str
24
- Component id. Should be used consistently with ToolRunner and other components.
25
- stop_btn: bool
26
+ Component id. Should be used consistently with ToolRunner and other components. Note that this parameter
27
+ does not support Trame bindings.
28
+ stop_btn: Union[bool, Tuple]
26
29
  Display stop button.
27
- download_btn : bool
30
+ download_btn : Union[bool, Tuple]
28
31
  Display download button.
29
32
 
30
33
  Returns
@@ -69,6 +72,9 @@ class ExecutionButtons:
69
72
  click=self.run,
70
73
  )
71
74
  if self.stop_btn:
75
+ extra_params = {}
76
+ if isinstance(self.stop_btn, tuple):
77
+ extra_params["v_if"] = self.stop_btn
72
78
  vuetify.VBtn(
73
79
  "Stop",
74
80
  disabled=(f"{self.id}.stop_disabled",),
@@ -77,6 +83,7 @@ class ExecutionButtons:
77
83
  id=f"{self.id}_stop",
78
84
  prepend_icon="mdi-stop",
79
85
  click=self.stop,
86
+ **extra_params,
80
87
  )
81
88
  vuetify.VBtn(
82
89
  "Cancel",
@@ -89,12 +96,16 @@ class ExecutionButtons:
89
96
  click=self.cancel,
90
97
  )
91
98
  if self.download_btn:
99
+ extra_params = {}
100
+ if isinstance(self.download_btn, tuple):
101
+ extra_params["v_if"] = self.download_btn
92
102
  vuetify.VBtn(
93
103
  "Download Results",
94
104
  disabled=(f"{self.id}.download_disabled",),
95
105
  loading=(f"{self.id}.download_in_progress",),
96
106
  id=f"{self.id}.download",
97
107
  click=self.download,
108
+ **extra_params,
98
109
  )
99
110
 
100
111
  async def download(self) -> None:
@@ -10,7 +10,14 @@ from .remote_file_input import RemoteFileInput
10
10
  class FileUpload(vuetify.VBtn):
11
11
  """Component for uploading a file from either the user's filesystem or the server filesystem."""
12
12
 
13
- def __init__(self, v_model: str, base_paths: Optional[List[str]] = None, label: str = "", **kwargs: Any) -> None:
13
+ def __init__(
14
+ self,
15
+ v_model: str,
16
+ base_paths: Optional[List[str]] = None,
17
+ label: str = "",
18
+ return_contents: bool = True,
19
+ **kwargs: Any,
20
+ ) -> None:
14
21
  """Constructor for FileUpload.
15
22
 
16
23
  Parameters
@@ -23,6 +30,9 @@ class FileUpload(vuetify.VBtn):
23
30
  Passed to :ref:`RemoteFileInput <api_remotefileinput>`.
24
31
  label : str, optional
25
32
  The text to display on the upload button.
33
+ return_contents : bool, optional
34
+ If true, the file contents will be stored in v_model. If false, a file path will be stored in v_model.
35
+ Defaults to true.
26
36
  **kwargs
27
37
  All other arguments will be passed to the underlying
28
38
  `Button component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VBtn>`_.
@@ -36,6 +46,7 @@ class FileUpload(vuetify.VBtn):
36
46
  self._base_paths = base_paths
37
47
  else:
38
48
  self._base_paths = ["/"]
49
+ self._return_contents = return_contents
39
50
  self._ref_name = f"nova__fileupload_{self._next_id}"
40
51
 
41
52
  super().__init__(label, **kwargs)
@@ -49,12 +60,15 @@ class FileUpload(vuetify.VBtn):
49
60
  # Serialize the content in a way that will work with nova-mvvm and then push it to the server.
50
61
  update_modelValue=(
51
62
  f"{self._v_model}.arrayBuffer().then((contents) => {{"
52
- f"trigger('decode_blob_{self._id}', [contents]); "
63
+ f" trigger('decode_blob_{self._id}', [contents]); "
53
64
  "});"
54
65
  ),
55
66
  )
56
67
  self.remote_file_input = RemoteFileInput(
57
- v_model=self._v_model, base_paths=self._base_paths, input_props={"classes": "d-none"}, return_contents=True
68
+ v_model=self._v_model,
69
+ base_paths=self._base_paths,
70
+ input_props={"classes": "d-none"},
71
+ return_contents=self._return_contents,
58
72
  )
59
73
 
60
74
  with self:
@@ -65,7 +79,7 @@ class FileUpload(vuetify.VBtn):
65
79
 
66
80
  @self.server.controller.trigger(f"decode_blob_{self._id}")
67
81
  def _decode_blob(contents: bytes) -> None:
68
- self.remote_file_input.decode_file(contents)
82
+ self.remote_file_input.decode_file(contents, self._return_contents)
69
83
 
70
84
  def select_file(self, value: str) -> None:
71
85
  """Programmatically set the RemoteFileInput path.
@@ -172,7 +172,7 @@ class InputField:
172
172
  The following example would set the auto_grow and label attributes on
173
173
  `VTextarea <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VTextarea>`_:
174
174
 
175
- .. literalinclude:: ../tests/gallery/app.py
175
+ .. literalinclude:: ../tests/gallery/views/app.py
176
176
  :start-after: InputField kwargs example start
177
177
  :end-before: InputField kwargs example end
178
178
  :dedent:
@@ -1,11 +1,12 @@
1
1
  """View Implementation for DataSelector."""
2
2
 
3
- from typing import Any, List, Optional
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,39 +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
- extensions: Optional[List[str]] = None,
34
- prefix: str = "",
35
- refresh_rate: int = 30,
36
- select_strategy: str = "all",
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",
37
39
  **kwargs: Any,
38
40
  ) -> None:
39
41
  """Constructor for DataSelector.
40
42
 
41
43
  Parameters
42
44
  ----------
43
- v_model : str
45
+ v_model : Union[str, Tuple]
44
46
  The name of the state variable to bind to this widget. The state variable will contain a list of the files
45
47
  selected by the user.
46
- allow_custom_directories : bool, optional
48
+ allow_custom_directories : Union[bool, Tuple], optional
47
49
  Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
48
50
  facility parameter is set.
49
- facility : str, optional
51
+ facility : Union[str, Tuple], optional
50
52
  The facility to restrict data selection to. Options: HFIR, SNS
51
- instrument : str, optional
53
+ instrument : Union[str, Tuple], optional
52
54
  The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
53
- extensions : List[str], optional
55
+ experiment : Union[str, Tuple], optional
56
+ The experiment to restrict data selection to.
57
+ extensions : Union[List[str], Tuple], optional
54
58
  A list of file extensions to restrict selection to. If unset, then all files will be shown.
55
- prefix : str, optional
56
- A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
57
- folder browser and will be able to see all files in the experiment that they have access to.
58
- refresh_rate : int, optional
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
59
63
  The number of seconds between attempts to automatically refresh the file list. Set to zero to disable this
60
64
  feature. Defaults to 30 seconds.
61
- select_strategy : str, optional
65
+ select_strategy : Union[str, Tuple], optional
62
66
  The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
63
67
  If unset, the `all` strategy will be used.
64
68
  **kwargs
@@ -69,53 +73,76 @@ class NeutronDataSelector(DataSelector):
69
73
  -------
70
74
  None
71
75
  """
72
- if facility and allow_custom_directories:
73
- warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
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)
74
78
 
75
79
  self._facility = facility
76
80
  self._instrument = instrument
81
+ self._experiment = experiment
77
82
  self._allow_custom_directories = allow_custom_directories
83
+ self._last_allow_custom_directories = self._allow_custom_directories
78
84
 
85
+ self._state_name = f"nova__dataselector_{self._next_id}_state"
79
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
+ )
80
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
+ )
81
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
+ )
82
98
 
83
- super().__init__(v_model, "", extensions, prefix, refresh_rate, select_strategy, **kwargs)
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
+ )
84
108
 
85
109
  def create_ui(self, **kwargs: Any) -> None:
86
110
  super().create_ui(**kwargs)
87
111
  with self._layout.filter:
88
112
  with GridLayout(columns=3):
89
113
  columns = 3
90
- if self._facility == "":
114
+ if isinstance(self._facility, tuple) or not self._facility:
91
115
  columns -= 1
92
116
  InputField(
93
- v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
117
+ v_model=self._selected_facility_name,
118
+ items=(self._facilities_name,),
119
+ type="autocomplete",
120
+ update_modelValue=(self.update_facility, "[$event]"),
94
121
  )
95
- if self._instrument == "":
122
+ if isinstance(self._instrument, tuple) or not self._instrument:
96
123
  columns -= 1
97
124
  InputField(
98
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
99
- v_model=f"{self._state_name}.instrument",
125
+ v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
126
+ v_model=self._selected_instrument_name,
100
127
  items=(self._instruments_name,),
101
128
  type="autocomplete",
129
+ update_modelValue=(self.update_instrument, "[$event]"),
102
130
  )
103
131
  InputField(
104
- v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
105
- v_model=f"{self._state_name}.experiment",
132
+ v_if=f"{self._selected_facility_name} !== '{CUSTOM_DIRECTORIES_LABEL}'",
133
+ v_model=self._selected_experiment_name,
106
134
  column_span=columns,
107
135
  items=(self._experiments_name,),
108
136
  type="autocomplete",
137
+ update_modelValue=(self.update_experiment, "[$event]"),
109
138
  )
110
139
  InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
111
140
 
112
- def create_model(self) -> None:
141
+ def _create_model(self) -> None:
113
142
  state = NeutronDataSelectorState()
114
- self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
115
- state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
116
- )
143
+ self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(state)
117
144
 
118
- def create_viewmodel(self) -> None:
145
+ def _create_viewmodel(self) -> None:
119
146
  server = get_server(None, client_type="vue3")
120
147
  binding = TrameBinding(server.state)
121
148
 
@@ -127,29 +154,106 @@ class NeutronDataSelector(DataSelector):
127
154
  self._vm.directories_bind.connect(self._directories_name)
128
155
  self._vm.datafiles_bind.connect(self._datafiles_name)
129
156
  self._vm.reset_bind.connect(self.reset)
157
+ self._vm.reset_grid_bind.connect(self._reset_rv_grid)
130
158
 
131
159
  self._vm.update_view()
132
160
 
133
- def set_state(
134
- self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
135
- ) -> None:
136
- """Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
161
+ # This method sets up Trame state change listeners for each binding parameter that can be changed directly by this
162
+ # component. This allows us to communicate the changes to the developer's bindings without requiring our own. We
163
+ # don't want bindings in the internal implementation as our callbacks could compete with the developer's.
164
+ def _setup_bindings(self) -> None:
165
+ # If the bindings were given initial values, write these to the state.
166
+ set_state_param(self.state, self._facility)
167
+ set_state_param(self.state, self._instrument)
168
+ set_state_param(self.state, self._experiment)
169
+ set_state_param(self.state, self._allow_custom_directories)
170
+ self._last_facility = get_state_param(self.state, self._facility)
171
+ self._last_instrument = get_state_param(self.state, self._instrument)
172
+ self._last_experiment = get_state_param(self.state, self._experiment)
173
+ self._vm.set_binding_parameters(
174
+ facility=get_state_param(self.state, self._facility),
175
+ instrument=get_state_param(self.state, self._instrument),
176
+ experiment=get_state_param(self.state, self._experiment),
177
+ allow_custom_directories=get_state_param(self.state, self._allow_custom_directories),
178
+ )
137
179
 
138
- If a parameter is None, then it will not be updated.
180
+ # Now we set up the change listeners for all bound parameters. These are responsible for updating the component
181
+ # when other portions of the application manipulate these parameters.
182
+ if isinstance(self._facility, tuple):
139
183
 
140
- Parameters
141
- ----------
142
- facility : str, optional
143
- The facility to restrict data selection to. Options: HFIR, SNS
144
- instrument : str, optional
145
- The instrument to restrict data selection to. Must be at the selected facility.
146
- experiment : str, optional
147
- The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
148
- that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
149
- shown to the user.
184
+ @self.state.change(self._facility[0].split(".")[0])
185
+ def on_facility_change(**kwargs: Any) -> None:
186
+ facility = rgetdictvalue(kwargs, self._facility[0])
187
+ if facility != self._last_facility:
188
+ self._last_facility = facility
189
+ self._vm.set_binding_parameters(
190
+ facility=set_state_param(self.state, (self._selected_facility_name,), facility)
191
+ )
192
+ self._vm.reset()
150
193
 
151
- Returns
152
- -------
153
- None
154
- """
155
- self._vm.set_state(facility, instrument, experiment)
194
+ if isinstance(self._instrument, tuple):
195
+
196
+ @self.state.change(self._instrument[0].split(".")[0])
197
+ def on_instrument_change(**kwargs: Any) -> None:
198
+ instrument = rgetdictvalue(kwargs, self._instrument[0])
199
+ if instrument != self._last_instrument:
200
+ self._last_instrument = instrument
201
+ self._vm.set_binding_parameters(
202
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument)
203
+ )
204
+ self._vm.reset()
205
+
206
+ if isinstance(self._experiment, tuple):
207
+
208
+ @self.state.change(self._experiment[0].split(".")[0])
209
+ def on_experiment_change(**kwargs: Any) -> None:
210
+ experiment = rgetdictvalue(kwargs, self._experiment[0])
211
+ if experiment and experiment != self._last_experiment:
212
+ self._last_experiment = experiment
213
+ # See the note in the update_experiment method for why we call this twice.
214
+ self._vm.set_binding_parameters(
215
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""),
216
+ )
217
+ self._vm.set_binding_parameters(
218
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment)
219
+ )
220
+ self._vm.reset()
221
+
222
+ if isinstance(self._allow_custom_directories, tuple):
223
+
224
+ @self.state.change(self._allow_custom_directories[0].split(".")[0])
225
+ def on_allow_custom_directories_change(**kwargs: Any) -> None:
226
+ allow_custom_directories = rgetdictvalue(kwargs, self._allow_custom_directories[0]) # type: ignore
227
+ if allow_custom_directories != self._last_allow_custom_directories:
228
+ self._last_allow_custom_directories = allow_custom_directories
229
+ self._vm.set_binding_parameters(
230
+ allow_custom_directories=set_state_param(
231
+ self.state, self._allow_custom_directories, allow_custom_directories
232
+ )
233
+ )
234
+
235
+ # These update methods notify the rest of the application when the component changes bound parameters.
236
+ def update_facility(self, facility: str) -> None:
237
+ self._vm.set_binding_parameters(
238
+ facility=set_state_param(self.state, (self._selected_facility_name,), facility),
239
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), ""), # Reset the instrument
240
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
241
+ )
242
+ self._vm.reset()
243
+
244
+ def update_instrument(self, instrument: str) -> None:
245
+ self._vm.set_binding_parameters(
246
+ instrument=set_state_param(self.state, (self._selected_instrument_name,), instrument),
247
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""), # Reset the experiment
248
+ )
249
+ self._vm.reset()
250
+
251
+ def update_experiment(self, experiment: str) -> None:
252
+ # Setting the experiment to an empty string forces the treeview to clear it's selection state.
253
+ self._vm.set_binding_parameters(
254
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), ""),
255
+ )
256
+ self._vm.set_binding_parameters(
257
+ experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
258
+ )
259
+ self._vm.reset()
@@ -20,7 +20,8 @@ class ProgressBar:
20
20
  Parameters
21
21
  ----------
22
22
  id : str
23
- Component id. Should be used consistently with ToolRunner and other components
23
+ Component id. Should be used consistently with ToolRunner and other components. Note that this parameter
24
+ does not support Trame bindings.
24
25
 
25
26
  Returns
26
27
  -------
@@ -1,6 +1,7 @@
1
1
  """View implementation for RemoteFileInput."""
2
2
 
3
3
  from functools import partial
4
+ from tempfile import NamedTemporaryFile
4
5
  from typing import Any, Optional, Union, cast
5
6
 
6
7
  from trame.app import get_server
@@ -207,9 +208,15 @@ class RemoteFileInput:
207
208
  with open(file_path, mode="rb") as file:
208
209
  self.decode_file(file.read())
209
210
 
210
- def decode_file(self, bytestream: bytes) -> None:
211
+ def decode_file(self, bytestream: bytes, set_contents: bool = False) -> None:
211
212
  decoded_content = bytestream.decode("latin1")
212
- self.set_v_model(decoded_content)
213
+ if set_contents:
214
+ self.set_v_model(decoded_content)
215
+ else:
216
+ with NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as temp_file:
217
+ temp_file.write(decoded_content)
218
+ temp_file.flush()
219
+ self.set_v_model(temp_file.name)
213
220
 
214
221
  def select_file(self, value: str) -> None:
215
222
  """Programmatically set the v_model value."""
@@ -21,7 +21,8 @@ class ToolOutputWindows:
21
21
  Parameters
22
22
  ----------
23
23
  id : str
24
- Component id. Should be used consistently with ToolRunner and other components
24
+ Component id. Should be used consistently with ToolRunner and other components. Note that this parameter
25
+ does not support Trame bindings.
25
26
 
26
27
  Returns
27
28
  -------
@@ -30,7 +30,7 @@ class Interactive2DPlot(vega.Figure):
30
30
 
31
31
  Parameters
32
32
  ----------
33
- figure : `altair.Chart <https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart>`_
33
+ figure : `altair.Chart <https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart>`__, optional
34
34
  Altair chart object
35
35
  kwargs
36
36
  Arguments to be passed to `AbstractElement <https://trame.readthedocs.io/en/latest/core.widget.html#trame_client.widgets.core.AbstractElement>`_
@@ -38,7 +38,7 @@ class Interactive2DPlot(vega.Figure):
38
38
  Returns
39
39
  -------
40
40
  None
41
- """
41
+ """ # noqa: E501
42
42
  self._initialized = False
43
43
 
44
44
  super().__init__(figure=figure, **kwargs)
@@ -83,3 +83,5 @@ class Interactive2DPlot(vega.Figure):
83
83
 
84
84
  if hasattr(self, "_start_update_handlers"):
85
85
  self._start_update_handlers()
86
+
87
+ self.server.state.flush()
@@ -200,18 +200,20 @@ class MatplotlibFigure(matplotlib.Figure):
200
200
 
201
201
  Parameters
202
202
  ----------
203
- figure : `altair.Chart <https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart>`_
204
- Altair chart object
205
- webagg : bool
203
+ figure : `matplotlib.figure.Figure <https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure>`__, optional
204
+ Initial Matplotlib figure.
205
+ webagg : bool, optional
206
206
  If true, then the WebAgg backend for Matplotlib is used. If not, then the default Trame matplotlib plugin
207
- is used.
207
+ is used. Note that this parameter does not supporting Trame bindings since the user experiences are
208
+ fundamentally different between the two options and toggling them is not considered a good idea by the
209
+ author of this component.
208
210
  kwargs
209
211
  Arguments to be passed to `AbstractElement <https://trame.readthedocs.io/en/latest/core.widget.html#trame_client.widgets.core.AbstractElement>`_
210
212
 
211
213
  Returns
212
214
  -------
213
215
  None
214
- """
216
+ """ # noqa: E501
215
217
  self._webagg = webagg
216
218
  if webagg:
217
219
  self._port = MatplotlibFigure._get_free_port()
@@ -253,6 +255,8 @@ class MatplotlibFigure(matplotlib.Figure):
253
255
  else:
254
256
  super().update(figure)
255
257
 
258
+ self._server.state.flush()
259
+
256
260
  def _setup_figure_websocket(self) -> None:
257
261
  thread = Thread(target=self._mpl_run_ws_server, daemon=True)
258
262
  thread.start()
@@ -53,7 +53,7 @@ class GridLayout(html.Div):
53
53
  --------
54
54
  Basic usage:
55
55
 
56
- .. literalinclude:: ../tests/gallery/app.py
56
+ .. literalinclude:: ../tests/gallery/views/app.py
57
57
  :start-after: setup grid
58
58
  :end-before: setup grid complete
59
59
  :dedent:
@@ -133,7 +133,7 @@ class GridLayout(html.Div):
133
133
 
134
134
  Example
135
135
  -------
136
- .. literalinclude:: ../tests/gallery/app.py
136
+ .. literalinclude:: ../tests/gallery/views/app.py
137
137
  :start-after: grid row and column span example
138
138
  :end-before: grid row and column span example end
139
139
  :dedent:
@@ -51,7 +51,7 @@ class HBoxLayout(html.Div):
51
51
 
52
52
  Example
53
53
  -------
54
- .. literalinclude:: ../tests/gallery/app.py
54
+ .. literalinclude:: ../tests/gallery/views/app.py
55
55
  :start-after: setup hbox
56
56
  :end-before: setup hbox complete
57
57
  :dedent:
@@ -51,7 +51,7 @@ class VBoxLayout(html.Div):
51
51
 
52
52
  Example
53
53
  -------
54
- .. literalinclude:: ../tests/gallery/app.py
54
+ .. literalinclude:: ../tests/gallery/views/app.py
55
55
  :start-after: setup vbox
56
56
  :end-before: setup vbox complete
57
57
  :dedent:
@@ -149,6 +149,10 @@ html {
149
149
  }
150
150
  }
151
151
 
152
+ .v-tabs {
153
+ height: 32px !important;
154
+ }
155
+
152
156
  .v-tab.v-btn {
153
157
  height: 30px !important;
154
158
  min-width: fit-content !important;
@@ -198,6 +202,10 @@ html {
198
202
 
199
203
  .v-field {
200
204
  margin: 8px 4px 8px 4px;
205
+
206
+ &.v-field--appended {
207
+ padding-right: 6px;
208
+ }
201
209
  }
202
210
 
203
211
  .v-field--active .v-label {
@@ -12,6 +12,11 @@ class RevoGrid {
12
12
  }
13
13
 
14
14
  updateCheckboxes() {
15
+ // Wait for the DOM to update after the Trame state is updated.
16
+ setTimeout(this._updateCheckboxes.bind(this), 10)
17
+ }
18
+
19
+ _updateCheckboxes() {
15
20
  const trameState = window.trame.state.state
16
21
  const modelValue = _.get(trameState, this.modelKey)
17
22
  const availableData = _.get(trameState, this.dataKey)
@@ -22,23 +27,34 @@ class RevoGrid {
22
27
  return
23
28
  }
24
29
 
30
+ let allSelected = null
31
+ rowCheckboxes.forEach((element) => {
32
+ const input = element.querySelector('input')
33
+
34
+ const rowIndex = element.dataset.rgrow
35
+ if (availableData[rowIndex] !== undefined) {
36
+ input.checked = modelValue.includes(availableData[rowIndex].path)
37
+ } else {
38
+ input.checked = false
39
+ }
40
+
41
+ if (allSelected === null && input.checked) {
42
+ allSelected = true
43
+ } else if (!input.checked) {
44
+ allSelected = false
45
+ }
46
+ })
47
+
25
48
  if (modelValue.length === 0) {
26
49
  selectAllCheckbox.checked = false
27
50
  selectAllCheckbox.indeterminate = false
28
- } else if (modelValue.length === availableData.length) {
51
+ } else if (allSelected === true) {
29
52
  selectAllCheckbox.checked = true
30
53
  selectAllCheckbox.indeterminate = false
31
54
  } else {
32
55
  selectAllCheckbox.checked = false
33
56
  selectAllCheckbox.indeterminate = true
34
57
  }
35
-
36
- rowCheckboxes.forEach((element) => {
37
- const input = element.querySelector('input')
38
-
39
- const rowIndex = element.dataset.rgrow
40
- input.checked = modelValue.includes(availableData[rowIndex].path)
41
- })
42
58
  }
43
59
 
44
60
  cellTemplate(createElement, props) {
@@ -22,6 +22,7 @@ class DataSelectorViewModel:
22
22
  self.directories_bind = binding.new_bind()
23
23
  self.datafiles_bind = binding.new_bind()
24
24
  self.reset_bind = binding.new_bind()
25
+ self.reset_grid_bind = binding.new_bind()
25
26
 
26
27
  def expand_directory(self, paths: List[str]) -> None:
27
28
  if paths[-1] in self.expanded:
@@ -57,6 +58,10 @@ class DataSelectorViewModel:
57
58
  def on_state_updated(self, results: Dict[str, Any]) -> None:
58
59
  pass
59
60
 
61
+ def set_binding_parameters(self, **kwargs: Any) -> None:
62
+ self.model.set_binding_parameters(**kwargs)
63
+ self.update_view(refresh_directories=True)
64
+
60
65
  def set_subdirectory(self, subdirectory_path: str = "") -> None:
61
66
  self.model.set_subdirectory(subdirectory_path)
62
67
  self.update_view()
@@ -72,3 +77,4 @@ class DataSelectorViewModel:
72
77
  {"path": datafile, "title": os.path.basename(datafile)} for datafile in self.model.get_datafiles()
73
78
  ]
74
79
  self.datafiles_bind.update_in_view(self.datafiles)
80
+ self.reset_grid_bind.update_in_view(None)
@@ -1,6 +1,6 @@
1
1
  """View model implementation for the DataSelector widget."""
2
2
 
3
- from typing import Any, Dict, Optional
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,30 +18,20 @@ 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
24
  self.expanded = {}
25
+
26
+ self.update_view()
29
27
  self.reset_bind.update_in_view(None)
30
28
 
31
29
  def on_state_updated(self, results: Dict[str, Any]) -> None:
32
30
  for update in results.get("updated", []):
33
31
  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
32
  case "custom_directory":
43
33
  self.reset()
44
- self.update_view()
34
+ self.update_view()
45
35
 
46
36
  def update_view(self, refresh_directories: bool = False) -> None:
47
37
  self.facilities_bind.update_in_view(self.model.get_facilities())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nova-trame
3
- Version: 0.23.1
3
+ Version: 0.25.3
4
4
  Summary: A Python Package for injecting curated themes and custom components into Trame applications
5
5
  License: MIT
6
6
  Keywords: NDIP,Python,Trame,Vuetify
@@ -17,6 +17,7 @@ Requires-Dist: altair
17
17
  Requires-Dist: blinker (>=1.9.0,<2.0.0)
18
18
  Requires-Dist: libsass
19
19
  Requires-Dist: mergedeep
20
+ Requires-Dist: mpld3 (>=0.5.11,<0.6.0)
20
21
  Requires-Dist: natsort (>=8.4.0,<9.0.0)
21
22
  Requires-Dist: nova-common (>=0.2.2)
22
23
  Requires-Dist: nova-mvvm
@@ -1,44 +1,45 @@
1
1
  nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
2
  nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
3
- nova/trame/model/data_selector.py,sha256=8eaIVGak6XOAQMxdUotOsiXisW14sSRd1V0oViglJVQ,4448
4
- nova/trame/model/ornl/neutron_data_selector.py,sha256=nfjXwT93JcPjJwyDynoP_stDVAfjUs7neeVZTk_04gc,6424
3
+ nova/trame/_internal/utils.py,sha256=lTTJnfqbbIe21Tg2buf5MXqKUEUop7Va5PZgpWMzRkI,1381
4
+ nova/trame/model/data_selector.py,sha256=rDmWDtHVGgi5e2fBEYvChM2vVKNu798m67Sq8MUN2UI,4463
5
+ nova/trame/model/ornl/neutron_data_selector.py,sha256=Nkj0DXdv3ydfXV3zeilbOGWuRCVzc_ClAOj6iNnN0uI,6276
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=d5UDF7ENBNifmLyIecx8VyINRY4vSxlQHyJ2a5G7IOY,9606
8
- nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
9
- nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
10
- nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
8
+ nova/trame/view/components/data_selector.py,sha256=XFwIuKhIeBbkqsNEkIFDhjdDD5PD6cvbgn1jkivUhC4,14817
9
+ nova/trame/view/components/execution_buttons.py,sha256=Br6uAmE5bY67TTYc5ZTHECNJ_RJqKmv17HAKPpQtbeg,4576
10
+ nova/trame/view/components/file_upload.py,sha256=Q3t7TUJ8w6wlEqb1mnJ23yBsM1XmQPtm0awaoBrlLXo,3509
11
+ nova/trame/view/components/input_field.py,sha256=Rtcl_eszvhgyC1rhTI7OMSLHjrE7DNH44eY08k7UXks,16094
11
12
  nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
12
- nova/trame/view/components/ornl/neutron_data_selector.py,sha256=8B0BVXzVtMApnbMGD6ms6G4gdv5l-dIWM6YlHcBgnoc,6878
13
- nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
14
- nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
15
- nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
13
+ nova/trame/view/components/ornl/neutron_data_selector.py,sha256=g633Ie3GSNu0QFEuexYl_XUxjUiGEJKjWLP7ZB5PRR0,13122
14
+ nova/trame/view/components/progress_bar.py,sha256=zhbJwPy_HPQ8YL-ISN8sCRUQ7qY6qqo9wiV59BmvL8I,3038
15
+ nova/trame/view/components/remote_file_input.py,sha256=6mUz6JZVhLO_Y4mZaQd_lpPe33KLtSpjxXS7uTNUmFI,10004
16
+ nova/trame/view/components/tool_outputs.py,sha256=IbYV4VjrkWAE354Bh5KH76SPsxGLIkOXChijS4-ce_Y,2408
16
17
  nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
17
- nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
18
- nova/trame/view/components/visualization/matplotlib_figure.py,sha256=GGH2cx-dQFkMAOTnlCrzMGDb2TN451I9J3gAS8tx2cs,12147
18
+ nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=z2s1janxAclpMEdDJk3z-CQ6r3KPNoR_SXPx9ppWnuQ,3481
19
+ nova/trame/view/components/visualization/matplotlib_figure.py,sha256=q0HLaaLFjM3_V1oUk-VBHWvokFY6AQZzmnMcynTroik,12488
19
20
  nova/trame/view/layouts/__init__.py,sha256=cMrlB5YMUoK8EGB83b34UU0kPTVrH8AxsYvKRtpUNEc,141
20
- nova/trame/view/layouts/grid.py,sha256=BYoylq-VN1l55BXBWMJ_7zvHcQYmfOo811nzD72IBOQ,5522
21
- nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddyhQ,3513
21
+ nova/trame/view/layouts/grid.py,sha256=vqEX-jghs6j9_sVtijdRH7uhlD9loWNi90k2qgg4Dhg,5534
22
+ nova/trame/view/layouts/hbox.py,sha256=cdwnGk93ec6dXAeEamRQx1WTj5T7Ygsmsy0xz130tWM,3519
22
23
  nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
23
- nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
24
+ nova/trame/view/layouts/vbox.py,sha256=XRV14e32MY1HWc9FTVTv1vOatWWbhLMd0lYwZP-isTg,3520
24
25
  nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
25
- nova/trame/view/theme/assets/core_style.scss,sha256=AJ-2hyQRVkyOGWJB0lGMzlQeshbCJP5BGNYCt9e9AOI,4208
26
+ nova/trame/view/theme/assets/core_style.scss,sha256=3-3qMc5gpaDhfuVWAF_psBH5alxwiuK-hPGhVgi2cW0,4335
26
27
  nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
27
28
  nova/trame/view/theme/assets/js/delay_manager.js,sha256=mRV6KoO8-Bxq3tG5Bh9CQYy-CRVbkj3IYlqNb-Og7cI,526
28
29
  nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
29
- nova/trame/view/theme/assets/js/revo_grid.js,sha256=WBsmoslu9qI5DHZkHkJam2AVgdiBp6szfOSV8a9cA5Q,3579
30
+ nova/trame/view/theme/assets/js/revo_grid.js,sha256=81s0fUo8HbHmAyWag7pW0jP796Ttb1noAPOgTJlxJss,4069
30
31
  nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
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=yDyYceZAUIxv4hhJGAPdIo54zHmrwqkxNZh7F2sRlw0,2810
35
+ nova/trame/view_model/data_selector.py,sha256=d8qdn6Q2b5fNo5lCXi1LTRdesfmy7wErIvGBa0UjOt8,3075
35
36
  nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
36
- nova/trame/view_model/ornl/neutron_data_selector.py,sha256=WTuz9QO4WzbOftTH0uP9Qxn7nYdsnIoixu1hWHKCVNU,2128
37
+ nova/trame/view_model/ornl/neutron_data_selector.py,sha256=zpwvqETPuw0sEUvv0A2sU5Ha2_BKG_3xFYSaUuixLXw,1579
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.23.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
41
- nova_trame-0.23.1.dist-info/METADATA,sha256=VVMLgB8JhBIzt_FbCAEUIjPh3Zu8gEqQy091t3Fpd8E,1688
42
- nova_trame-0.23.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
- nova_trame-0.23.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
44
- nova_trame-0.23.1.dist-info/RECORD,,
41
+ nova_trame-0.25.3.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
42
+ nova_trame-0.25.3.dist-info/METADATA,sha256=fUOB8LAzrU6ZyuLaEwTISI81BTrr-JbSQC7gX1e0eZo,1727
43
+ nova_trame-0.25.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
+ nova_trame-0.25.3.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
45
+ nova_trame-0.25.3.dist-info/RECORD,,