nova-trame 0.22.0__py3-none-any.whl → 0.23.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nova/trame/model/data_selector.py +25 -162
- nova/trame/model/ornl/neutron_data_selector.py +193 -0
- nova/trame/view/components/data_selector.py +29 -80
- nova/trame/view/components/ornl/__init__.py +3 -0
- nova/trame/view/components/ornl/neutron_data_selector.py +151 -0
- nova/trame/view/theme/assets/core_style.scss +8 -2
- nova/trame/view/theme/exit_button.py +73 -0
- nova/trame/view/theme/theme.py +29 -0
- nova/trame/view_model/data_selector.py +8 -34
- nova/trame/view_model/ornl/neutron_data_selector.py +51 -0
- {nova_trame-0.22.0.dist-info → nova_trame-0.23.0.dist-info}/METADATA +4 -4
- {nova_trame-0.22.0.dist-info → nova_trame-0.23.0.dist-info}/RECORD +15 -10
- {nova_trame-0.22.0.dist-info → nova_trame-0.23.0.dist-info}/LICENSE +0 -0
- {nova_trame-0.22.0.dist-info → nova_trame-0.23.0.dist-info}/WHEEL +0 -0
- {nova_trame-0.22.0.dist-info → nova_trame-0.23.0.dist-info}/entry_points.txt +0 -0
@@ -3,142 +3,29 @@
|
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
5
|
from typing import Any, Dict, List, Optional
|
6
|
-
from warnings import warn
|
7
6
|
|
8
7
|
from natsort import natsorted
|
9
|
-
from pydantic import BaseModel, Field
|
10
|
-
from typing_extensions import Self
|
11
|
-
|
12
|
-
CUSTOM_DIRECTORIES_LABEL = "Custom Directory"
|
13
|
-
|
14
|
-
INSTRUMENTS = {
|
15
|
-
"HFIR": {
|
16
|
-
"CG-1A": "CG1A",
|
17
|
-
"CG-1B": "CG1B",
|
18
|
-
"CG-1D": "CG1D",
|
19
|
-
"CG-2": "CG2",
|
20
|
-
"CG-3": "CG3",
|
21
|
-
"CG-4B": "CG4B",
|
22
|
-
"CG-4C": "CG4C",
|
23
|
-
"CG-4D": "CG4D",
|
24
|
-
"HB-1": "HB1",
|
25
|
-
"HB-1A": "HB1A",
|
26
|
-
"HB-2A": "HB2A",
|
27
|
-
"HB-2B": "HB2B",
|
28
|
-
"HB-2C": "HB2C",
|
29
|
-
"HB-3": "HB3",
|
30
|
-
"HB-3A": "HB3A",
|
31
|
-
"NOW-G": "NOWG",
|
32
|
-
"NOW-V": "NOWV",
|
33
|
-
},
|
34
|
-
"SNS": {
|
35
|
-
"BL-18": "ARCS",
|
36
|
-
"BL-0": "BL0",
|
37
|
-
"BL-2": "BSS",
|
38
|
-
"BL-5": "CNCS",
|
39
|
-
"BL-9": "CORELLI",
|
40
|
-
"BL-6": "EQSANS",
|
41
|
-
"BL-14B": "HYS",
|
42
|
-
"BL-11B": "MANDI",
|
43
|
-
"BL-1B": "NOM",
|
44
|
-
"NOW-G": "NOWG",
|
45
|
-
"BL-15": "NSE",
|
46
|
-
"BL-11A": "PG3",
|
47
|
-
"BL-4B": "REF_L",
|
48
|
-
"BL-4A": "REF_M",
|
49
|
-
"BL-17": "SEQ",
|
50
|
-
"BL-3": "SNAP",
|
51
|
-
"BL-12": "TOPAZ",
|
52
|
-
"BL-1A": "USANS",
|
53
|
-
"BL-10": "VENUS",
|
54
|
-
"BL-16B": "VIS",
|
55
|
-
"BL-7": "VULCAN",
|
56
|
-
},
|
57
|
-
}
|
8
|
+
from pydantic import BaseModel, Field
|
58
9
|
|
59
10
|
|
60
11
|
class DataSelectorState(BaseModel, validate_assignment=True):
|
61
12
|
"""Selection state for identifying datafiles."""
|
62
13
|
|
63
|
-
allow_custom_directories: bool = Field(default=False)
|
64
|
-
facility: str = Field(default="", title="Facility")
|
65
|
-
instrument: str = Field(default="", title="Instrument")
|
66
|
-
experiment: str = Field(default="", title="Experiment")
|
67
|
-
custom_directory: str = Field(default="", title="Custom Directory")
|
68
14
|
directory: str = Field(default="")
|
15
|
+
subdirectory: str = Field(default="")
|
69
16
|
extensions: List[str] = Field(default=[])
|
70
17
|
prefix: str = Field(default="")
|
71
18
|
|
72
|
-
@field_validator("experiment", mode="after")
|
73
|
-
@classmethod
|
74
|
-
def validate_experiment(cls, experiment: str) -> str:
|
75
|
-
if experiment and not experiment.startswith("IPTS-"):
|
76
|
-
raise ValueError("experiment must begin with IPTS-")
|
77
|
-
return experiment
|
78
|
-
|
79
|
-
@model_validator(mode="after")
|
80
|
-
def validate_state(self) -> Self:
|
81
|
-
valid_facilities = self.get_facilities()
|
82
|
-
if self.facility and self.facility not in valid_facilities:
|
83
|
-
warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
|
84
|
-
|
85
|
-
valid_instruments = self.get_instruments()
|
86
|
-
if self.instrument and self.facility != CUSTOM_DIRECTORIES_LABEL and self.instrument not in valid_instruments:
|
87
|
-
warn(
|
88
|
-
(
|
89
|
-
f"Instrument '{self.instrument}' could not be found in '{self.facility}'. "
|
90
|
-
f"Valid options: {valid_instruments}"
|
91
|
-
),
|
92
|
-
stacklevel=1,
|
93
|
-
)
|
94
|
-
# Validating the experiment is expensive and will fail in our CI due to the filesystem not being mounted there.
|
95
|
-
|
96
|
-
return self
|
97
|
-
|
98
|
-
def get_facilities(self) -> List[str]:
|
99
|
-
facilities = list(INSTRUMENTS.keys())
|
100
|
-
if self.allow_custom_directories:
|
101
|
-
facilities.append(CUSTOM_DIRECTORIES_LABEL)
|
102
|
-
return facilities
|
103
|
-
|
104
|
-
def get_instruments(self) -> List[str]:
|
105
|
-
return list(INSTRUMENTS.get(self.facility, {}).keys())
|
106
|
-
|
107
19
|
|
108
20
|
class DataSelectorModel:
|
109
21
|
"""Manages file system interactions for the DataSelector widget."""
|
110
22
|
|
111
|
-
def __init__(
|
112
|
-
self
|
113
|
-
|
114
|
-
self.state =
|
115
|
-
self.state.facility = facility
|
116
|
-
self.state.instrument = instrument
|
23
|
+
def __init__(self, state: DataSelectorState, directory: str, extensions: List[str], prefix: str) -> None:
|
24
|
+
self.state: DataSelectorState = state
|
25
|
+
|
26
|
+
self.state.directory = directory
|
117
27
|
self.state.extensions = extensions
|
118
28
|
self.state.prefix = prefix
|
119
|
-
self.state.allow_custom_directories = allow_custom_directories
|
120
|
-
|
121
|
-
def get_facilities(self) -> List[str]:
|
122
|
-
return natsorted(self.state.get_facilities())
|
123
|
-
|
124
|
-
def get_instrument_dir(self) -> str:
|
125
|
-
return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
|
126
|
-
|
127
|
-
def get_instruments(self) -> List[str]:
|
128
|
-
return natsorted(self.state.get_instruments())
|
129
|
-
|
130
|
-
def get_experiments(self) -> List[str]:
|
131
|
-
experiments = []
|
132
|
-
|
133
|
-
instrument_path = Path("/") / self.state.facility / self.get_instrument_dir()
|
134
|
-
try:
|
135
|
-
for dirname in os.listdir(instrument_path):
|
136
|
-
if dirname.startswith("IPTS-") and os.access(instrument_path / dirname, mode=os.R_OK):
|
137
|
-
experiments.append(dirname)
|
138
|
-
except OSError:
|
139
|
-
pass
|
140
|
-
|
141
|
-
return natsorted(experiments)
|
142
29
|
|
143
30
|
def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
144
31
|
# Sort the current level of dictionaries
|
@@ -151,31 +38,7 @@ class DataSelectorModel:
|
|
151
38
|
|
152
39
|
return sorted_dirs
|
153
40
|
|
154
|
-
def
|
155
|
-
if not self.state.experiment:
|
156
|
-
return None
|
157
|
-
|
158
|
-
return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
159
|
-
|
160
|
-
def get_custom_directory_path(self) -> Optional[Path]:
|
161
|
-
# Don't expose the full file system
|
162
|
-
if not self.state.custom_directory:
|
163
|
-
return None
|
164
|
-
|
165
|
-
return Path(self.state.custom_directory)
|
166
|
-
|
167
|
-
def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
|
168
|
-
using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
|
169
|
-
if base_path:
|
170
|
-
pass
|
171
|
-
elif using_custom_directory:
|
172
|
-
base_path = self.get_custom_directory_path()
|
173
|
-
else:
|
174
|
-
base_path = self.get_experiment_directory_path()
|
175
|
-
|
176
|
-
if not base_path:
|
177
|
-
return []
|
178
|
-
|
41
|
+
def get_directories_from_path(self, base_path: Path) -> List[Dict[str, Any]]:
|
179
42
|
directories = []
|
180
43
|
try:
|
181
44
|
for dirpath, dirs, _ in os.walk(base_path):
|
@@ -208,21 +71,24 @@ class DataSelectorModel:
|
|
208
71
|
|
209
72
|
return self.sort_directories(directories)
|
210
73
|
|
211
|
-
def
|
212
|
-
|
213
|
-
|
214
|
-
if self.state.experiment:
|
215
|
-
base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
216
|
-
elif self.state.custom_directory:
|
217
|
-
base_path = Path(self.state.custom_directory)
|
74
|
+
def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
|
75
|
+
if base_path:
|
76
|
+
pass
|
218
77
|
else:
|
78
|
+
base_path = Path(self.state.directory)
|
79
|
+
|
80
|
+
if not base_path:
|
219
81
|
return []
|
220
82
|
|
83
|
+
return self.get_directories_from_path(base_path)
|
84
|
+
|
85
|
+
def get_datafiles_from_path(self, base_path: Path) -> List[str]:
|
86
|
+
datafiles = []
|
221
87
|
try:
|
222
88
|
if self.state.prefix:
|
223
|
-
datafile_path =
|
89
|
+
datafile_path = base_path / self.state.prefix
|
224
90
|
else:
|
225
|
-
datafile_path =
|
91
|
+
datafile_path = base_path / self.state.subdirectory
|
226
92
|
|
227
93
|
for entry in os.scandir(datafile_path):
|
228
94
|
if entry.is_file():
|
@@ -237,13 +103,10 @@ class DataSelectorModel:
|
|
237
103
|
|
238
104
|
return natsorted(datafiles)
|
239
105
|
|
240
|
-
def
|
241
|
-
self.state.directory
|
106
|
+
def get_datafiles(self) -> List[str]:
|
107
|
+
base_path = Path(self.state.directory)
|
108
|
+
|
109
|
+
return self.get_datafiles_from_path(base_path)
|
242
110
|
|
243
|
-
def
|
244
|
-
|
245
|
-
self.state.facility = facility
|
246
|
-
if instrument is not None:
|
247
|
-
self.state.instrument = instrument
|
248
|
-
if experiment is not None:
|
249
|
-
self.state.experiment = experiment
|
111
|
+
def set_subdirectory(self, subdirectory_path: str) -> None:
|
112
|
+
self.state.subdirectory = subdirectory_path
|
@@ -0,0 +1,193 @@
|
|
1
|
+
"""Model implementation for DataSelector."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
|
+
from warnings import warn
|
7
|
+
|
8
|
+
from natsort import natsorted
|
9
|
+
from pydantic import Field, field_validator, model_validator
|
10
|
+
from typing_extensions import Self
|
11
|
+
|
12
|
+
from ..data_selector import DataSelectorModel, DataSelectorState
|
13
|
+
|
14
|
+
CUSTOM_DIRECTORIES_LABEL = "Custom Directory"
|
15
|
+
|
16
|
+
INSTRUMENTS = {
|
17
|
+
"HFIR": {
|
18
|
+
"CG-1A": "CG1A",
|
19
|
+
"CG-1B": "CG1B",
|
20
|
+
"CG-1D": "CG1D",
|
21
|
+
"CG-2": "CG2",
|
22
|
+
"CG-3": "CG3",
|
23
|
+
"CG-4B": "CG4B",
|
24
|
+
"CG-4C": "CG4C",
|
25
|
+
"CG-4D": "CG4D",
|
26
|
+
"HB-1": "HB1",
|
27
|
+
"HB-1A": "HB1A",
|
28
|
+
"HB-2A": "HB2A",
|
29
|
+
"HB-2B": "HB2B",
|
30
|
+
"HB-2C": "HB2C",
|
31
|
+
"HB-3": "HB3",
|
32
|
+
"HB-3A": "HB3A",
|
33
|
+
"NOW-G": "NOWG",
|
34
|
+
"NOW-V": "NOWV",
|
35
|
+
},
|
36
|
+
"SNS": {
|
37
|
+
"BL-18": "ARCS",
|
38
|
+
"BL-0": "BL0",
|
39
|
+
"BL-2": "BSS",
|
40
|
+
"BL-5": "CNCS",
|
41
|
+
"BL-9": "CORELLI",
|
42
|
+
"BL-6": "EQSANS",
|
43
|
+
"BL-14B": "HYS",
|
44
|
+
"BL-11B": "MANDI",
|
45
|
+
"BL-1B": "NOM",
|
46
|
+
"NOW-G": "NOWG",
|
47
|
+
"BL-15": "NSE",
|
48
|
+
"BL-11A": "PG3",
|
49
|
+
"BL-4B": "REF_L",
|
50
|
+
"BL-4A": "REF_M",
|
51
|
+
"BL-17": "SEQ",
|
52
|
+
"BL-3": "SNAP",
|
53
|
+
"BL-12": "TOPAZ",
|
54
|
+
"BL-1A": "USANS",
|
55
|
+
"BL-10": "VENUS",
|
56
|
+
"BL-16B": "VIS",
|
57
|
+
"BL-7": "VULCAN",
|
58
|
+
},
|
59
|
+
}
|
60
|
+
|
61
|
+
|
62
|
+
class NeutronDataSelectorState(DataSelectorState):
|
63
|
+
"""Selection state for identifying datafiles."""
|
64
|
+
|
65
|
+
allow_custom_directories: bool = Field(default=False)
|
66
|
+
facility: str = Field(default="", title="Facility")
|
67
|
+
instrument: str = Field(default="", title="Instrument")
|
68
|
+
experiment: str = Field(default="", title="Experiment")
|
69
|
+
custom_directory: str = Field(default="", title="Custom Directory")
|
70
|
+
|
71
|
+
@field_validator("experiment", mode="after")
|
72
|
+
@classmethod
|
73
|
+
def validate_experiment(cls, experiment: str) -> str:
|
74
|
+
if experiment and not experiment.startswith("IPTS-"):
|
75
|
+
raise ValueError("experiment must begin with IPTS-")
|
76
|
+
return experiment
|
77
|
+
|
78
|
+
@model_validator(mode="after")
|
79
|
+
def validate_state(self) -> Self:
|
80
|
+
valid_facilities = self.get_facilities()
|
81
|
+
if self.facility and self.facility not in valid_facilities:
|
82
|
+
warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
|
83
|
+
|
84
|
+
valid_instruments = self.get_instruments()
|
85
|
+
if self.instrument and self.facility != CUSTOM_DIRECTORIES_LABEL and self.instrument not in valid_instruments:
|
86
|
+
warn(
|
87
|
+
(
|
88
|
+
f"Instrument '{self.instrument}' could not be found in '{self.facility}'. "
|
89
|
+
f"Valid options: {valid_instruments}"
|
90
|
+
),
|
91
|
+
stacklevel=1,
|
92
|
+
)
|
93
|
+
# Validating the experiment is expensive and will fail in our CI due to the filesystem not being mounted there.
|
94
|
+
|
95
|
+
return self
|
96
|
+
|
97
|
+
def get_facilities(self) -> List[str]:
|
98
|
+
facilities = list(INSTRUMENTS.keys())
|
99
|
+
if self.allow_custom_directories:
|
100
|
+
facilities.append(CUSTOM_DIRECTORIES_LABEL)
|
101
|
+
return facilities
|
102
|
+
|
103
|
+
def get_instruments(self) -> List[str]:
|
104
|
+
return list(INSTRUMENTS.get(self.facility, {}).keys())
|
105
|
+
|
106
|
+
|
107
|
+
class NeutronDataSelectorModel(DataSelectorModel):
|
108
|
+
"""Manages file system interactions for the DataSelector widget."""
|
109
|
+
|
110
|
+
def __init__(
|
111
|
+
self,
|
112
|
+
state: NeutronDataSelectorState,
|
113
|
+
facility: str,
|
114
|
+
instrument: str,
|
115
|
+
extensions: List[str],
|
116
|
+
prefix: str,
|
117
|
+
allow_custom_directories: bool,
|
118
|
+
) -> None:
|
119
|
+
super().__init__(state, "", extensions, prefix)
|
120
|
+
self.state: NeutronDataSelectorState = state
|
121
|
+
|
122
|
+
self.state.facility = facility
|
123
|
+
self.state.instrument = instrument
|
124
|
+
self.state.extensions = extensions
|
125
|
+
self.state.prefix = prefix
|
126
|
+
self.state.allow_custom_directories = allow_custom_directories
|
127
|
+
|
128
|
+
def get_facilities(self) -> List[str]:
|
129
|
+
return natsorted(self.state.get_facilities())
|
130
|
+
|
131
|
+
def get_instrument_dir(self) -> str:
|
132
|
+
return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
|
133
|
+
|
134
|
+
def get_instruments(self) -> List[str]:
|
135
|
+
return natsorted(self.state.get_instruments())
|
136
|
+
|
137
|
+
def get_experiments(self) -> List[str]:
|
138
|
+
experiments = []
|
139
|
+
|
140
|
+
instrument_path = Path("/") / self.state.facility / self.get_instrument_dir()
|
141
|
+
try:
|
142
|
+
for dirname in os.listdir(instrument_path):
|
143
|
+
if dirname.startswith("IPTS-") and os.access(instrument_path / dirname, mode=os.R_OK):
|
144
|
+
experiments.append(dirname)
|
145
|
+
except OSError:
|
146
|
+
pass
|
147
|
+
|
148
|
+
return natsorted(experiments)
|
149
|
+
|
150
|
+
def get_experiment_directory_path(self) -> Optional[Path]:
|
151
|
+
if not self.state.experiment:
|
152
|
+
return None
|
153
|
+
|
154
|
+
return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
155
|
+
|
156
|
+
def get_custom_directory_path(self) -> Optional[Path]:
|
157
|
+
# Don't expose the full file system
|
158
|
+
if not self.state.custom_directory:
|
159
|
+
return None
|
160
|
+
|
161
|
+
return Path(self.state.custom_directory)
|
162
|
+
|
163
|
+
def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
|
164
|
+
using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
|
165
|
+
if base_path:
|
166
|
+
pass
|
167
|
+
elif using_custom_directory:
|
168
|
+
base_path = self.get_custom_directory_path()
|
169
|
+
else:
|
170
|
+
base_path = self.get_experiment_directory_path()
|
171
|
+
|
172
|
+
if not base_path:
|
173
|
+
return []
|
174
|
+
|
175
|
+
return self.get_directories_from_path(base_path)
|
176
|
+
|
177
|
+
def get_datafiles(self) -> List[str]:
|
178
|
+
if self.state.experiment:
|
179
|
+
base_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
180
|
+
elif self.state.custom_directory:
|
181
|
+
base_path = Path(self.state.custom_directory)
|
182
|
+
else:
|
183
|
+
return []
|
184
|
+
|
185
|
+
return self.get_datafiles_from_path(base_path)
|
186
|
+
|
187
|
+
def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
|
188
|
+
if facility is not None:
|
189
|
+
self.state.facility = facility
|
190
|
+
if instrument is not None:
|
191
|
+
self.state.instrument = instrument
|
192
|
+
if experiment is not None:
|
193
|
+
self.state.experiment = experiment
|
@@ -1,14 +1,13 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
3
|
from typing import Any, List, Optional, cast
|
4
|
-
from warnings import warn
|
5
4
|
|
6
5
|
from trame.app import get_server
|
7
6
|
from trame.widgets import client, datagrid, html
|
8
7
|
from trame.widgets import vuetify3 as vuetify
|
9
8
|
|
10
9
|
from nova.mvvm.trame_binding import TrameBinding
|
11
|
-
from nova.trame.model.data_selector import
|
10
|
+
from nova.trame.model.data_selector import DataSelectorModel, DataSelectorState
|
12
11
|
from nova.trame.view.layouts import GridLayout, VBoxLayout
|
13
12
|
from nova.trame.view_model.data_selector import DataSelectorViewModel
|
14
13
|
|
@@ -18,14 +17,12 @@ vuetify.enable_lab()
|
|
18
17
|
|
19
18
|
|
20
19
|
class DataSelector(datagrid.VGrid):
|
21
|
-
"""Allows the user to select datafiles from
|
20
|
+
"""Allows the user to select datafiles from the server."""
|
22
21
|
|
23
22
|
def __init__(
|
24
23
|
self,
|
25
24
|
v_model: str,
|
26
|
-
|
27
|
-
facility: str = "",
|
28
|
-
instrument: str = "",
|
25
|
+
directory: str,
|
29
26
|
extensions: Optional[List[str]] = None,
|
30
27
|
prefix: str = "",
|
31
28
|
select_strategy: str = "all",
|
@@ -38,18 +35,14 @@ class DataSelector(datagrid.VGrid):
|
|
38
35
|
v_model : str
|
39
36
|
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
40
37
|
selected by the user.
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
facility : str, optional
|
45
|
-
The facility to restrict data selection to. Options: HFIR, SNS
|
46
|
-
instrument : str, optional
|
47
|
-
The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
|
38
|
+
directory : str
|
39
|
+
The top-level folder to expose to users. Only contents of this directory and its children will be exposed to
|
40
|
+
users.
|
48
41
|
extensions : List[str], optional
|
49
42
|
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
50
43
|
prefix : str, optional
|
51
|
-
A subdirectory within the
|
52
|
-
folder browser and will be able to see all files in the
|
44
|
+
A subdirectory within the selected top-level folder to show files. If not specified, the user will be shown
|
45
|
+
a folder browser and will be able to see all files in the selected top-level folder.
|
53
46
|
select_strategy : str, optional
|
54
47
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
55
48
|
If unset, the `all` strategy will be used.
|
@@ -61,6 +54,12 @@ class DataSelector(datagrid.VGrid):
|
|
61
54
|
-------
|
62
55
|
None
|
63
56
|
"""
|
57
|
+
if "allow_custom_directory" in kwargs or "facility" in kwargs or "instrument" in kwargs:
|
58
|
+
raise TypeError(
|
59
|
+
"The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
|
60
|
+
"`nova.trame.view.components.ornl`."
|
61
|
+
)
|
62
|
+
|
64
63
|
if "items" in kwargs:
|
65
64
|
raise AttributeError("The items parameter is not allowed on DataSelector widget.")
|
66
65
|
|
@@ -69,21 +68,15 @@ class DataSelector(datagrid.VGrid):
|
|
69
68
|
else:
|
70
69
|
self._label = None
|
71
70
|
|
72
|
-
if facility and allow_custom_directories:
|
73
|
-
warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
|
74
|
-
|
75
71
|
self._v_model = v_model
|
76
72
|
self._v_model_name_in_state = v_model.split(".")[0]
|
77
|
-
self.
|
73
|
+
self._directory = directory
|
78
74
|
self._extensions = extensions if extensions is not None else []
|
79
75
|
self._prefix = prefix
|
80
76
|
self._select_strategy = select_strategy
|
81
77
|
|
82
78
|
self._revogrid_id = f"nova__dataselector_{self._next_id}_rv"
|
83
79
|
self._state_name = f"nova__dataselector_{self._next_id}_state"
|
84
|
-
self._facilities_name = f"nova__dataselector_{self._next_id}_facilities"
|
85
|
-
self._instruments_name = f"nova__dataselector_{self._next_id}_instruments"
|
86
|
-
self._experiments_name = f"nova__dataselector_{self._next_id}_experiments"
|
87
80
|
self._directories_name = f"nova__dataselector_{self._next_id}_directories"
|
88
81
|
self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"
|
89
82
|
|
@@ -93,36 +86,14 @@ class DataSelector(datagrid.VGrid):
|
|
93
86
|
).exec
|
94
87
|
self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
|
95
88
|
|
96
|
-
self.create_model(
|
89
|
+
self.create_model()
|
97
90
|
self.create_viewmodel()
|
98
91
|
|
99
|
-
self.create_ui(
|
92
|
+
self.create_ui(**kwargs)
|
100
93
|
|
101
|
-
def create_ui(self,
|
102
|
-
with VBoxLayout(classes="nova-data-selector", height="100%"):
|
103
|
-
|
104
|
-
columns = 3
|
105
|
-
if facility == "":
|
106
|
-
columns -= 1
|
107
|
-
InputField(
|
108
|
-
v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
|
109
|
-
)
|
110
|
-
if instrument == "":
|
111
|
-
columns -= 1
|
112
|
-
InputField(
|
113
|
-
v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
114
|
-
v_model=f"{self._state_name}.instrument",
|
115
|
-
items=(self._instruments_name,),
|
116
|
-
type="autocomplete",
|
117
|
-
)
|
118
|
-
InputField(
|
119
|
-
v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
120
|
-
v_model=f"{self._state_name}.experiment",
|
121
|
-
column_span=columns,
|
122
|
-
items=(self._experiments_name,),
|
123
|
-
type="autocomplete",
|
124
|
-
)
|
125
|
-
InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
|
94
|
+
def create_ui(self, *args: Any, **kwargs: Any) -> None:
|
95
|
+
with VBoxLayout(classes="nova-data-selector", height="100%") as self._layout:
|
96
|
+
self._layout.filter = html.Div()
|
126
97
|
|
127
98
|
with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
|
128
99
|
if not self._prefix:
|
@@ -137,7 +108,7 @@ class DataSelector(datagrid.VGrid):
|
|
137
108
|
item_value="path",
|
138
109
|
items=(self._directories_name,),
|
139
110
|
click_open=(self._vm.expand_directory, "[$event.path]"),
|
140
|
-
update_activated=(self._vm.
|
111
|
+
update_activated=(self._vm.set_subdirectory, "$event"),
|
141
112
|
)
|
142
113
|
vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
|
143
114
|
|
@@ -198,10 +169,9 @@ class DataSelector(datagrid.VGrid):
|
|
198
169
|
f"(+{{{{ {self._v_model}.length - 2 }}}} others)", v_if="index === 2", classes="text-caption"
|
199
170
|
)
|
200
171
|
|
201
|
-
def create_model(self
|
202
|
-
|
203
|
-
|
204
|
-
)
|
172
|
+
def create_model(self) -> None:
|
173
|
+
state = DataSelectorState()
|
174
|
+
self._model = DataSelectorModel(state, self._directory, self._extensions, self._prefix)
|
205
175
|
|
206
176
|
def create_viewmodel(self) -> None:
|
207
177
|
server = get_server(None, client_type="vue3")
|
@@ -209,9 +179,6 @@ class DataSelector(datagrid.VGrid):
|
|
209
179
|
|
210
180
|
self._vm = DataSelectorViewModel(self._model, binding)
|
211
181
|
self._vm.state_bind.connect(self._state_name)
|
212
|
-
self._vm.facilities_bind.connect(self._facilities_name)
|
213
|
-
self._vm.instruments_bind.connect(self._instruments_name)
|
214
|
-
self._vm.experiments_bind.connect(self._experiments_name)
|
215
182
|
self._vm.directories_bind.connect(self._directories_name)
|
216
183
|
self._vm.datafiles_bind.connect(self._datafiles_name)
|
217
184
|
self._vm.reset_bind.connect(self.reset)
|
@@ -222,26 +189,8 @@ class DataSelector(datagrid.VGrid):
|
|
222
189
|
self._reset_state()
|
223
190
|
self._reset_rv_grid()
|
224
191
|
|
225
|
-
def set_state(
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
If a parameter is None, then it will not be updated.
|
231
|
-
|
232
|
-
Parameters
|
233
|
-
----------
|
234
|
-
facility : str, optional
|
235
|
-
The facility to restrict data selection to. Options: HFIR, SNS
|
236
|
-
instrument : str, optional
|
237
|
-
The instrument to restrict data selection to. Must be at the selected facility.
|
238
|
-
experiment : str, optional
|
239
|
-
The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
|
240
|
-
that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
|
241
|
-
shown to the user.
|
242
|
-
|
243
|
-
Returns
|
244
|
-
-------
|
245
|
-
None
|
246
|
-
"""
|
247
|
-
self._vm.set_state(facility, instrument, experiment)
|
192
|
+
def set_state(self, *args: Any, **kwargs: Any) -> None:
|
193
|
+
raise TypeError(
|
194
|
+
"The old DataSelector component has been renamed to NeutronDataSelector. Please import it from "
|
195
|
+
"`nova.trame.view.components.ornl`."
|
196
|
+
)
|
@@ -0,0 +1,151 @@
|
|
1
|
+
"""View Implementation for DataSelector."""
|
2
|
+
|
3
|
+
from typing import Any, List, Optional
|
4
|
+
from warnings import warn
|
5
|
+
|
6
|
+
from trame.app import get_server
|
7
|
+
from trame.widgets import vuetify3 as vuetify
|
8
|
+
|
9
|
+
from nova.mvvm.trame_binding import TrameBinding
|
10
|
+
from nova.trame.model.ornl.neutron_data_selector import (
|
11
|
+
CUSTOM_DIRECTORIES_LABEL,
|
12
|
+
NeutronDataSelectorModel,
|
13
|
+
NeutronDataSelectorState,
|
14
|
+
)
|
15
|
+
from nova.trame.view.layouts import GridLayout
|
16
|
+
from nova.trame.view_model.ornl.neutron_data_selector import NeutronDataSelectorViewModel
|
17
|
+
|
18
|
+
from ..data_selector import DataSelector
|
19
|
+
from ..input_field import InputField
|
20
|
+
|
21
|
+
vuetify.enable_lab()
|
22
|
+
|
23
|
+
|
24
|
+
class NeutronDataSelector(DataSelector):
|
25
|
+
"""Allows the user to select datafiles from an IPTS experiment."""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
v_model: str,
|
30
|
+
allow_custom_directories: bool = False,
|
31
|
+
facility: str = "",
|
32
|
+
instrument: str = "",
|
33
|
+
extensions: Optional[List[str]] = None,
|
34
|
+
prefix: str = "",
|
35
|
+
select_strategy: str = "all",
|
36
|
+
**kwargs: Any,
|
37
|
+
) -> None:
|
38
|
+
"""Constructor for DataSelector.
|
39
|
+
|
40
|
+
Parameters
|
41
|
+
----------
|
42
|
+
v_model : str
|
43
|
+
The name of the state variable to bind to this widget. The state variable will contain a list of the files
|
44
|
+
selected by the user.
|
45
|
+
allow_custom_directories : bool, optional
|
46
|
+
Whether or not to allow users to provide their own directories to search for datafiles in. Ignored if the
|
47
|
+
facility parameter is set.
|
48
|
+
facility : str, optional
|
49
|
+
The facility to restrict data selection to. Options: HFIR, SNS
|
50
|
+
instrument : str, optional
|
51
|
+
The instrument to restrict data selection to. Please use the instrument acronym (e.g. CG-2).
|
52
|
+
extensions : List[str], optional
|
53
|
+
A list of file extensions to restrict selection to. If unset, then all files will be shown.
|
54
|
+
prefix : str, optional
|
55
|
+
A subdirectory within the user's chosen experiment to show files. If not specified, the user will be shown a
|
56
|
+
folder browser and will be able to see all files in the experiment that they have access to.
|
57
|
+
select_strategy : str, optional
|
58
|
+
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
59
|
+
If unset, the `all` strategy will be used.
|
60
|
+
**kwargs
|
61
|
+
All other arguments will be passed to the underlying
|
62
|
+
`VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
|
63
|
+
|
64
|
+
Returns
|
65
|
+
-------
|
66
|
+
None
|
67
|
+
"""
|
68
|
+
if facility and allow_custom_directories:
|
69
|
+
warn("allow_custom_directories will be ignored since the facility parameter is set.", stacklevel=1)
|
70
|
+
|
71
|
+
self._facility = facility
|
72
|
+
self._instrument = instrument
|
73
|
+
self._allow_custom_directories = allow_custom_directories
|
74
|
+
|
75
|
+
self._facilities_name = f"nova__neutrondataselector_{self._next_id}_facilities"
|
76
|
+
self._instruments_name = f"nova__neutrondataselector_{self._next_id}_instruments"
|
77
|
+
self._experiments_name = f"nova__neutrondataselector_{self._next_id}_experiments"
|
78
|
+
|
79
|
+
super().__init__(v_model, "", extensions, prefix, select_strategy, **kwargs)
|
80
|
+
|
81
|
+
def create_ui(self, **kwargs: Any) -> None:
|
82
|
+
super().create_ui(**kwargs)
|
83
|
+
with self._layout.filter:
|
84
|
+
with GridLayout(columns=3):
|
85
|
+
columns = 3
|
86
|
+
if self._facility == "":
|
87
|
+
columns -= 1
|
88
|
+
InputField(
|
89
|
+
v_model=f"{self._state_name}.facility", items=(self._facilities_name,), type="autocomplete"
|
90
|
+
)
|
91
|
+
if self._instrument == "":
|
92
|
+
columns -= 1
|
93
|
+
InputField(
|
94
|
+
v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
95
|
+
v_model=f"{self._state_name}.instrument",
|
96
|
+
items=(self._instruments_name,),
|
97
|
+
type="autocomplete",
|
98
|
+
)
|
99
|
+
InputField(
|
100
|
+
v_if=f"{self._state_name}.facility !== '{CUSTOM_DIRECTORIES_LABEL}'",
|
101
|
+
v_model=f"{self._state_name}.experiment",
|
102
|
+
column_span=columns,
|
103
|
+
items=(self._experiments_name,),
|
104
|
+
type="autocomplete",
|
105
|
+
)
|
106
|
+
InputField(v_else=True, v_model=f"{self._state_name}.custom_directory", column_span=2)
|
107
|
+
|
108
|
+
def create_model(self) -> None:
|
109
|
+
state = NeutronDataSelectorState()
|
110
|
+
self._model: NeutronDataSelectorModel = NeutronDataSelectorModel(
|
111
|
+
state, self._facility, self._instrument, self._extensions, self._prefix, self._allow_custom_directories
|
112
|
+
)
|
113
|
+
|
114
|
+
def create_viewmodel(self) -> None:
|
115
|
+
server = get_server(None, client_type="vue3")
|
116
|
+
binding = TrameBinding(server.state)
|
117
|
+
|
118
|
+
self._vm: NeutronDataSelectorViewModel = NeutronDataSelectorViewModel(self._model, binding)
|
119
|
+
self._vm.state_bind.connect(self._state_name)
|
120
|
+
self._vm.facilities_bind.connect(self._facilities_name)
|
121
|
+
self._vm.instruments_bind.connect(self._instruments_name)
|
122
|
+
self._vm.experiments_bind.connect(self._experiments_name)
|
123
|
+
self._vm.directories_bind.connect(self._directories_name)
|
124
|
+
self._vm.datafiles_bind.connect(self._datafiles_name)
|
125
|
+
self._vm.reset_bind.connect(self.reset)
|
126
|
+
|
127
|
+
self._vm.update_view()
|
128
|
+
|
129
|
+
def set_state(
|
130
|
+
self, facility: Optional[str] = None, instrument: Optional[str] = None, experiment: Optional[str] = None
|
131
|
+
) -> None:
|
132
|
+
"""Programmatically set the facility, instrument, and/or experiment to restrict data selection to.
|
133
|
+
|
134
|
+
If a parameter is None, then it will not be updated.
|
135
|
+
|
136
|
+
Parameters
|
137
|
+
----------
|
138
|
+
facility : str, optional
|
139
|
+
The facility to restrict data selection to. Options: HFIR, SNS
|
140
|
+
instrument : str, optional
|
141
|
+
The instrument to restrict data selection to. Must be at the selected facility.
|
142
|
+
experiment : str, optional
|
143
|
+
The experiment to restrict data selection to. Must begin with "IPTS-". It is your responsibility to validate
|
144
|
+
that the provided experiment exists within the instrument directory. If it doesn't then no datafiles will be
|
145
|
+
shown to the user.
|
146
|
+
|
147
|
+
Returns
|
148
|
+
-------
|
149
|
+
None
|
150
|
+
"""
|
151
|
+
self._vm.set_state(facility, instrument, experiment)
|
@@ -22,8 +22,14 @@ html {
|
|
22
22
|
|
23
23
|
.nova-data-selector {
|
24
24
|
.v-list-group {
|
25
|
-
|
26
|
-
|
25
|
+
--prepend-width: 12px !important;
|
26
|
+
}
|
27
|
+
|
28
|
+
.v-treeview.v-list {
|
29
|
+
--indent-padding: 0px !important;
|
30
|
+
|
31
|
+
& > .v-list-group > .v-list-item {
|
32
|
+
padding-left: 0 !important;
|
27
33
|
}
|
28
34
|
}
|
29
35
|
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""Components used to control the lifecycle of a Themed Application."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from trame.app import get_server
|
7
|
+
from trame.widgets import vuetify3 as vuetify
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
logger.setLevel(logging.INFO)
|
11
|
+
|
12
|
+
|
13
|
+
class ExitButton:
|
14
|
+
"""Exit button for Trame Applications."""
|
15
|
+
|
16
|
+
def __init__(self, exit_callback: Any = None, job_status_callback: Any = None) -> None:
|
17
|
+
self.server = get_server(None, client_type="vue3")
|
18
|
+
self.server.state.nova_kill_jobs_on_exit = False
|
19
|
+
self.server.state.nova_show_exit_dialog = False
|
20
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
|
21
|
+
self.server.state.nova_running_jobs = []
|
22
|
+
self.server.state.nova_show_exit_progress = False
|
23
|
+
self.exit_application_callback = exit_callback
|
24
|
+
self.job_status_callback = job_status_callback
|
25
|
+
self.create_ui()
|
26
|
+
|
27
|
+
def create_ui(self) -> None:
|
28
|
+
with vuetify.VBtn(
|
29
|
+
"Exit",
|
30
|
+
prepend_icon="mdi-close-box",
|
31
|
+
classes="mr-4 bg-error",
|
32
|
+
id="shutdown_app_theme_button",
|
33
|
+
color="white",
|
34
|
+
click=self.open_exit_dialog,
|
35
|
+
):
|
36
|
+
with vuetify.VDialog(v_model="nova_show_exit_dialog", persistent="true"):
|
37
|
+
with vuetify.VCard(classes="pa-4 ma-auto"):
|
38
|
+
vuetify.VCardTitle("Exit Application")
|
39
|
+
with vuetify.VCardText(
|
40
|
+
"Are you sure you want to exit this application?",
|
41
|
+
variant="outlined",
|
42
|
+
):
|
43
|
+
vuetify.VCheckbox(
|
44
|
+
v_model="nova_kill_jobs_on_exit",
|
45
|
+
label="Stop All Jobs On Exit.",
|
46
|
+
v_if="nova_running_jobs.length > 0",
|
47
|
+
)
|
48
|
+
with vuetify.VList():
|
49
|
+
vuetify.VListSubheader("Running Jobs:", v_if="nova_running_jobs.length > 0")
|
50
|
+
vuetify.VListItem("{{ item }}", v_for="(item, index) in nova_running_jobs")
|
51
|
+
with vuetify.VCardActions(v_if="!nova_show_exit_progress"):
|
52
|
+
vuetify.VBtn(
|
53
|
+
"Exit App",
|
54
|
+
click=self.exit_application_callback,
|
55
|
+
color="error",
|
56
|
+
)
|
57
|
+
vuetify.VBtn(
|
58
|
+
"Stay In App",
|
59
|
+
click=self.close_exit_dialog,
|
60
|
+
)
|
61
|
+
with vuetify.VCardActions(v_else=True):
|
62
|
+
vuetify.VCardText(
|
63
|
+
"Exiting Application...",
|
64
|
+
variant="outlined",
|
65
|
+
)
|
66
|
+
vuetify.VProgressCircular(indeterminate=True)
|
67
|
+
|
68
|
+
async def open_exit_dialog(self) -> None:
|
69
|
+
self.server.state.nova_show_exit_dialog = True
|
70
|
+
await self.job_status_callback()
|
71
|
+
|
72
|
+
async def close_exit_dialog(self) -> None:
|
73
|
+
self.server.state.nova_show_exit_dialog = False
|
nova/trame/view/theme/theme.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
"""Implementation of ThemedApp."""
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
import json
|
4
5
|
import logging
|
6
|
+
import sys
|
5
7
|
from asyncio import create_task
|
6
8
|
from functools import partial
|
7
9
|
from pathlib import Path
|
8
10
|
from typing import Optional
|
9
11
|
|
12
|
+
import blinker
|
10
13
|
import sass
|
11
14
|
from mergedeep import Strategy, merge
|
12
15
|
from trame.app import get_server
|
@@ -18,7 +21,9 @@ from trame_client.widgets import html
|
|
18
21
|
from trame_server.core import Server
|
19
22
|
from trame_server.state import State
|
20
23
|
|
24
|
+
from nova.common.signals import Signal
|
21
25
|
from nova.mvvm.pydantic_utils import validate_pydantic_parameter
|
26
|
+
from nova.trame.view.theme.exit_button import ExitButton
|
22
27
|
from nova.trame.view.utilities.local_storage import LocalStorageManager
|
23
28
|
|
24
29
|
THEME_PATH = Path(__file__).parent
|
@@ -138,6 +143,29 @@ class ThemedApp:
|
|
138
143
|
async def init_theme(self) -> None:
|
139
144
|
create_task(self._init_theme())
|
140
145
|
|
146
|
+
async def get_jobs_callback(self) -> None:
|
147
|
+
get_tools_signal = blinker.signal(Signal.GET_ALL_TOOLS)
|
148
|
+
response = get_tools_signal.send()
|
149
|
+
if response and len(response[0]) > 1: # Make sure that the callback had a return value
|
150
|
+
try:
|
151
|
+
self.server.state.nova_running_jobs = [tool.id for tool in response[0][1]]
|
152
|
+
if len(self.server.state.nova_running_jobs) > 0:
|
153
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = True
|
154
|
+
self.server.state.nova_kill_jobs_on_exit = True
|
155
|
+
else:
|
156
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
|
157
|
+
except Exception as e:
|
158
|
+
logger.warning(f"Issue getting running jobs: {e}")
|
159
|
+
|
160
|
+
async def exit_callback(self) -> None:
|
161
|
+
logger.info(f"Closing App. Killing jobs: {self.server.state.nova_kill_jobs_on_exit}")
|
162
|
+
if self.server.state.nova_kill_jobs_on_exit:
|
163
|
+
self.server.state.nova_show_exit_progress = True
|
164
|
+
await asyncio.sleep(2)
|
165
|
+
stop_signal = blinker.signal(Signal.EXIT_SIGNAL)
|
166
|
+
stop_signal.send()
|
167
|
+
sys.exit(0)
|
168
|
+
|
141
169
|
def set_theme(self, theme: Optional[str], force: bool = True) -> None:
|
142
170
|
"""Sets the theme of the application.
|
143
171
|
|
@@ -227,6 +255,7 @@ class ThemedApp:
|
|
227
255
|
"Selected",
|
228
256
|
v_if=f"nova__theme === '{theme['value']}'",
|
229
257
|
)
|
258
|
+
ExitButton(self.exit_callback, self.get_jobs_callback)
|
230
259
|
|
231
260
|
with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
|
232
261
|
# [slot override example]
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
|
-
from typing import Any, Dict, List
|
5
|
+
from typing import Any, Dict, List
|
6
6
|
|
7
7
|
from nova.mvvm.interface import BindingInterface
|
8
8
|
from nova.trame.model.data_selector import DataSelectorModel
|
@@ -12,16 +12,13 @@ class DataSelectorViewModel:
|
|
12
12
|
"""Manages the view state of the DataSelector widget."""
|
13
13
|
|
14
14
|
def __init__(self, model: DataSelectorModel, binding: BindingInterface) -> None:
|
15
|
-
self.model = model
|
15
|
+
self.model: DataSelectorModel = model
|
16
16
|
|
17
17
|
self.datafiles: List[Dict[str, Any]] = []
|
18
18
|
self.directories: List[Dict[str, Any]] = []
|
19
19
|
self.expanded: List[str] = []
|
20
20
|
|
21
21
|
self.state_bind = binding.new_bind(self.model.state, callback_after_update=self.on_state_updated)
|
22
|
-
self.facilities_bind = binding.new_bind()
|
23
|
-
self.instruments_bind = binding.new_bind()
|
24
|
-
self.experiments_bind = binding.new_bind()
|
25
22
|
self.directories_bind = binding.new_bind()
|
26
23
|
self.datafiles_bind = binding.new_bind()
|
27
24
|
self.reset_bind = binding.new_bind()
|
@@ -50,40 +47,17 @@ class DataSelectorViewModel:
|
|
50
47
|
self.expanded.append(paths[-1])
|
51
48
|
self.directories_bind.update_in_view(self.directories)
|
52
49
|
|
53
|
-
def set_directory(self, directory_path: str = "") -> None:
|
54
|
-
self.model.set_directory(directory_path)
|
55
|
-
self.update_view()
|
56
|
-
|
57
|
-
def set_state(self, facility: Optional[str], instrument: Optional[str], experiment: Optional[str]) -> None:
|
58
|
-
self.model.set_state(facility, instrument, experiment)
|
59
|
-
self.update_view()
|
60
|
-
|
61
|
-
def reset(self) -> None:
|
62
|
-
self.model.set_directory("")
|
63
|
-
self.directories = self.model.get_directories()
|
64
|
-
self.expanded = []
|
65
|
-
self.reset_bind.update_in_view(None)
|
66
|
-
|
67
50
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
self.reset()
|
73
|
-
case "instrument":
|
74
|
-
self.model.set_state(facility=None, instrument=None, experiment="")
|
75
|
-
self.reset()
|
76
|
-
case "experiment":
|
77
|
-
self.reset()
|
78
|
-
case "custom_directory":
|
79
|
-
self.reset()
|
51
|
+
pass
|
52
|
+
|
53
|
+
def set_subdirectory(self, subdirectory_path: str = "") -> None:
|
54
|
+
self.model.set_subdirectory(subdirectory_path)
|
80
55
|
self.update_view()
|
81
56
|
|
82
57
|
def update_view(self) -> None:
|
83
58
|
self.state_bind.update_in_view(self.model.state)
|
84
|
-
self.
|
85
|
-
|
86
|
-
self.experiments_bind.update_in_view(self.model.get_experiments())
|
59
|
+
if not self.directories:
|
60
|
+
self.directories = self.model.get_directories()
|
87
61
|
self.directories_bind.update_in_view(self.directories)
|
88
62
|
|
89
63
|
self.datafiles = [
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""View model implementation for the DataSelector widget."""
|
2
|
+
|
3
|
+
from typing import Any, Dict, Optional
|
4
|
+
|
5
|
+
from nova.mvvm.interface import BindingInterface
|
6
|
+
from nova.trame.model.ornl.neutron_data_selector import NeutronDataSelectorModel
|
7
|
+
from nova.trame.view_model.data_selector import DataSelectorViewModel
|
8
|
+
|
9
|
+
|
10
|
+
class NeutronDataSelectorViewModel(DataSelectorViewModel):
|
11
|
+
"""Manages the view state of the DataSelector widget."""
|
12
|
+
|
13
|
+
def __init__(self, model: NeutronDataSelectorModel, binding: BindingInterface) -> None:
|
14
|
+
super().__init__(model, binding)
|
15
|
+
self.model: NeutronDataSelectorModel = model
|
16
|
+
|
17
|
+
self.facilities_bind = binding.new_bind()
|
18
|
+
self.instruments_bind = binding.new_bind()
|
19
|
+
self.experiments_bind = binding.new_bind()
|
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
|
+
def reset(self) -> None:
|
26
|
+
self.model.set_subdirectory("")
|
27
|
+
self.directories = self.model.get_directories()
|
28
|
+
self.expanded = []
|
29
|
+
self.reset_bind.update_in_view(None)
|
30
|
+
|
31
|
+
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
32
|
+
for update in results.get("updated", []):
|
33
|
+
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
|
+
case "custom_directory":
|
43
|
+
self.reset()
|
44
|
+
self.update_view()
|
45
|
+
|
46
|
+
def update_view(self) -> None:
|
47
|
+
self.facilities_bind.update_in_view(self.model.get_facilities())
|
48
|
+
self.instruments_bind.update_in_view(self.model.get_instruments())
|
49
|
+
self.experiments_bind.update_in_view(self.model.get_experiments())
|
50
|
+
|
51
|
+
super().update_view()
|
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: nova-trame
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.23.0
|
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
|
7
|
-
Author: Duggan
|
7
|
+
Author: John Duggan
|
8
8
|
Author-email: dugganjw@ornl.gov
|
9
9
|
Requires-Python: >=3.10,<4.0
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -18,7 +18,7 @@ Requires-Dist: blinker (>=1.9.0,<2.0.0)
|
|
18
18
|
Requires-Dist: libsass
|
19
19
|
Requires-Dist: mergedeep
|
20
20
|
Requires-Dist: natsort (>=8.4.0,<9.0.0)
|
21
|
-
Requires-Dist: nova-common (>=0.2.
|
21
|
+
Requires-Dist: nova-common (>=0.2.2)
|
22
22
|
Requires-Dist: nova-mvvm
|
23
23
|
Requires-Dist: pydantic
|
24
24
|
Requires-Dist: tomli
|
@@ -43,7 +43,7 @@ You can install this package directly with
|
|
43
43
|
pip install nova-trame
|
44
44
|
```
|
45
45
|
|
46
|
-
A user guide, examples, and a full API for this package can be found at https://nova-application-development.readthedocs.io/en/stable
|
46
|
+
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/).
|
47
47
|
|
48
48
|
Developers: please read [this document](DEVELOPMENT.md)
|
49
49
|
|
@@ -1,12 +1,15 @@
|
|
1
1
|
nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
2
2
|
nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
|
3
|
-
nova/trame/model/data_selector.py,sha256=
|
3
|
+
nova/trame/model/data_selector.py,sha256=jXu2tRmk0sI6fSbBs-bSggEiRw99M5d8NwpvPOk9nDs,4182
|
4
|
+
nova/trame/model/ornl/neutron_data_selector.py,sha256=nfjXwT93JcPjJwyDynoP_stDVAfjUs7neeVZTk_04gc,6424
|
4
5
|
nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
|
5
6
|
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
6
|
-
nova/trame/view/components/data_selector.py,sha256=
|
7
|
+
nova/trame/view/components/data_selector.py,sha256=uahrLdTRKrec-g8L-xIYW0OG_dCnFuKJxq69ROf2c6I,8532
|
7
8
|
nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
|
8
9
|
nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
|
9
10
|
nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
|
11
|
+
nova/trame/view/components/ornl/__init__.py,sha256=HnxzzSsxw0vQSDCVFfWsAxx1n3HnU37LMuQkfiewmSU,90
|
12
|
+
nova/trame/view/components/ornl/neutron_data_selector.py,sha256=RZAowBxRaxlVsHTsukHTX0reveWG1K6ib8zb1kKL9Ow,6631
|
10
13
|
nova/trame/view/components/progress_bar.py,sha256=fCfPw4MPAvORaeFOXugreok4GLpDVZGMkqvnv-AhMxg,2967
|
11
14
|
nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
|
12
15
|
nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
|
@@ -19,21 +22,23 @@ nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddy
|
|
19
22
|
nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
|
20
23
|
nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
|
21
24
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
22
|
-
nova/trame/view/theme/assets/core_style.scss,sha256=
|
25
|
+
nova/trame/view/theme/assets/core_style.scss,sha256=AJ-2hyQRVkyOGWJB0lGMzlQeshbCJP5BGNYCt9e9AOI,4208
|
23
26
|
nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
|
24
27
|
nova/trame/view/theme/assets/js/delay_manager.js,sha256=mRV6KoO8-Bxq3tG5Bh9CQYy-CRVbkj3IYlqNb-Og7cI,526
|
25
28
|
nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
|
26
29
|
nova/trame/view/theme/assets/js/revo_grid.js,sha256=WBsmoslu9qI5DHZkHkJam2AVgdiBp6szfOSV8a9cA5Q,3579
|
27
30
|
nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
|
28
|
-
nova/trame/view/theme/
|
31
|
+
nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
|
32
|
+
nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
|
29
33
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
30
|
-
nova/trame/view_model/data_selector.py,sha256=
|
34
|
+
nova/trame/view_model/data_selector.py,sha256=8XBCc12CcQfctOyz5zAOZTIXddKfpzIp8A_yZI-MpFc,2499
|
31
35
|
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
36
|
+
nova/trame/view_model/ornl/neutron_data_selector.py,sha256=CtVva_MXWGRG_KclE4ln8XJP_dfy6-5unE3pjEStOpE,2074
|
32
37
|
nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
|
33
38
|
nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
|
34
39
|
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
35
|
-
nova_trame-0.
|
36
|
-
nova_trame-0.
|
37
|
-
nova_trame-0.
|
38
|
-
nova_trame-0.
|
39
|
-
nova_trame-0.
|
40
|
+
nova_trame-0.23.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
41
|
+
nova_trame-0.23.0.dist-info/METADATA,sha256=vKtoLVHkJVqpA4yqwZZmjWWoYdPk1avkhUc5B8KuF80,1688
|
42
|
+
nova_trame-0.23.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
43
|
+
nova_trame-0.23.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
44
|
+
nova_trame-0.23.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|