nova-trame 1.0.0rc5__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nova-trame might be problematic. Click here for more details.
- nova/trame/model/data_selector.py +46 -4
- nova/trame/view/components/data_selector.py +115 -46
- nova/trame/view/components/ornl/neutron_data_selector.py +11 -1
- nova/trame/view/components/visualization/matplotlib_figure.py +11 -17
- nova/trame/view/layouts/grid.py +7 -1
- nova/trame/view/layouts/hbox.py +7 -1
- nova/trame/view/layouts/vbox.py +7 -1
- nova/trame/view/theme/assets/core_style.scss +39 -2
- nova/trame/view/theme/assets/favicon.png +0 -0
- nova/trame/view/theme/assets/js/revo_grid.js +52 -7
- nova/trame/view/theme/exit_button.py +16 -2
- nova/trame/view/theme/theme.py +31 -2
- nova/trame/view_model/data_selector.py +11 -1
- nova/trame/view_model/ornl/neutron_data_selector.py +2 -0
- {nova_trame-1.0.0rc5.dist-info → nova_trame-1.1.0.dist-info}/METADATA +9 -2
- {nova_trame-1.0.0rc5.dist-info → nova_trame-1.1.0.dist-info}/RECORD +19 -19
- {nova_trame-1.0.0rc5.dist-info → nova_trame-1.1.0.dist-info}/WHEEL +0 -0
- {nova_trame-1.0.0rc5.dist-info → nova_trame-1.1.0.dist-info}/entry_points.txt +0 -0
- {nova_trame-1.0.0rc5.dist-info → nova_trame-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
6
|
|
|
7
7
|
from natsort import natsorted
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
@@ -13,6 +13,11 @@ class DataSelectorState(BaseModel, validate_assignment=True):
|
|
|
13
13
|
|
|
14
14
|
directory: str = Field(default="")
|
|
15
15
|
extensions: List[str] = Field(default=[])
|
|
16
|
+
search: str = Field(default="", title="Search")
|
|
17
|
+
# True: A->Z, False: Z->A, None: no order
|
|
18
|
+
sort_alpha: Optional[bool] = Field(default=None)
|
|
19
|
+
# True: Recent modifications first, False: Older modifications first, None: no order
|
|
20
|
+
sort_time: Optional[bool] = Field(default=None)
|
|
16
21
|
subdirectory: str = Field(default="")
|
|
17
22
|
|
|
18
23
|
|
|
@@ -95,18 +100,25 @@ class DataSelectorModel:
|
|
|
95
100
|
datafile_path = base_path / self.state.subdirectory
|
|
96
101
|
|
|
97
102
|
for entry in os.scandir(datafile_path):
|
|
103
|
+
can_add = False
|
|
98
104
|
if entry.is_file():
|
|
99
105
|
if self.state.extensions:
|
|
100
106
|
for extension in self.state.extensions:
|
|
101
107
|
if entry.path.lower().endswith(extension):
|
|
102
|
-
|
|
108
|
+
can_add = True
|
|
103
109
|
break
|
|
104
110
|
else:
|
|
105
|
-
|
|
111
|
+
can_add = True
|
|
112
|
+
|
|
113
|
+
if self.state.search and self.state.search.lower() not in entry.path.lower():
|
|
114
|
+
can_add = False
|
|
115
|
+
|
|
116
|
+
if can_add:
|
|
117
|
+
datafiles.append((entry.path, entry.stat().st_mtime))
|
|
106
118
|
except OSError:
|
|
107
119
|
pass
|
|
108
120
|
|
|
109
|
-
return
|
|
121
|
+
return self.sort_datafiles(datafiles)
|
|
110
122
|
|
|
111
123
|
def get_datafiles(self) -> List[str]:
|
|
112
124
|
base_path = Path(self.state.directory)
|
|
@@ -115,3 +127,33 @@ class DataSelectorModel:
|
|
|
115
127
|
|
|
116
128
|
def set_subdirectory(self, subdirectory_path: str) -> None:
|
|
117
129
|
self.state.subdirectory = subdirectory_path
|
|
130
|
+
|
|
131
|
+
def sort_datafiles(self, files: List[Tuple[str, float]]) -> List[str]:
|
|
132
|
+
if self.state.sort_alpha is not None:
|
|
133
|
+
files = natsorted(files, key=lambda x: x[0].lower(), reverse=not self.state.sort_alpha)
|
|
134
|
+
elif self.state.sort_time is not None:
|
|
135
|
+
files = sorted(files, key=lambda x: x[1], reverse=self.state.sort_time)
|
|
136
|
+
|
|
137
|
+
return [file[0] for file in files]
|
|
138
|
+
|
|
139
|
+
def toggle_alpha_sort(self) -> None:
|
|
140
|
+
# Reset the time sort since we've changed alpha sort more recently
|
|
141
|
+
self.state.sort_time = None
|
|
142
|
+
|
|
143
|
+
if self.state.sort_alpha is None:
|
|
144
|
+
self.state.sort_alpha = True
|
|
145
|
+
elif self.state.sort_alpha:
|
|
146
|
+
self.state.sort_alpha = False
|
|
147
|
+
else:
|
|
148
|
+
self.state.sort_alpha = None
|
|
149
|
+
|
|
150
|
+
def toggle_time_sort(self) -> None:
|
|
151
|
+
# Reset the alpha sort since we've changed time sort more recently
|
|
152
|
+
self.state.sort_alpha = None
|
|
153
|
+
|
|
154
|
+
if self.state.sort_time is None:
|
|
155
|
+
self.state.sort_time = True
|
|
156
|
+
elif self.state.sort_time:
|
|
157
|
+
self.state.sort_time = False
|
|
158
|
+
else:
|
|
159
|
+
self.state.sort_time = None
|
|
@@ -11,7 +11,7 @@ from trame_server.core import State
|
|
|
11
11
|
|
|
12
12
|
from nova.mvvm._internal.utils import rgetdictvalue
|
|
13
13
|
from nova.mvvm.trame_binding import TrameBinding
|
|
14
|
-
from nova.trame._internal.utils import get_state_param, set_state_param
|
|
14
|
+
from nova.trame._internal.utils import get_state_name, get_state_param, set_state_param
|
|
15
15
|
from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
|
|
16
16
|
from nova.trame.view.layouts import GridLayout, HBoxLayout, VBoxLayout
|
|
17
17
|
from nova.trame.view_model.data_selector import DataSelectorViewModel
|
|
@@ -28,6 +28,7 @@ class DataSelector(datagrid.VGrid):
|
|
|
28
28
|
self,
|
|
29
29
|
v_model: Union[str, Tuple],
|
|
30
30
|
directory: Union[str, Tuple],
|
|
31
|
+
clear_selection_on_directory_change: Union[bool, Tuple] = True,
|
|
31
32
|
extensions: Union[List[str], Tuple, None] = None,
|
|
32
33
|
prefix: Union[str, Tuple] = "",
|
|
33
34
|
subdirectory: Union[str, Tuple] = "",
|
|
@@ -45,6 +46,8 @@ class DataSelector(datagrid.VGrid):
|
|
|
45
46
|
directory : Union[str, Tuple]
|
|
46
47
|
The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
|
|
47
48
|
users.
|
|
49
|
+
clear_selection_on_directory_change: Union[bool, Tuple], optional
|
|
50
|
+
Whether or not to clear the selected files when the directory is changed.
|
|
48
51
|
extensions : Union[List[str], Tuple], optional
|
|
49
52
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
|
50
53
|
prefix : Union[str, Tuple], optional
|
|
@@ -96,6 +99,7 @@ class DataSelector(datagrid.VGrid):
|
|
|
96
99
|
else:
|
|
97
100
|
self._v_model_name_in_state = v_model[0].split(".")[0]
|
|
98
101
|
|
|
102
|
+
self._clear_selection = clear_selection_on_directory_change
|
|
99
103
|
self._directory = directory
|
|
100
104
|
self._last_directory = get_state_param(self.state, self._directory)
|
|
101
105
|
self._extensions = extensions if extensions is not None else []
|
|
@@ -129,6 +133,8 @@ class DataSelector(datagrid.VGrid):
|
|
|
129
133
|
return get_server(None, client_type="vue3").state
|
|
130
134
|
|
|
131
135
|
def create_ui(self, *args: Any, **kwargs: Any) -> None:
|
|
136
|
+
show_directories = isinstance(self._subdirectory, tuple) or not self._subdirectory
|
|
137
|
+
|
|
132
138
|
with VBoxLayout(classes="nova-data-selector", stretch=True) as self._layout:
|
|
133
139
|
with HBoxLayout(valign="center"):
|
|
134
140
|
self._layout.filter = html.Div(classes="flex-1-1")
|
|
@@ -139,7 +145,7 @@ class DataSelector(datagrid.VGrid):
|
|
|
139
145
|
vuetify.VTooltip("Refresh Contents", activator="parent")
|
|
140
146
|
|
|
141
147
|
with GridLayout(columns=2, stretch=True):
|
|
142
|
-
if
|
|
148
|
+
if show_directories:
|
|
143
149
|
with VBoxLayout(stretch=True):
|
|
144
150
|
vuetify.VListSubheader("Available Directories", classes="flex-0-1 justify-center px-0")
|
|
145
151
|
vuetify.VTreeview(
|
|
@@ -155,51 +161,113 @@ class DataSelector(datagrid.VGrid):
|
|
|
155
161
|
)
|
|
156
162
|
vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
|
|
157
163
|
|
|
158
|
-
if
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
with VBoxLayout(column_span=1 if show_directories else 2, stretch=True):
|
|
165
|
+
with VBoxLayout(classes="mx-2", gap="0.5em"):
|
|
166
|
+
with HBoxLayout(gap="0.25em", valign="center"):
|
|
167
|
+
if isinstance(self._extensions, tuple):
|
|
168
|
+
extensions_name = f"{get_state_name(self._extensions[0])}.extensions"
|
|
169
|
+
else:
|
|
170
|
+
extensions_name = f"{self._state_name}.extensions"
|
|
171
|
+
|
|
172
|
+
InputField(v_model=f"{self._state_name}.search")
|
|
173
|
+
with vuetify.VBtn(classes="icon-btn", icon=True, click=self._vm.toggle_alpha_sort):
|
|
174
|
+
vuetify.VTooltip(
|
|
175
|
+
"Sorting A->Z",
|
|
176
|
+
activator="parent",
|
|
177
|
+
v_if=f"{self._state_name}.sort_alpha === true",
|
|
178
|
+
)
|
|
179
|
+
vuetify.VTooltip(
|
|
180
|
+
"Sorting Z->A",
|
|
181
|
+
activator="parent",
|
|
182
|
+
v_else_if=f"{self._state_name}.sort_alpha === false",
|
|
183
|
+
)
|
|
184
|
+
vuetify.VTooltip("Click to sort alphanumerically", activator="parent", v_else=True)
|
|
185
|
+
|
|
186
|
+
vuetify.VIcon(
|
|
187
|
+
"mdi-sort-alphabetical-ascending",
|
|
188
|
+
size=16,
|
|
189
|
+
v_if=f"{self._state_name}.sort_alpha === true",
|
|
190
|
+
)
|
|
191
|
+
vuetify.VIcon(
|
|
192
|
+
"mdi-sort-alphabetical-descending",
|
|
193
|
+
size=16,
|
|
194
|
+
v_else_if=f"{self._state_name}.sort_alpha === false",
|
|
195
|
+
)
|
|
196
|
+
vuetify.VIcon("mdi-order-alphabetical-ascending", size=16, v_else=True)
|
|
197
|
+
with vuetify.VBtn(classes="icon-btn", icon=True, click=self._vm.toggle_time_sort):
|
|
198
|
+
vuetify.VTooltip(
|
|
199
|
+
"Newest modification times first",
|
|
200
|
+
activator="parent",
|
|
201
|
+
v_if=f"{self._state_name}.sort_time === true",
|
|
202
|
+
)
|
|
203
|
+
vuetify.VTooltip(
|
|
204
|
+
"Oldest modification times first",
|
|
205
|
+
activator="parent",
|
|
206
|
+
v_else_if=f"{self._state_name}.sort_time === false",
|
|
207
|
+
)
|
|
208
|
+
vuetify.VTooltip("Click to sort by modification times", activator="parent", v_else=True)
|
|
209
|
+
|
|
210
|
+
vuetify.VIcon(
|
|
211
|
+
"mdi-sort-clock-ascending",
|
|
212
|
+
size=16,
|
|
213
|
+
v_if=f"{self._state_name}.sort_time === true",
|
|
214
|
+
)
|
|
215
|
+
vuetify.VIcon(
|
|
216
|
+
"mdi-sort-clock-descending",
|
|
217
|
+
size=16,
|
|
218
|
+
v_else_if=f"{self._state_name}.sort_time === false",
|
|
219
|
+
)
|
|
220
|
+
vuetify.VIcon("mdi-clock", size=16, v_else=True)
|
|
221
|
+
|
|
222
|
+
html.P(
|
|
223
|
+
f"Showing {{{{ {extensions_name}.join(',') }}}} files",
|
|
224
|
+
v_if=f"{extensions_name}.length > 0",
|
|
225
|
+
)
|
|
171
226
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
**kwargs,
|
|
185
|
-
)
|
|
186
|
-
if self._label:
|
|
187
|
-
self.label = self._label
|
|
188
|
-
if "update_modelValue" not in kwargs:
|
|
189
|
-
self.update_modelValue = self._flush_state
|
|
190
|
-
|
|
191
|
-
# Sets up some JavaScript event handlers when the component is mounted.
|
|
192
|
-
with self:
|
|
193
|
-
client.ClientTriggers(
|
|
194
|
-
mounted=(
|
|
195
|
-
"window.grid_manager.add("
|
|
196
|
-
f" '{self._revogrid_id}',"
|
|
197
|
-
f" '{self._v_model}',"
|
|
198
|
-
f" '{self._datafiles_name}',"
|
|
199
|
-
f" '{self._v_model_name_in_state}'"
|
|
200
|
-
")"
|
|
227
|
+
if "columns" in kwargs:
|
|
228
|
+
columns = kwargs.pop("columns")
|
|
229
|
+
else:
|
|
230
|
+
columns = (
|
|
231
|
+
"[{"
|
|
232
|
+
" cellTemplate: (createElement, props) =>"
|
|
233
|
+
f" window.grid_manager.get('{self._revogrid_id}').cellTemplate(createElement, props),"
|
|
234
|
+
" columnTemplate: (createElement) =>"
|
|
235
|
+
f" window.grid_manager.get('{self._revogrid_id}').columnTemplate(createElement),"
|
|
236
|
+
" name: 'Available Datafiles',"
|
|
237
|
+
" prop: 'title',"
|
|
238
|
+
"}]",
|
|
201
239
|
)
|
|
240
|
+
|
|
241
|
+
super().__init__(
|
|
242
|
+
v_model=self._v_model,
|
|
243
|
+
can_focus=False,
|
|
244
|
+
columns=columns,
|
|
245
|
+
frame_size=10,
|
|
246
|
+
hide_attribution=True,
|
|
247
|
+
id=self._revogrid_id,
|
|
248
|
+
readonly=True,
|
|
249
|
+
stretch=True,
|
|
250
|
+
source=(self._datafiles_name,),
|
|
251
|
+
theme="compact",
|
|
252
|
+
**kwargs,
|
|
202
253
|
)
|
|
254
|
+
if self._label:
|
|
255
|
+
self.label = self._label
|
|
256
|
+
if "update_modelValue" not in kwargs:
|
|
257
|
+
self.update_modelValue = self._flush_state
|
|
258
|
+
|
|
259
|
+
# Sets up some JavaScript event handlers when the component is mounted.
|
|
260
|
+
with self:
|
|
261
|
+
client.ClientTriggers(
|
|
262
|
+
mounted=(
|
|
263
|
+
"window.grid_manager.add("
|
|
264
|
+
f" '{self._revogrid_id}',"
|
|
265
|
+
f" '{self._v_model}',"
|
|
266
|
+
f" '{self._datafiles_name}',"
|
|
267
|
+
f" '{self._v_model_name_in_state}'"
|
|
268
|
+
")"
|
|
269
|
+
)
|
|
270
|
+
)
|
|
203
271
|
|
|
204
272
|
with cast(
|
|
205
273
|
vuetify.VSelect,
|
|
@@ -240,8 +308,9 @@ class DataSelector(datagrid.VGrid):
|
|
|
240
308
|
self._vm.update_view(refresh_directories=True)
|
|
241
309
|
|
|
242
310
|
def reset(self, _: Any = None) -> None:
|
|
243
|
-
self.
|
|
244
|
-
|
|
311
|
+
if bool(get_state_param(self.state, self._clear_selection)):
|
|
312
|
+
self._reset_state()
|
|
313
|
+
self._reset_rv_grid()
|
|
245
314
|
|
|
246
315
|
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
|
247
316
|
set_state_param(self.state, self._subdirectory, subdirectory_path)
|
|
@@ -31,6 +31,7 @@ class NeutronDataSelector(DataSelector):
|
|
|
31
31
|
self,
|
|
32
32
|
v_model: Union[str, Tuple],
|
|
33
33
|
allow_custom_directories: Union[bool, Tuple] = False,
|
|
34
|
+
clear_selection_on_experiment_change: Union[bool, Tuple] = True,
|
|
34
35
|
data_source: Literal["filesystem", "oncat"] = "filesystem",
|
|
35
36
|
facility: Union[str, Tuple] = "",
|
|
36
37
|
instrument: Union[str, Tuple] = "",
|
|
@@ -52,6 +53,8 @@ class NeutronDataSelector(DataSelector):
|
|
|
52
53
|
allow_custom_directories : Union[bool, Tuple], optional
|
|
53
54
|
Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
|
|
54
55
|
facility parameter is set.
|
|
56
|
+
clear_selection_on_experiment_change: Union[bool, Tuple], optional
|
|
57
|
+
Whether or not to clear the selected files when the user changes the facility, instrument, or experiment.
|
|
55
58
|
data_source : Literal["filesystem", "oncat"], optional
|
|
56
59
|
The source from which to pull datafiles. Defaults to "filesystem". If using ONCat, you will need to set the
|
|
57
60
|
following environment variables for local development: `ONCAT_CLIENT_ID` and `ONCAT_CLIENT_SECRET`. Note
|
|
@@ -91,7 +94,7 @@ class NeutronDataSelector(DataSelector):
|
|
|
91
94
|
if data_source == "oncat" and subdirectory:
|
|
92
95
|
warn("subdirectory will be ignored since data will be pulled from ONCat.", stacklevel=1)
|
|
93
96
|
|
|
94
|
-
if isinstance(facility, str) and allow_custom_directories:
|
|
97
|
+
if isinstance(facility, str) and facility and allow_custom_directories:
|
|
95
98
|
warn("allow_custom_directories will be ignored since the facility parameter is fixed.", stacklevel=1)
|
|
96
99
|
|
|
97
100
|
self._facility = facility
|
|
@@ -119,6 +122,7 @@ class NeutronDataSelector(DataSelector):
|
|
|
119
122
|
super().__init__(
|
|
120
123
|
v_model,
|
|
121
124
|
"",
|
|
125
|
+
clear_selection_on_directory_change=clear_selection_on_experiment_change,
|
|
122
126
|
extensions=extensions,
|
|
123
127
|
subdirectory=subdirectory if data_source == "filesystem" else "oncat",
|
|
124
128
|
refresh_rate=refresh_rate,
|
|
@@ -319,3 +323,9 @@ class NeutronDataSelector(DataSelector):
|
|
|
319
323
|
experiment=set_state_param(self.state, (self._selected_experiment_name,), experiment),
|
|
320
324
|
)
|
|
321
325
|
self._vm.reset()
|
|
326
|
+
|
|
327
|
+
def set_state(self, *args: Any, **kwargs: Any) -> None:
|
|
328
|
+
raise TypeError(
|
|
329
|
+
"The set_state method has been removed. Please use update_facility, update_instrument, and "
|
|
330
|
+
"update_experiment instead."
|
|
331
|
+
)
|
|
@@ -13,7 +13,7 @@ from warnings import warn
|
|
|
13
13
|
|
|
14
14
|
import tornado
|
|
15
15
|
from aiohttp import ClientSession, WSMsgType, web
|
|
16
|
-
from matplotlib import get_data_path
|
|
16
|
+
from matplotlib import get_data_path, rcParams
|
|
17
17
|
from matplotlib.backends.backend_webagg import FigureManagerWebAgg, new_figure_manager_given_figure # type: ignore
|
|
18
18
|
from matplotlib.figure import Figure
|
|
19
19
|
from trame.app import get_server
|
|
@@ -56,7 +56,10 @@ class _MPLApplication(tornado.web.Application):
|
|
|
56
56
|
self.supports_binary = message["value"]
|
|
57
57
|
else:
|
|
58
58
|
manager = self.application.manager # type: ignore
|
|
59
|
-
|
|
59
|
+
try:
|
|
60
|
+
manager.handle_json(message)
|
|
61
|
+
except Exception:
|
|
62
|
+
manager.refresh_all()
|
|
60
63
|
|
|
61
64
|
def send_json(self, content: Any) -> None:
|
|
62
65
|
set_event_loop(self.application.loop) # type: ignore
|
|
@@ -229,7 +232,6 @@ class MatplotlibFigure(matplotlib.Figure):
|
|
|
229
232
|
else:
|
|
230
233
|
kwargs["classes"] = "flex-1-1"
|
|
231
234
|
if webagg:
|
|
232
|
-
self._initial_resize = True
|
|
233
235
|
if "id" in kwargs:
|
|
234
236
|
kwargs.pop("id")
|
|
235
237
|
warn("id parameter to MatplotlibFigure is ignored when webagg=True.", stacklevel=1)
|
|
@@ -264,7 +266,7 @@ class MatplotlibFigure(matplotlib.Figure):
|
|
|
264
266
|
f"window.document.querySelectorAll('.nova-mpl').forEach((item) => {{ item.style.display = ''; }});"
|
|
265
267
|
"window.trame.trigger("
|
|
266
268
|
f" '{self._id}_resize',"
|
|
267
|
-
f" [height, width
|
|
269
|
+
f" [height, width]"
|
|
268
270
|
");"
|
|
269
271
|
)
|
|
270
272
|
self._resize_figure = client.JSEval(exec=self._trigger).exec
|
|
@@ -280,29 +282,21 @@ class MatplotlibFigure(matplotlib.Figure):
|
|
|
280
282
|
).exec
|
|
281
283
|
|
|
282
284
|
@self._server.controller.trigger(f"{self._id}_resize")
|
|
283
|
-
def resize_figure(height: int, width: int
|
|
285
|
+
def resize_figure(height: int, width: int) -> None:
|
|
284
286
|
if self._figure:
|
|
285
|
-
# This is the browser standard assumption for DPI.
|
|
286
|
-
dpi = 96
|
|
287
|
-
|
|
288
287
|
if self._webagg:
|
|
289
288
|
# Reserve space for the controls injected by webagg.
|
|
290
289
|
height -= 48
|
|
291
290
|
width -= 4
|
|
292
291
|
|
|
293
|
-
if not self._initial_resize:
|
|
294
|
-
# Handle device pixel ratio for retina displays
|
|
295
|
-
dpi = int(dpi * device_pixel_ratio)
|
|
296
|
-
height = int(height * device_pixel_ratio)
|
|
297
|
-
width = int(width * device_pixel_ratio)
|
|
298
|
-
|
|
299
292
|
if height <= 0 or width <= 0:
|
|
300
293
|
return
|
|
301
294
|
|
|
302
295
|
if self._webagg:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
# Webagg does not respect the Figure object's DPI.
|
|
297
|
+
dpi = rcParams["figure.dpi"]
|
|
298
|
+
else:
|
|
299
|
+
dpi = self._figure.get_dpi()
|
|
306
300
|
new_width = width / dpi
|
|
307
301
|
new_height = height / dpi
|
|
308
302
|
current_size = self._figure.get_size_inches()
|
nova/trame/view/layouts/grid.py
CHANGED
|
@@ -74,7 +74,7 @@ class GridLayout(html.Div):
|
|
|
74
74
|
classes = kwargs.pop("classes", [])
|
|
75
75
|
if isinstance(classes, list):
|
|
76
76
|
classes = " ".join(classes)
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
if stretch:
|
|
79
79
|
if valign:
|
|
80
80
|
warn("Ignoring valign parameter to GridLayout since stretch=True.", stacklevel=1)
|
|
@@ -83,6 +83,12 @@ class GridLayout(html.Div):
|
|
|
83
83
|
else:
|
|
84
84
|
classes += " flex-0-1"
|
|
85
85
|
|
|
86
|
+
v_show = kwargs.get("v_show", None)
|
|
87
|
+
if v_show:
|
|
88
|
+
classes = (f"{v_show} ? '{classes} d-grid' : '{classes}'",)
|
|
89
|
+
else:
|
|
90
|
+
classes += " d-grid"
|
|
91
|
+
|
|
86
92
|
widget_style = self.get_root_styles(columns, height, width, halign, valign, gap)
|
|
87
93
|
user_style = kwargs.pop("style", {})
|
|
88
94
|
|
nova/trame/view/layouts/hbox.py
CHANGED
|
@@ -63,12 +63,18 @@ class HBoxLayout(html.Div):
|
|
|
63
63
|
classes = kwargs.pop("classes", [])
|
|
64
64
|
if isinstance(classes, list):
|
|
65
65
|
classes = " ".join(classes)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
if stretch:
|
|
68
68
|
classes += " flex-1-1 overflow-y-auto"
|
|
69
69
|
else:
|
|
70
70
|
classes += " flex-0-1"
|
|
71
71
|
|
|
72
|
+
v_show = kwargs.get("v_show", None)
|
|
73
|
+
if v_show:
|
|
74
|
+
classes = (f"{v_show} ? '{classes} d-flex flex-row' : '{classes}'",)
|
|
75
|
+
else:
|
|
76
|
+
classes += " d-flex flex-row"
|
|
77
|
+
|
|
72
78
|
widget_style = self.get_root_styles(height, width, halign, valign, gap, vspace)
|
|
73
79
|
user_style = kwargs.pop("style", {})
|
|
74
80
|
|
nova/trame/view/layouts/vbox.py
CHANGED
|
@@ -63,12 +63,18 @@ class VBoxLayout(html.Div):
|
|
|
63
63
|
classes = kwargs.pop("classes", [])
|
|
64
64
|
if isinstance(classes, list):
|
|
65
65
|
classes = " ".join(classes)
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
if stretch:
|
|
68
68
|
classes += " flex-1-1 overflow-y-auto"
|
|
69
69
|
else:
|
|
70
70
|
classes += " flex-0-1"
|
|
71
71
|
|
|
72
|
+
v_show = kwargs.get("v_show", None)
|
|
73
|
+
if v_show:
|
|
74
|
+
classes = (f"{v_show} ? '{classes} d-flex flex-column' : '{classes}'",)
|
|
75
|
+
else:
|
|
76
|
+
classes += " d-flex flex-column"
|
|
77
|
+
|
|
72
78
|
widget_style = self.get_root_styles(height, width, halign, valign, gap, vspace)
|
|
73
79
|
user_style = kwargs.pop("style", {})
|
|
74
80
|
|
|
@@ -12,6 +12,13 @@ html {
|
|
|
12
12
|
box-shadow: none !important;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
.exit-button {
|
|
16
|
+
.v-btn__content,
|
|
17
|
+
.v-btn__prepend i {
|
|
18
|
+
font-size: 0.875rem !important;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
.mpl-message, .ui-dialog-titlebar {
|
|
16
23
|
display: none !important;
|
|
17
24
|
}
|
|
@@ -20,6 +27,10 @@ html {
|
|
|
20
27
|
white-space: pre-wrap;
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
.nova-pre-content:empty {
|
|
31
|
+
margin-bottom: 12px !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
.nova-data-selector {
|
|
24
35
|
.v-list-group {
|
|
25
36
|
--prepend-width: 12px !important;
|
|
@@ -47,11 +58,27 @@ html {
|
|
|
47
58
|
cursor: pointer;
|
|
48
59
|
margin-right: 0.25em;
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
p {
|
|
63
|
+
line-height: 1;
|
|
64
|
+
margin-top: 1em;
|
|
65
|
+
white-space: normal;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.rgCell label {
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.rv-row-text {
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
text-overflow: ellipsis;
|
|
76
|
+
white-space: nowrap;
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
.header-content {
|
|
53
80
|
font-weight: 500;
|
|
54
|
-
height:
|
|
81
|
+
height: fit-content;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
.inner-content-table {
|
|
@@ -86,6 +113,8 @@ html {
|
|
|
86
113
|
|
|
87
114
|
@media only screen and (max-width: 959px) {
|
|
88
115
|
.d-grid {
|
|
116
|
+
/* This forces all grid rows to have the same height. If one doesn't want this to happen, then they should use the box layouts. :) */
|
|
117
|
+
grid-auto-rows: 1fr;
|
|
89
118
|
grid-template-columns: repeat(1, 1fr) !important;
|
|
90
119
|
}
|
|
91
120
|
|
|
@@ -183,7 +212,7 @@ html {
|
|
|
183
212
|
font-size: 1rem;
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
.v-btn {
|
|
215
|
+
.v-btn:not(.exit-button) {
|
|
187
216
|
min-width: 0px !important;
|
|
188
217
|
padding: 5px 5px !important;
|
|
189
218
|
box-shadow: none !important;
|
|
@@ -244,6 +273,14 @@ html {
|
|
|
244
273
|
}
|
|
245
274
|
}
|
|
246
275
|
}
|
|
276
|
+
|
|
277
|
+
.v-treeview.hide-actions .v-list-item .v-list-item__prepend {
|
|
278
|
+
width: unset;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.nova-data-selector .icon-btn {
|
|
282
|
+
height: 28px;
|
|
283
|
+
}
|
|
247
284
|
}
|
|
248
285
|
|
|
249
286
|
|
|
Binary file
|
|
@@ -4,11 +4,15 @@ class RevoGrid {
|
|
|
4
4
|
this.modelKey = modelKey
|
|
5
5
|
this.dataKey = dataKey
|
|
6
6
|
this.stateKey = stateKey
|
|
7
|
+
this.lastSelection = null
|
|
8
|
+
this.shiftPressed = false
|
|
7
9
|
|
|
8
10
|
this.grid = document.querySelector(`#${this.id}`)
|
|
9
11
|
this.grid.addEventListener('viewportscroll', () => {
|
|
10
12
|
this.updateCheckboxes()
|
|
11
13
|
})
|
|
14
|
+
|
|
15
|
+
this.initShiftKeyListeners()
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
updateCheckboxes() {
|
|
@@ -66,11 +70,32 @@ class RevoGrid {
|
|
|
66
70
|
const path = props.data[props.rowIndex].path
|
|
67
71
|
const index = modelValue.indexOf(path)
|
|
68
72
|
|
|
69
|
-
//
|
|
73
|
+
// I use _.set instead of modifying the modelValue in place in order for the Trame watcher to properly detect the change.
|
|
70
74
|
if (e.target.checked && index < 0) {
|
|
71
|
-
|
|
75
|
+
const newIndex = props.data.findIndex((entry) => entry.path === path)
|
|
76
|
+
|
|
77
|
+
if (this.shiftPressed && this.lastSelection !== null) {
|
|
78
|
+
let newPaths = []
|
|
79
|
+
// JavaScript doesn't allow a backwards step during slice, so we need to order the start/stop correctly.
|
|
80
|
+
if (this.lastSelection < newIndex) {
|
|
81
|
+
newPaths = props.data.slice(this.lastSelection, newIndex + 1)
|
|
82
|
+
} else {
|
|
83
|
+
newPaths = props.data.slice(newIndex, this.lastSelection)
|
|
84
|
+
}
|
|
85
|
+
// Exclude paths that are already selected to avoid duplicates.
|
|
86
|
+
newPaths = newPaths.map((entry) => entry.path).filter((path) => !modelValue.includes(path))
|
|
87
|
+
|
|
88
|
+
_.set(trameState, this.modelKey, _.concat(modelValue, newPaths))
|
|
89
|
+
} else {
|
|
90
|
+
_.set(trameState, this.modelKey, _.concat(modelValue, path))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.lastSelection = newIndex
|
|
72
94
|
} else if (index >= 0) {
|
|
73
95
|
_.set(trameState, this.modelKey, modelValue.toSpliced(index, 1))
|
|
96
|
+
|
|
97
|
+
// Only allow range selection if the last action was to select a file.
|
|
98
|
+
this.lastSelection = null
|
|
74
99
|
}
|
|
75
100
|
|
|
76
101
|
// Update the UI
|
|
@@ -79,16 +104,18 @@ class RevoGrid {
|
|
|
79
104
|
},
|
|
80
105
|
})
|
|
81
106
|
|
|
82
|
-
|
|
107
|
+
const spanNode = createElement('span', {'class': 'cursor-pointer rv-row-text'}, props.model[props.prop])
|
|
108
|
+
|
|
109
|
+
return createElement('label', { 'title': props.model[props.prop] }, inputVNode, spanNode)
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
columnTemplate(createElement) {
|
|
113
|
+
const trameState = window.trame.state.state
|
|
114
|
+
const availableData = _.get(trameState, this.dataKey)
|
|
115
|
+
|
|
86
116
|
const inputVNode = createElement('input', {
|
|
87
117
|
type: 'checkbox',
|
|
88
118
|
onChange: (e) => {
|
|
89
|
-
const trameState = window.trame.state.state
|
|
90
|
-
const availableData = _.get(trameState, this.dataKey)
|
|
91
|
-
|
|
92
119
|
if (e.target.checked) {
|
|
93
120
|
_.set(trameState, this.modelKey, availableData.map((item) => item.path))
|
|
94
121
|
} else {
|
|
@@ -100,8 +127,26 @@ class RevoGrid {
|
|
|
100
127
|
window.trame.state.dirty(this.stateKey)
|
|
101
128
|
},
|
|
102
129
|
})
|
|
130
|
+
const header = createElement('div', {'class': 'd-flex'}, inputVNode, 'Available Datafiles')
|
|
103
131
|
|
|
104
|
-
|
|
132
|
+
let controls = null
|
|
133
|
+
if (availableData.length < 1) {
|
|
134
|
+
controls = createElement('p', {}, 'No files found. Select a directory with files on the left.')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return createElement('div', {'class': 'd-flex flex-column'}, header, controls)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
initShiftKeyListeners() {
|
|
141
|
+
window.document.addEventListener('keydown', (e) => {
|
|
142
|
+
this.shiftPressed = e.shiftKey
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
window.document.addEventListener('keyup', (e) => {
|
|
146
|
+
if (e.key === 'Shift') {
|
|
147
|
+
this.shiftPressed = false
|
|
148
|
+
}
|
|
149
|
+
})
|
|
105
150
|
}
|
|
106
151
|
}
|
|
107
152
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Components used to control the lifecycle of a Themed Application."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from asyncio import sleep
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from trame.app import get_server
|
|
8
|
+
from trame.widgets import client
|
|
7
9
|
from trame.widgets import vuetify3 as vuetify
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
@@ -20,6 +22,10 @@ class ExitButton:
|
|
|
20
22
|
self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
|
|
21
23
|
self.server.state.nova_running_jobs = []
|
|
22
24
|
self.server.state.nova_show_exit_progress = False
|
|
25
|
+
# Note that window.close() will fail in many situations due to security concerns: https://developer.mozilla.org/en-US/docs/Web/API/Window/close
|
|
26
|
+
# This is simply a best effort to close the tab. My hope is that it will generally work when people are running
|
|
27
|
+
# applications through our dashboard since tabs are opened via target="_blank" links.
|
|
28
|
+
self.close_browser = client.JSEval(exec="window.close();").exec
|
|
23
29
|
self.exit_application_callback = exit_callback
|
|
24
30
|
self.job_status_callback = job_status_callback
|
|
25
31
|
self.create_ui()
|
|
@@ -28,9 +34,10 @@ class ExitButton:
|
|
|
28
34
|
with vuetify.VBtn(
|
|
29
35
|
"Exit",
|
|
30
36
|
prepend_icon="mdi-close-box",
|
|
31
|
-
classes="mr-4 bg-
|
|
37
|
+
classes="exit-button mr-4 bg-secondary",
|
|
32
38
|
id="shutdown_app_theme_button",
|
|
33
39
|
color="white",
|
|
40
|
+
size="default",
|
|
34
41
|
click=self.open_exit_dialog,
|
|
35
42
|
):
|
|
36
43
|
with vuetify.VDialog(v_model="nova_show_exit_dialog", persistent="true"):
|
|
@@ -51,7 +58,7 @@ class ExitButton:
|
|
|
51
58
|
with vuetify.VCardActions(v_if="!nova_show_exit_progress"):
|
|
52
59
|
vuetify.VBtn(
|
|
53
60
|
"Exit App",
|
|
54
|
-
click=self.
|
|
61
|
+
click=self.exit_application,
|
|
55
62
|
color="error",
|
|
56
63
|
)
|
|
57
64
|
vuetify.VBtn(
|
|
@@ -65,6 +72,13 @@ class ExitButton:
|
|
|
65
72
|
)
|
|
66
73
|
vuetify.VProgressCircular(indeterminate=True)
|
|
67
74
|
|
|
75
|
+
async def exit_application(self) -> None:
|
|
76
|
+
self.close_browser()
|
|
77
|
+
|
|
78
|
+
# sleep gives time for the Trame server to communicate the close request to the browser.
|
|
79
|
+
await sleep(0.1)
|
|
80
|
+
await self.exit_application_callback()
|
|
81
|
+
|
|
68
82
|
async def open_exit_dialog(self) -> None:
|
|
69
83
|
self.server.state.nova_show_exit_dialog = True
|
|
70
84
|
await self.job_status_callback()
|
nova/trame/view/theme/theme.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from asyncio import create_task
|
|
8
9
|
from functools import partial
|
|
@@ -210,6 +211,10 @@ class ThemedApp:
|
|
|
210
211
|
-------
|
|
211
212
|
`trame_client.ui.core.AbstractLayout <https://trame.readthedocs.io/en/latest/core.ui.html#trame_client.ui.core.AbstractLayout>`_
|
|
212
213
|
"""
|
|
214
|
+
# This detects if Poetry is running Python so that we can show links to NOVA resources during development.
|
|
215
|
+
# Poetry should not be used in production.
|
|
216
|
+
show_nova_resources = os.environ.get("VIRTUAL_ENV") is not None
|
|
217
|
+
|
|
213
218
|
with VAppLayout(self.server, vuetify_config=self.vuetify_config) as layout:
|
|
214
219
|
self.local_storage = LocalStorageManager(self.server.controller)
|
|
215
220
|
|
|
@@ -225,8 +230,32 @@ class ThemedApp:
|
|
|
225
230
|
with vuetify.VAppBar() as toolbar:
|
|
226
231
|
layout.toolbar = toolbar
|
|
227
232
|
|
|
228
|
-
with vuetify.VAppBarTitle() as toolbar_title:
|
|
233
|
+
with vuetify.VAppBarTitle(classes="flex-0-1") as toolbar_title:
|
|
229
234
|
layout.toolbar_title = toolbar_title
|
|
235
|
+
|
|
236
|
+
if show_nova_resources:
|
|
237
|
+
vuetify.VBtn(
|
|
238
|
+
"NOVA Examples",
|
|
239
|
+
classes="ml-4",
|
|
240
|
+
href="https://github.com/nova-model/nova-examples/",
|
|
241
|
+
__properties=["target"],
|
|
242
|
+
target="_blank",
|
|
243
|
+
)
|
|
244
|
+
html.Div("·", classes="mx-1")
|
|
245
|
+
vuetify.VBtn(
|
|
246
|
+
"NOVA Tutorial",
|
|
247
|
+
href="https://nova.ornl.gov/tutorial/",
|
|
248
|
+
__properties=["target"],
|
|
249
|
+
target="_blank",
|
|
250
|
+
)
|
|
251
|
+
html.Div("·", classes="mx-1")
|
|
252
|
+
vuetify.VBtn(
|
|
253
|
+
"NOVA Documentation",
|
|
254
|
+
href="https://nova-application-development.readthedocs.io/en/latest/",
|
|
255
|
+
__properties=["target"],
|
|
256
|
+
target="_blank",
|
|
257
|
+
)
|
|
258
|
+
|
|
230
259
|
vuetify.VSpacer()
|
|
231
260
|
with html.Div(classes="mr-2") as actions:
|
|
232
261
|
layout.actions = actions
|
|
@@ -259,7 +288,7 @@ class ThemedApp:
|
|
|
259
288
|
|
|
260
289
|
with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
|
|
261
290
|
# [slot override example]
|
|
262
|
-
layout.pre_content = vuetify.VSheet(classes="bg-background flex-0-1 mt-1 ")
|
|
291
|
+
layout.pre_content = vuetify.VSheet(classes="bg-background flex-0-1 mt-1 nova-pre-content ")
|
|
263
292
|
# [slot override example complete]
|
|
264
293
|
with vuetify.VContainer(classes="flex-1-1 overflow-hidden pt-0 pb-2", fluid=True):
|
|
265
294
|
layout.content = vuetify.VCard(
|
|
@@ -56,7 +56,9 @@ class DataSelectorViewModel:
|
|
|
56
56
|
self.expand_directory(paths)
|
|
57
57
|
|
|
58
58
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
|
59
|
-
|
|
59
|
+
for result in results.get("updated", []):
|
|
60
|
+
if result == "search":
|
|
61
|
+
self.update_view()
|
|
60
62
|
|
|
61
63
|
def set_binding_parameters(self, **kwargs: Any) -> None:
|
|
62
64
|
self.model.set_binding_parameters(**kwargs)
|
|
@@ -66,6 +68,14 @@ class DataSelectorViewModel:
|
|
|
66
68
|
self.model.set_subdirectory(subdirectory_path)
|
|
67
69
|
self.update_view()
|
|
68
70
|
|
|
71
|
+
def toggle_alpha_sort(self) -> None:
|
|
72
|
+
self.model.toggle_alpha_sort()
|
|
73
|
+
self.update_view()
|
|
74
|
+
|
|
75
|
+
def toggle_time_sort(self) -> None:
|
|
76
|
+
self.model.toggle_time_sort()
|
|
77
|
+
self.update_view()
|
|
78
|
+
|
|
69
79
|
def transform_datafiles(self, datafiles: List[Any]) -> List[Dict[str, str]]:
|
|
70
80
|
return [{"path": datafile, "title": os.path.basename(datafile)} for datafile in datafiles]
|
|
71
81
|
|
|
@@ -33,6 +33,8 @@ class NeutronDataSelectorViewModel(DataSelectorViewModel):
|
|
|
33
33
|
case "custom_directory":
|
|
34
34
|
self.reset()
|
|
35
35
|
self.update_view()
|
|
36
|
+
case "search":
|
|
37
|
+
self.update_view()
|
|
36
38
|
|
|
37
39
|
def transform_datafiles(self, datafiles: List[Any]) -> List[Dict[str, str]]:
|
|
38
40
|
return [{"title": os.path.basename(datafile["path"]), **datafile} for datafile in datafiles]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nova-trame
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A Python Package for injecting curated themes and custom components into Trame applications
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
nova-trame
|
|
39
39
|
==========
|
|
40
40
|
|
|
41
|
-
`nova-trame` is a Python package for streamlining development of Trame applications used in the NOVA
|
|
41
|
+
`nova-trame` is a Python package for streamlining development of Trame applications used in the NOVA framework.
|
|
42
42
|
|
|
43
43
|
You can install this package directly with
|
|
44
44
|
|
|
@@ -46,6 +46,13 @@ You can install this package directly with
|
|
|
46
46
|
pip install nova-trame
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Once installed, you can check that it's working by running the widget gallery we use for visually testing components available through NOVA and Trame. We use [Poetry](https://python-poetry.org/) internally and recommend it for running the gallery.
|
|
50
|
+
|
|
51
|
+
```commandline
|
|
52
|
+
poetry install
|
|
53
|
+
poetry run app
|
|
54
|
+
```
|
|
55
|
+
|
|
49
56
|
A user guide, examples, and a full API for this package can be found at [https://nova-application-development.readthedocs.io/en/stable/](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/).
|
|
50
57
|
|
|
51
58
|
Developers: please read [this document](DEVELOPMENT.md)
|
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
|
2
2
|
nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
|
|
3
3
|
nova/trame/_internal/utils.py,sha256=lTTJnfqbbIe21Tg2buf5MXqKUEUop7Va5PZgpWMzRkI,1381
|
|
4
|
-
nova/trame/model/data_selector.py,sha256=
|
|
4
|
+
nova/trame/model/data_selector.py,sha256=_pGKvKoz9QheY4Zyi5dy9XjHuo75MoiN1J928J09dSo,6180
|
|
5
5
|
nova/trame/model/ornl/analysis_data_selector.py,sha256=P7IEJdqCAUsEOCof4c2JantPXd9vz0EtvhryEKvscbw,5544
|
|
6
6
|
nova/trame/model/ornl/neutron_data_selector.py,sha256=YMoNEpDKgjP_y18oYj-N9IjkxtqwHz9JYMlURQA4BCE,2148
|
|
7
7
|
nova/trame/model/ornl/oncat_data_selector.py,sha256=3JEkWGMU-esWA9DUTglju9hEP9LyZ7EUXLj1yO5BIDs,4755
|
|
8
8
|
nova/trame/model/remote_file_input.py,sha256=eAk7ZsFgNKcnpJ6KmOQDhiI6pPZpcrr1GMKkRLEWht8,4338
|
|
9
9
|
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
|
10
|
-
nova/trame/view/components/data_selector.py,sha256=
|
|
10
|
+
nova/trame/view/components/data_selector.py,sha256=yQSxlotZX21pEZsKJKFEVQytN5PbjUCBMHPsROa4tjI,19127
|
|
11
11
|
nova/trame/view/components/execution_buttons.py,sha256=Br6uAmE5bY67TTYc5ZTHECNJ_RJqKmv17HAKPpQtbeg,4576
|
|
12
12
|
nova/trame/view/components/file_upload.py,sha256=WOaFXeNNwN0DYZJr-W6vWdBiTpr7m-lq3WKJaHmeMe8,4560
|
|
13
13
|
nova/trame/view/components/input_field.py,sha256=xzCmNEoB4ljGx99-gGgTV0UwriwtS8ce22zPA4QneZw,17372
|
|
14
14
|
nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
|
|
15
|
-
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=
|
|
15
|
+
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=m_XAXdqTelUj6x1kj0kBq2dClVytTQqZ4a6fgNxGloA,17149
|
|
16
16
|
nova/trame/view/components/progress_bar.py,sha256=zhbJwPy_HPQ8YL-ISN8sCRUQ7qY6qqo9wiV59BmvL8I,3038
|
|
17
17
|
nova/trame/view/components/remote_file_input.py,sha256=mcz_bmI2rD8gdmIOKLhlzfj-XoWBwC99T9ZgQORaKqE,14674
|
|
18
18
|
nova/trame/view/components/tool_outputs.py,sha256=IbYV4VjrkWAE354Bh5KH76SPsxGLIkOXChijS4-ce_Y,2408
|
|
19
19
|
nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
|
|
20
20
|
nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=z2s1janxAclpMEdDJk3z-CQ6r3KPNoR_SXPx9ppWnuQ,3481
|
|
21
|
-
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=
|
|
21
|
+
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=8JgbF6elZC8EFZn-c2mLqGSyg0Lb4NLVAKJVSBb9-5g,16010
|
|
22
22
|
nova/trame/view/layouts/__init__.py,sha256=cMrlB5YMUoK8EGB83b34UU0kPTVrH8AxsYvKRtpUNEc,141
|
|
23
|
-
nova/trame/view/layouts/grid.py,sha256=
|
|
24
|
-
nova/trame/view/layouts/hbox.py,sha256=
|
|
23
|
+
nova/trame/view/layouts/grid.py,sha256=3zm9rVa-cexqKCJXZPIGpfrZIbi7qaAGYSmUg7njTS4,6265
|
|
24
|
+
nova/trame/view/layouts/hbox.py,sha256=EOh-GT2rkt5qsvpBLjtrwdHlOA3hopcpFo7_TpxDOVs,4014
|
|
25
25
|
nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
|
|
26
|
-
nova/trame/view/layouts/vbox.py,sha256=
|
|
26
|
+
nova/trame/view/layouts/vbox.py,sha256=DrhBjDe2m4V42JV7Ma2YSPkJyaI6k74Yc9U1MgyuFfQ,4018
|
|
27
27
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
|
28
|
-
nova/trame/view/theme/assets/core_style.scss,sha256=
|
|
29
|
-
nova/trame/view/theme/assets/favicon.png,sha256=
|
|
28
|
+
nova/trame/view/theme/assets/core_style.scss,sha256=IR3xekkhiJen1HVYOjbb2wB8JG80_7INzDtgosAdkOU,5290
|
|
29
|
+
nova/trame/view/theme/assets/favicon.png,sha256=F5r6SjZet8uARzESJgwRZGK_Q7pDguDG11hyOWIBOc4,11812
|
|
30
30
|
nova/trame/view/theme/assets/js/delay_manager.js,sha256=BN4OL88QsyZG4XQ1sTorHpN1rwD4GnWoVKHvl5F5ydo,776
|
|
31
31
|
nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
|
|
32
|
-
nova/trame/view/theme/assets/js/revo_grid.js,sha256=
|
|
32
|
+
nova/trame/view/theme/assets/js/revo_grid.js,sha256=KSUc8fOoX_BrOVtzIHcLscAiS4qLUclrbo1ndD2cj-A,6128
|
|
33
33
|
nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
|
|
34
|
-
nova/trame/view/theme/exit_button.py,sha256=
|
|
35
|
-
nova/trame/view/theme/theme.py,sha256=
|
|
34
|
+
nova/trame/view/theme/exit_button.py,sha256=GrgCMLytIrDTWAMtkuFv5JaIBpslFrmI5k2izklSqQs,3872
|
|
35
|
+
nova/trame/view/theme/theme.py,sha256=YecxrUw5I-7NDTyC8jrVdpb49U1TMzIbwO7jDid_jN0,14715
|
|
36
36
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
|
37
|
-
nova/trame/view_model/data_selector.py,sha256=
|
|
37
|
+
nova/trame/view_model/data_selector.py,sha256=Z_aLHcxatxneyjUBwrus79p_4YQcolmYohNktlraI9k,3507
|
|
38
38
|
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
|
39
|
-
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=
|
|
39
|
+
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=PcnUUNN3S1i-Tc9G4ZZP6NPGcVhHtA0tx9cwlZ5g_5Y,1848
|
|
40
40
|
nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
|
|
41
41
|
nova/trame/view_model/remote_file_input.py,sha256=zWOflmCDJYYR_pacHphwzricV667GSRokh-mlxpBAOo,3646
|
|
42
42
|
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
|
43
|
-
nova_trame-1.0.
|
|
44
|
-
nova_trame-1.0.
|
|
45
|
-
nova_trame-1.0.
|
|
46
|
-
nova_trame-1.0.
|
|
47
|
-
nova_trame-1.0.
|
|
43
|
+
nova_trame-1.1.0.dist-info/METADATA,sha256=zhUMQdY9OepYShQefWIQ_zwhLP197w9ecz0_G_QSDPU,2096
|
|
44
|
+
nova_trame-1.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
45
|
+
nova_trame-1.1.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
|
46
|
+
nova_trame-1.1.0.dist-info/licenses/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
|
47
|
+
nova_trame-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|