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.
- nova/trame/_internal/utils.py +37 -0
- nova/trame/model/data_selector.py +10 -10
- nova/trame/model/ornl/neutron_data_selector.py +15 -24
- nova/trame/view/components/data_selector.py +132 -30
- nova/trame/view/components/execution_buttons.py +15 -4
- nova/trame/view/components/file_upload.py +18 -4
- nova/trame/view/components/input_field.py +1 -1
- nova/trame/view/components/ornl/neutron_data_selector.py +159 -55
- nova/trame/view/components/progress_bar.py +2 -1
- nova/trame/view/components/remote_file_input.py +9 -2
- nova/trame/view/components/tool_outputs.py +2 -1
- nova/trame/view/components/visualization/interactive_2d_plot.py +4 -2
- nova/trame/view/components/visualization/matplotlib_figure.py +9 -5
- nova/trame/view/layouts/grid.py +2 -2
- nova/trame/view/layouts/hbox.py +1 -1
- nova/trame/view/layouts/vbox.py +1 -1
- nova/trame/view/theme/assets/core_style.scss +8 -0
- nova/trame/view/theme/assets/js/revo_grid.js +24 -8
- nova/trame/view_model/data_selector.py +6 -0
- nova/trame/view_model/ornl/neutron_data_selector.py +4 -14
- {nova_trame-0.23.1.dist-info → nova_trame-0.25.3.dist-info}/METADATA +2 -1
- {nova_trame-0.23.1.dist-info → nova_trame-0.25.3.dist-info}/RECORD +25 -24
- {nova_trame-0.23.1.dist-info → nova_trame-0.25.3.dist-info}/LICENSE +0 -0
- {nova_trame-0.23.1.dist-info → nova_trame-0.25.3.dist-info}/WHEEL +0 -0
- {nova_trame-0.23.1.dist-info → nova_trame-0.25.3.dist-info}/entry_points.txt +0 -0
@@ -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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
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())
|
@@ -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,
|
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:
|
28
|
-
prefix: str = "",
|
29
|
-
|
30
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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.
|
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.
|
96
|
-
self.
|
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.
|
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.
|
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
|
-
|
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
|
221
|
+
self._model = DataSelectorModel(state)
|
189
222
|
|
190
|
-
def
|
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
|
-
|
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(
|
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
|
-
|
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__(
|
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,
|
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,
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
56
|
-
A subdirectory within the user's chosen experiment to show files. If not specified, the user
|
57
|
-
folder browser and will be able to see all files in the experiment that they have access to.
|
58
|
-
refresh_rate :
|
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
|
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__(
|
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=
|
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.
|
99
|
-
v_model=
|
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.
|
105
|
-
v_model=
|
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
|
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
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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 : `
|
204
|
-
|
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()
|
nova/trame/view/layouts/grid.py
CHANGED
@@ -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:
|
nova/trame/view/layouts/hbox.py
CHANGED
nova/trame/view/layouts/vbox.py
CHANGED
@@ -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 (
|
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
|
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
|
-
|
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.
|
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/
|
4
|
-
nova/trame/model/
|
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=
|
8
|
-
nova/trame/view/components/execution_buttons.py,sha256=
|
9
|
-
nova/trame/view/components/file_upload.py,sha256=
|
10
|
-
nova/trame/view/components/input_field.py,sha256=
|
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=
|
13
|
-
nova/trame/view/components/progress_bar.py,sha256=
|
14
|
-
nova/trame/view/components/remote_file_input.py,sha256=
|
15
|
-
nova/trame/view/components/tool_outputs.py,sha256
|
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=
|
18
|
-
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=
|
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=
|
21
|
-
nova/trame/view/layouts/hbox.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
41
|
-
nova_trame-0.
|
42
|
-
nova_trame-0.
|
43
|
-
nova_trame-0.
|
44
|
-
nova_trame-0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|