nova-trame 0.19.2__py3-none-any.whl → 0.20.1__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 +39 -18
- nova/trame/view/components/__init__.py +12 -1
- nova/trame/view/components/data_selector.py +20 -3
- nova/trame/view/components/execution_buttons.py +112 -0
- nova/trame/view/components/progress_bar.py +65 -0
- nova/trame/view/components/tool_outputs.py +63 -0
- nova/trame/view/theme/assets/core_style.scss +11 -3
- nova/trame/view_model/data_selector.py +10 -4
- nova/trame/view_model/execution_buttons.py +87 -0
- nova/trame/view_model/progress_bar.py +75 -0
- nova/trame/view_model/tool_outputs.py +23 -0
- {nova_trame-0.19.2.dist-info → nova_trame-0.20.1.dist-info}/METADATA +3 -1
- {nova_trame-0.19.2.dist-info → nova_trame-0.20.1.dist-info}/RECORD +16 -10
- {nova_trame-0.19.2.dist-info → nova_trame-0.20.1.dist-info}/WHEEL +1 -1
- {nova_trame-0.19.2.dist-info → nova_trame-0.20.1.dist-info}/LICENSE +0 -0
- {nova_trame-0.19.2.dist-info → nova_trame-0.20.1.dist-info}/entry_points.txt +0 -0
@@ -54,23 +54,17 @@ INSTRUMENTS = {
|
|
54
54
|
}
|
55
55
|
|
56
56
|
|
57
|
-
def get_facilities() -> List[str]:
|
58
|
-
return list(INSTRUMENTS.keys())
|
59
|
-
|
60
|
-
|
61
|
-
def get_instruments(facility: str) -> List[str]:
|
62
|
-
return list(INSTRUMENTS.get(facility, {}).keys())
|
63
|
-
|
64
|
-
|
65
57
|
class DataSelectorState(BaseModel, validate_assignment=True):
|
66
58
|
"""Selection state for identifying datafiles."""
|
67
59
|
|
68
60
|
facility: str = Field(default="", title="Facility")
|
69
61
|
instrument: str = Field(default="", title="Instrument")
|
70
62
|
experiment: str = Field(default="", title="Experiment")
|
63
|
+
user_directory: str = Field(default="", title="User Directory")
|
71
64
|
directory: str = Field(default="")
|
72
65
|
extensions: List[str] = Field(default=[])
|
73
66
|
prefix: str = Field(default="")
|
67
|
+
show_user_directories: bool = Field(default=False)
|
74
68
|
|
75
69
|
@field_validator("experiment", mode="after")
|
76
70
|
@classmethod
|
@@ -81,11 +75,11 @@ class DataSelectorState(BaseModel, validate_assignment=True):
|
|
81
75
|
|
82
76
|
@model_validator(mode="after")
|
83
77
|
def validate_state(self) -> Self:
|
84
|
-
valid_facilities = get_facilities()
|
78
|
+
valid_facilities = self.get_facilities()
|
85
79
|
if self.facility and self.facility not in valid_facilities:
|
86
80
|
warn(f"Facility '{self.facility}' could not be found. Valid options: {valid_facilities}", stacklevel=1)
|
87
81
|
|
88
|
-
valid_instruments = get_instruments(
|
82
|
+
valid_instruments = self.get_instruments()
|
89
83
|
if self.instrument and self.instrument not in valid_instruments:
|
90
84
|
warn(
|
91
85
|
(
|
@@ -98,25 +92,37 @@ class DataSelectorState(BaseModel, validate_assignment=True):
|
|
98
92
|
|
99
93
|
return self
|
100
94
|
|
95
|
+
def get_facilities(self) -> List[str]:
|
96
|
+
facilities = list(INSTRUMENTS.keys())
|
97
|
+
if self.show_user_directories:
|
98
|
+
facilities.append("User Directory")
|
99
|
+
return facilities
|
100
|
+
|
101
|
+
def get_instruments(self) -> List[str]:
|
102
|
+
return list(INSTRUMENTS.get(self.facility, {}).keys())
|
103
|
+
|
101
104
|
|
102
105
|
class DataSelectorModel:
|
103
106
|
"""Manages file system interactions for the DataSelector widget."""
|
104
107
|
|
105
|
-
def __init__(
|
108
|
+
def __init__(
|
109
|
+
self, facility: str, instrument: str, extensions: List[str], prefix: str, show_user_directories: bool
|
110
|
+
) -> None:
|
106
111
|
self.state = DataSelectorState()
|
107
112
|
self.state.facility = facility
|
108
113
|
self.state.instrument = instrument
|
109
114
|
self.state.extensions = extensions
|
110
115
|
self.state.prefix = prefix
|
116
|
+
self.state.show_user_directories = show_user_directories
|
111
117
|
|
112
118
|
def get_facilities(self) -> List[str]:
|
113
|
-
return sorted(get_facilities())
|
119
|
+
return sorted(self.state.get_facilities())
|
114
120
|
|
115
121
|
def get_instrument_dir(self) -> str:
|
116
122
|
return INSTRUMENTS.get(self.state.facility, {}).get(self.state.instrument, "")
|
117
123
|
|
118
124
|
def get_instruments(self) -> List[str]:
|
119
|
-
return sorted(
|
125
|
+
return sorted(self.state.get_instruments())
|
120
126
|
|
121
127
|
def get_experiments(self) -> List[str]:
|
122
128
|
experiments = []
|
@@ -142,17 +148,32 @@ class DataSelectorModel:
|
|
142
148
|
|
143
149
|
return sorted_dirs
|
144
150
|
|
145
|
-
def
|
151
|
+
def get_experiment_directory_path(self) -> Optional[Path]:
|
146
152
|
if not self.state.experiment:
|
153
|
+
return None
|
154
|
+
|
155
|
+
return Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
156
|
+
|
157
|
+
def get_user_directory_path(self) -> Optional[Path]:
|
158
|
+
if not self.state.user_directory:
|
159
|
+
return None
|
160
|
+
|
161
|
+
return Path("/SNS/users") / self.state.user_directory
|
162
|
+
|
163
|
+
def get_directories(self) -> List[str]:
|
164
|
+
if self.state.facility == "User Directory":
|
165
|
+
base_path = self.get_user_directory_path()
|
166
|
+
else:
|
167
|
+
base_path = self.get_experiment_directory_path()
|
168
|
+
|
169
|
+
if not base_path:
|
147
170
|
return []
|
148
171
|
|
149
172
|
directories = []
|
150
|
-
|
151
|
-
experiment_path = Path("/") / self.state.facility / self.get_instrument_dir() / self.state.experiment
|
152
173
|
try:
|
153
|
-
for dirpath, _, _ in os.walk(
|
174
|
+
for dirpath, _, _ in os.walk(base_path):
|
154
175
|
# Get the relative path from the start path
|
155
|
-
path_parts = os.path.relpath(dirpath,
|
176
|
+
path_parts = os.path.relpath(dirpath, base_path).split(os.sep)
|
156
177
|
|
157
178
|
# Only create a new entry for top-level directories
|
158
179
|
if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
|
@@ -1,6 +1,17 @@
|
|
1
1
|
from .data_selector import DataSelector
|
2
|
+
from .execution_buttons import ExecutionButtons
|
2
3
|
from .file_upload import FileUpload
|
3
4
|
from .input_field import InputField
|
5
|
+
from .progress_bar import ProgressBar
|
4
6
|
from .remote_file_input import RemoteFileInput
|
7
|
+
from .tool_outputs import ToolOutputWindows
|
5
8
|
|
6
|
-
__all__ = [
|
9
|
+
__all__ = [
|
10
|
+
"DataSelector",
|
11
|
+
"ExecutionButtons",
|
12
|
+
"FileUpload",
|
13
|
+
"InputField",
|
14
|
+
"ProgressBar",
|
15
|
+
"RemoteFileInput",
|
16
|
+
"ToolOutputWindows",
|
17
|
+
]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""View Implementation for DataSelector."""
|
2
2
|
|
3
3
|
from typing import Any, List, Optional, cast
|
4
|
+
from warnings import warn
|
4
5
|
|
5
6
|
from trame.app import get_server
|
6
7
|
from trame.widgets import client, html
|
@@ -27,6 +28,7 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
27
28
|
extensions: Optional[List[str]] = None,
|
28
29
|
prefix: str = "",
|
29
30
|
select_strategy: str = "all",
|
31
|
+
show_user_directories: bool = False,
|
30
32
|
**kwargs: Any,
|
31
33
|
) -> None:
|
32
34
|
"""Constructor for DataSelector.
|
@@ -48,6 +50,9 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
48
50
|
select_strategy : str, optional
|
49
51
|
The selection strategy to pass to the `VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`__.
|
50
52
|
If unset, the `all` strategy will be used.
|
53
|
+
show_user_directories : bool, optional
|
54
|
+
Whether or not to allow users to select data files from user directories. Ignored if the facility parameter
|
55
|
+
is set.
|
51
56
|
**kwargs
|
52
57
|
All other arguments will be passed to the underlying
|
53
58
|
`VDataTable component <https://trame.readthedocs.io/en/latest/trame.widgets.vuetify3.html#trame.widgets.vuetify3.VDataTable>`_.
|
@@ -64,10 +69,15 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
64
69
|
else:
|
65
70
|
self._label = None
|
66
71
|
|
72
|
+
if facility and show_user_directories:
|
73
|
+
warn("show_user_directories will be ignored since the facility parameter is set.", stacklevel=1)
|
74
|
+
|
67
75
|
self._v_model = v_model
|
76
|
+
self._v_model_name_in_state = v_model.split(".")[0]
|
68
77
|
self._extensions = extensions if extensions is not None else []
|
69
78
|
self._prefix = prefix
|
70
79
|
self._select_strategy = select_strategy
|
80
|
+
self._show_user_directories = show_user_directories
|
71
81
|
|
72
82
|
self._state_name = f"nova__dataselector_{self._next_id}_state"
|
73
83
|
self._facilities_name = f"nova__dataselector_{self._next_id}_facilities"
|
@@ -76,7 +86,7 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
76
86
|
self._directories_name = f"nova__dataselector_{self._next_id}_directories"
|
77
87
|
self._datafiles_name = f"nova__dataselector_{self._next_id}_datafiles"
|
78
88
|
|
79
|
-
self._flush_state = f"flushState('{self.
|
89
|
+
self._flush_state = f"flushState('{self._v_model_name_in_state}');"
|
80
90
|
self._reset_state = client.JSEval(exec=f"{self._v_model} = []; {self._flush_state}").exec
|
81
91
|
|
82
92
|
self.create_model(facility, instrument)
|
@@ -96,14 +106,19 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
96
106
|
if instrument == "":
|
97
107
|
columns -= 1
|
98
108
|
InputField(
|
99
|
-
|
109
|
+
v_if=f"{self._state_name}.facility !== 'User Directory'",
|
110
|
+
v_model=f"{self._state_name}.instrument",
|
111
|
+
items=(self._instruments_name,),
|
112
|
+
type="autocomplete",
|
100
113
|
)
|
101
114
|
InputField(
|
115
|
+
v_if=f"{self._state_name}.facility !== 'User Directory'",
|
102
116
|
v_model=f"{self._state_name}.experiment",
|
103
117
|
column_span=columns,
|
104
118
|
items=(self._experiments_name,),
|
105
119
|
type="autocomplete",
|
106
120
|
)
|
121
|
+
InputField(v_else=True, v_model=f"{self._state_name}.user_directory", column_span=2)
|
107
122
|
|
108
123
|
with GridLayout(columns=2, classes="flex-1-0 h-0", valign="start"):
|
109
124
|
if not self._prefix:
|
@@ -155,7 +170,9 @@ class DataSelector(vuetify.VDataTableVirtual):
|
|
155
170
|
)
|
156
171
|
|
157
172
|
def create_model(self, facility: str, instrument: str) -> None:
|
158
|
-
self._model = DataSelectorModel(
|
173
|
+
self._model = DataSelectorModel(
|
174
|
+
facility, instrument, self._extensions, self._prefix, self._show_user_directories
|
175
|
+
)
|
159
176
|
|
160
177
|
def create_viewmodel(self) -> None:
|
161
178
|
server = get_server(None, client_type="vue3")
|
@@ -0,0 +1,112 @@
|
|
1
|
+
"""Module for the Progress Tab."""
|
2
|
+
|
3
|
+
from trame.app import get_server
|
4
|
+
from trame.widgets import client
|
5
|
+
from trame.widgets import vuetify3 as vuetify
|
6
|
+
from trame_client.widgets import html
|
7
|
+
|
8
|
+
from nova.mvvm.trame_binding import TrameBinding
|
9
|
+
from nova.trame.view_model.execution_buttons import ExecutionButtonsViewModel
|
10
|
+
|
11
|
+
|
12
|
+
class ExecutionButtons:
|
13
|
+
"""Execution buttons class. Adds Run/Stop/Cancel/Download buttons to the view.
|
14
|
+
|
15
|
+
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
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, id: str, stop_btn: bool = False, download_btn: bool = False) -> None:
|
19
|
+
"""Constructor for ExecutionButtons.
|
20
|
+
|
21
|
+
Parameters
|
22
|
+
----------
|
23
|
+
id : str
|
24
|
+
Component id. Should be used consistently with ToolRunner and other components.
|
25
|
+
stop_btn: bool
|
26
|
+
Display stop button.
|
27
|
+
download_btn : bool
|
28
|
+
Display download button.
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
None
|
33
|
+
"""
|
34
|
+
self.id = f"execution_{id}"
|
35
|
+
|
36
|
+
self.server = get_server(None, client_type="vue3")
|
37
|
+
binding = TrameBinding(self.server.state)
|
38
|
+
self.ctrl = self.server.controller
|
39
|
+
self.stop_btn = stop_btn
|
40
|
+
self.download_btn = download_btn
|
41
|
+
self.view_model = ExecutionButtonsViewModel(id, binding)
|
42
|
+
self.view_model.buttons_state_bind.connect(self.id)
|
43
|
+
self._download = client.JSEval(
|
44
|
+
exec=(
|
45
|
+
"async ($event) => {"
|
46
|
+
" const blob = new window.Blob([$event], {type: 'application/zip'});"
|
47
|
+
" const url = window.URL.createObjectURL(blob);"
|
48
|
+
" const anchor = window.document.createElement('a');"
|
49
|
+
" anchor.setAttribute('href', url);"
|
50
|
+
" anchor.setAttribute('download', 'results.zip');"
|
51
|
+
" window.document.body.appendChild(anchor);"
|
52
|
+
" anchor.click();"
|
53
|
+
" window.document.body.removeChild(anchor);"
|
54
|
+
" setTimeout(() => window.URL.revokeObjectURL(url), 1000);"
|
55
|
+
"}"
|
56
|
+
)
|
57
|
+
).exec
|
58
|
+
|
59
|
+
self.create_ui()
|
60
|
+
|
61
|
+
def create_ui(self) -> None:
|
62
|
+
with html.Div(classes="d-flex justify-center my-4 w-100"):
|
63
|
+
vuetify.VBtn(
|
64
|
+
"Run",
|
65
|
+
disabled=(f"{self.id}.run_disabled",),
|
66
|
+
prepend_icon="mdi-play",
|
67
|
+
classes="mr-4",
|
68
|
+
id=f"{self.id}_run",
|
69
|
+
click=self.run,
|
70
|
+
)
|
71
|
+
if self.stop_btn:
|
72
|
+
vuetify.VBtn(
|
73
|
+
"Stop",
|
74
|
+
disabled=(f"{self.id}.stop_disabled",),
|
75
|
+
loading=(f"{self.id}.stop_in_progress",),
|
76
|
+
classes="mr-4",
|
77
|
+
id=f"{self.id}_stop",
|
78
|
+
prepend_icon="mdi-stop",
|
79
|
+
click=self.stop,
|
80
|
+
)
|
81
|
+
vuetify.VBtn(
|
82
|
+
"Cancel",
|
83
|
+
disabled=(f"{self.id}.cancel_disabled",),
|
84
|
+
color="error",
|
85
|
+
loading=(f"{self.id}.cancel_in_progress",),
|
86
|
+
prepend_icon="mdi-cancel",
|
87
|
+
classes="mr-4",
|
88
|
+
id=f"{self.id}_cancel",
|
89
|
+
click=self.cancel,
|
90
|
+
)
|
91
|
+
if self.download_btn:
|
92
|
+
vuetify.VBtn(
|
93
|
+
"Download Results",
|
94
|
+
disabled=(f"{self.id}.download_disabled",),
|
95
|
+
loading=(f"{self.id}.download_in_progress",),
|
96
|
+
id=f"{self.id}.download",
|
97
|
+
click=self.download,
|
98
|
+
)
|
99
|
+
|
100
|
+
async def download(self) -> None:
|
101
|
+
content = await self.view_model.prepare_results()
|
102
|
+
if content:
|
103
|
+
self._download(content)
|
104
|
+
|
105
|
+
async def run(self) -> None:
|
106
|
+
await self.view_model.run()
|
107
|
+
|
108
|
+
async def cancel(self) -> None:
|
109
|
+
await self.view_model.cancel()
|
110
|
+
|
111
|
+
async def stop(self) -> None:
|
112
|
+
await self.view_model.stop()
|
@@ -0,0 +1,65 @@
|
|
1
|
+
"""Module for the Progress Tab."""
|
2
|
+
|
3
|
+
from trame.app import get_server
|
4
|
+
from trame.widgets import vuetify3 as vuetify
|
5
|
+
from trame_client.widgets import html
|
6
|
+
|
7
|
+
from nova.mvvm.trame_binding import TrameBinding
|
8
|
+
from nova.trame.view_model.progress_bar import ProgressBarViewModel
|
9
|
+
|
10
|
+
|
11
|
+
class ProgressBar:
|
12
|
+
"""Progress bar class. Adds progress bar that displays job status to the view.
|
13
|
+
|
14
|
+
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>`__.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(self, id: str) -> None:
|
18
|
+
"""Constructor for ProgressBar.
|
19
|
+
|
20
|
+
Parameters
|
21
|
+
----------
|
22
|
+
id : str
|
23
|
+
Component id. Should be used consistently with ToolRunner and other components
|
24
|
+
|
25
|
+
Returns
|
26
|
+
-------
|
27
|
+
None
|
28
|
+
"""
|
29
|
+
self.id = f"progress_bar_{id}"
|
30
|
+
self.create_viewmodel(id)
|
31
|
+
self.view_model.progress_state_bind.connect(self.id)
|
32
|
+
self.create_ui()
|
33
|
+
|
34
|
+
def create_viewmodel(self, id: str) -> None:
|
35
|
+
server = get_server(None, client_type="vue3")
|
36
|
+
binding = TrameBinding(server.state)
|
37
|
+
self.view_model = ProgressBarViewModel(id, binding)
|
38
|
+
|
39
|
+
def create_ui(self) -> None:
|
40
|
+
with vuetify.VProgressLinear(
|
41
|
+
height="25",
|
42
|
+
model_value=(f"{self.id}.progress", "0"),
|
43
|
+
striped=True,
|
44
|
+
id=f"{self.id}_show_progress",
|
45
|
+
v_show=(f"{self.id}.show_progress",),
|
46
|
+
):
|
47
|
+
html.H5(v_text=f"{self.id}.details")
|
48
|
+
with vuetify.VProgressLinear(
|
49
|
+
height="25",
|
50
|
+
model_value="100",
|
51
|
+
striped=False,
|
52
|
+
color="error",
|
53
|
+
id=f"{self.id}_show_failed",
|
54
|
+
v_show=(f"{self.id}.show_failed",),
|
55
|
+
):
|
56
|
+
html.H5(v_text=f"{self.id}.details", classes="text-white")
|
57
|
+
with vuetify.VProgressLinear(
|
58
|
+
height="25",
|
59
|
+
model_value="100",
|
60
|
+
striped=False,
|
61
|
+
color="primary",
|
62
|
+
id=f"{self.id}_show_ok",
|
63
|
+
v_show=(f"{self.id}.show_ok",),
|
64
|
+
):
|
65
|
+
html.H5(v_text=f"{self.id}.details", classes="text-white")
|
@@ -0,0 +1,63 @@
|
|
1
|
+
"""Module for the Tool outputs."""
|
2
|
+
|
3
|
+
from trame.app import get_server
|
4
|
+
from trame.widgets import vuetify3 as vuetify
|
5
|
+
|
6
|
+
from nova.mvvm.trame_binding import TrameBinding
|
7
|
+
from nova.trame.view.components import InputField
|
8
|
+
from nova.trame.view.layouts import HBoxLayout
|
9
|
+
from nova.trame.view_model.tool_outputs import ToolOutputsViewModel
|
10
|
+
|
11
|
+
|
12
|
+
class ToolOutputWindows:
|
13
|
+
"""Tool outputs class. Displays windows with tool stdout/stderr.
|
14
|
+
|
15
|
+
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
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, id: str) -> None:
|
19
|
+
"""Constructor for ToolOutputWindows.
|
20
|
+
|
21
|
+
Parameters
|
22
|
+
----------
|
23
|
+
id : str
|
24
|
+
Component id. Should be used consistently with ToolRunner and other components
|
25
|
+
|
26
|
+
Returns
|
27
|
+
-------
|
28
|
+
None
|
29
|
+
"""
|
30
|
+
self.id = f"tool_outputs_{id}"
|
31
|
+
self.create_viewmodel(id)
|
32
|
+
self.view_model.tool_outputs_bind.connect(self.id)
|
33
|
+
self.create_ui()
|
34
|
+
|
35
|
+
def create_viewmodel(self, id: str) -> None:
|
36
|
+
server = get_server(None, client_type="vue3")
|
37
|
+
binding = TrameBinding(server.state)
|
38
|
+
self.view_model = ToolOutputsViewModel(id, binding)
|
39
|
+
|
40
|
+
def create_ui(self) -> None:
|
41
|
+
with HBoxLayout(classes="d-flex", width="100%"):
|
42
|
+
with vuetify.VTabs(v_model=(f"{self.id}_active_output_tab", "1"), direction="vertical"):
|
43
|
+
vuetify.VTab("Console output", value=1)
|
44
|
+
vuetify.VTab("Console error", value=2)
|
45
|
+
with HBoxLayout(classes="flex-grow-1"):
|
46
|
+
InputField(
|
47
|
+
v_show=f"{self.id}_active_output_tab === '1'",
|
48
|
+
v_model=f"{self.id}.stdout",
|
49
|
+
id=f"{self.id}_outputs",
|
50
|
+
type="autoscroll",
|
51
|
+
auto_grow=True,
|
52
|
+
readonly=True,
|
53
|
+
max_rows="30",
|
54
|
+
)
|
55
|
+
InputField(
|
56
|
+
v_show=f"{self.id}_active_output_tab === '2'",
|
57
|
+
v_model=f"{self.id}.stderr",
|
58
|
+
id=f"{self.id}_errors",
|
59
|
+
type="autoscroll",
|
60
|
+
auto_grow=True,
|
61
|
+
readonly=True,
|
62
|
+
max_rows="30",
|
63
|
+
)
|
@@ -130,10 +130,14 @@ html {
|
|
130
130
|
min-width: 0px !important;
|
131
131
|
padding: 5px 5px !important;
|
132
132
|
box-shadow: none !important;
|
133
|
-
}
|
134
133
|
|
135
|
-
|
136
|
-
|
134
|
+
.v-btn__content {
|
135
|
+
text-transform: none;
|
136
|
+
}
|
137
|
+
|
138
|
+
.v-btn__prepend {
|
139
|
+
margin-left: 1px;
|
140
|
+
}
|
137
141
|
}
|
138
142
|
|
139
143
|
.v-label {
|
@@ -145,6 +149,10 @@ html {
|
|
145
149
|
padding: 5px;
|
146
150
|
}
|
147
151
|
|
152
|
+
textarea.v-field__input {
|
153
|
+
mask-image: none !important;
|
154
|
+
}
|
155
|
+
|
148
156
|
.v-field {
|
149
157
|
margin: 8px 4px 8px 4px;
|
150
158
|
}
|
@@ -29,17 +29,23 @@ class DataSelectorViewModel:
|
|
29
29
|
self.model.set_state(facility, instrument, experiment)
|
30
30
|
self.update_view()
|
31
31
|
|
32
|
+
def reset(self) -> None:
|
33
|
+
self.model.set_directory("")
|
34
|
+
self.reset_bind.update_in_view(None)
|
35
|
+
|
32
36
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
33
37
|
for update in results.get("updated", []):
|
34
38
|
match update:
|
35
39
|
case "facility":
|
36
40
|
self.model.set_state(facility=None, instrument="", experiment="")
|
37
|
-
self.
|
38
|
-
self.reset_bind.update_in_view(None)
|
41
|
+
self.reset()
|
39
42
|
case "instrument":
|
40
43
|
self.model.set_state(facility=None, instrument=None, experiment="")
|
41
|
-
self.
|
42
|
-
|
44
|
+
self.reset()
|
45
|
+
case "experiment":
|
46
|
+
self.reset()
|
47
|
+
case "user_directory":
|
48
|
+
self.reset()
|
43
49
|
self.update_view()
|
44
50
|
|
45
51
|
def update_view(self) -> None:
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""Module for the JobProgress ViewModel."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import Any, Optional
|
5
|
+
|
6
|
+
import blinker
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from nova.common.job import WorkState
|
10
|
+
from nova.common.signals import Signal, ToolCommand, get_signal_id
|
11
|
+
from nova.mvvm.interface import BindingInterface
|
12
|
+
|
13
|
+
|
14
|
+
def job_running(status: WorkState) -> bool:
|
15
|
+
"""A helper function to check if job is doing something in Galaxy."""
|
16
|
+
return status in [
|
17
|
+
WorkState.UPLOADING_DATA,
|
18
|
+
WorkState.QUEUED,
|
19
|
+
WorkState.RUNNING,
|
20
|
+
WorkState.STOPPING,
|
21
|
+
WorkState.CANCELING,
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
class ButtonsState(BaseModel):
|
26
|
+
"""Class that manages start/stop/cancel button states."""
|
27
|
+
|
28
|
+
run_disabled: bool = False
|
29
|
+
cancel_disabled: bool = True
|
30
|
+
stop_disabled: bool = True
|
31
|
+
download_disabled: bool = True
|
32
|
+
|
33
|
+
stop_in_progress: bool = False
|
34
|
+
cancel_in_progress: bool = False
|
35
|
+
download_in_progress: bool = False
|
36
|
+
|
37
|
+
def update_from_workstate(self, status: WorkState) -> None:
|
38
|
+
running = job_running(status)
|
39
|
+
self.run_disabled = running
|
40
|
+
self.cancel_disabled = not running
|
41
|
+
self.stop_disabled = status not in [WorkState.RUNNING, WorkState.STOPPING]
|
42
|
+
self.stop_in_progress = status == WorkState.STOPPING
|
43
|
+
self.cancel_in_progress = status == WorkState.CANCELING
|
44
|
+
self.download_disabled = status != WorkState.FINISHED
|
45
|
+
|
46
|
+
|
47
|
+
class ExecutionButtonsViewModel:
|
48
|
+
"""A viewmodel responsible for execution buttons."""
|
49
|
+
|
50
|
+
def __init__(self, id: str, binding: BindingInterface):
|
51
|
+
self.sender_id = f"ExecutionButtonsViewModel_{id}"
|
52
|
+
self.button_states = ButtonsState()
|
53
|
+
self.buttons_state_bind = binding.new_bind(self.button_states)
|
54
|
+
self.execution_signal = blinker.signal(get_signal_id(id, Signal.TOOL_COMMAND))
|
55
|
+
self.progress_signal = blinker.signal(get_signal_id(id, Signal.PROGRESS))
|
56
|
+
self.progress_signal.connect(self.update_state, weak=False)
|
57
|
+
|
58
|
+
async def update_state(self, _sender: Any, state: WorkState, details: str) -> None:
|
59
|
+
self.button_states.update_from_workstate(state)
|
60
|
+
self.buttons_state_bind.update_in_view(self.button_states)
|
61
|
+
|
62
|
+
async def run(self) -> None:
|
63
|
+
# disable run now since it might take some time before the client updates the status
|
64
|
+
self.button_states.run_disabled = True
|
65
|
+
self.buttons_state_bind.update_in_view(self.button_states)
|
66
|
+
await self.execution_signal.send_async(self.sender_id, command=ToolCommand.START)
|
67
|
+
|
68
|
+
async def cancel(self) -> None:
|
69
|
+
await self.execution_signal.send_async(self.sender_id, command=ToolCommand.CANCEL)
|
70
|
+
|
71
|
+
async def stop(self) -> None:
|
72
|
+
await self.execution_signal.send_async(self.sender_id, command=ToolCommand.CANCEL)
|
73
|
+
|
74
|
+
async def prepare_results(self) -> Optional[bytes]:
|
75
|
+
self.button_states.download_in_progress = True
|
76
|
+
self.buttons_state_bind.update_in_view(self.button_states)
|
77
|
+
await asyncio.sleep(0.5) # to give Trame time to update view
|
78
|
+
responses = await self.execution_signal.send_async(self.sender_id, command=ToolCommand.GET_RESULTS)
|
79
|
+
res = None
|
80
|
+
for response in responses: # responses can come from multiple places
|
81
|
+
if response[1] is None:
|
82
|
+
continue
|
83
|
+
if response[1]["sender"] == self.sender_id and response[1]["command"] == ToolCommand.GET_RESULTS:
|
84
|
+
res = response[1]["results"]
|
85
|
+
self.button_states.download_in_progress = False
|
86
|
+
self.buttons_state_bind.update_in_view(self.button_states)
|
87
|
+
return res
|
@@ -0,0 +1,75 @@
|
|
1
|
+
"""Module for the JobProgress ViewModel."""
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import blinker
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from nova.common.job import WorkState
|
9
|
+
from nova.common.signals import Signal, get_signal_id
|
10
|
+
from nova.mvvm.interface import BindingInterface
|
11
|
+
|
12
|
+
|
13
|
+
def details_from_state(state: WorkState) -> str:
|
14
|
+
work_state_map = {
|
15
|
+
WorkState.NOT_STARTED: "job not started",
|
16
|
+
WorkState.UPLOADING_DATA: "uploading data",
|
17
|
+
WorkState.QUEUED: "job is queued",
|
18
|
+
WorkState.RUNNING: "job is running",
|
19
|
+
WorkState.FINISHED: "job finished",
|
20
|
+
WorkState.ERROR: "job produced an error",
|
21
|
+
WorkState.DELETED: "job deleted",
|
22
|
+
WorkState.CANCELED: "job canceled",
|
23
|
+
WorkState.STOPPING: "stopping job",
|
24
|
+
WorkState.CANCELING: "canceling job",
|
25
|
+
}
|
26
|
+
if state in work_state_map:
|
27
|
+
return work_state_map[state]
|
28
|
+
else:
|
29
|
+
return state.value
|
30
|
+
|
31
|
+
|
32
|
+
class ProgressState(BaseModel):
|
33
|
+
"""Class that manages progress bars states."""
|
34
|
+
|
35
|
+
progress: str = ""
|
36
|
+
details: str = ""
|
37
|
+
show_progress: bool = False
|
38
|
+
show_failed: bool = False
|
39
|
+
show_ok: bool = False
|
40
|
+
|
41
|
+
def update_from_workstate(self, state: WorkState) -> None:
|
42
|
+
progress = "0"
|
43
|
+
match state:
|
44
|
+
case WorkState.UPLOADING_DATA:
|
45
|
+
progress = "10"
|
46
|
+
case WorkState.QUEUED:
|
47
|
+
progress = "20"
|
48
|
+
case WorkState.RUNNING:
|
49
|
+
progress = "50"
|
50
|
+
|
51
|
+
self.show_progress = state in [
|
52
|
+
WorkState.UPLOADING_DATA,
|
53
|
+
WorkState.QUEUED,
|
54
|
+
WorkState.RUNNING,
|
55
|
+
WorkState.CANCELING,
|
56
|
+
WorkState.STOPPING,
|
57
|
+
]
|
58
|
+
self.show_failed = state == WorkState.ERROR
|
59
|
+
self.show_ok = state == WorkState.FINISHED
|
60
|
+
self.progress = progress
|
61
|
+
self.details = details_from_state(state)
|
62
|
+
|
63
|
+
|
64
|
+
class ProgressBarViewModel:
|
65
|
+
"""A viewmodel responsible for progress bar."""
|
66
|
+
|
67
|
+
def __init__(self, id: str, binding: BindingInterface):
|
68
|
+
self.progress_state = ProgressState()
|
69
|
+
self.progress_state_bind = binding.new_bind(self.progress_state)
|
70
|
+
self.progress_signal = blinker.signal(get_signal_id(id, Signal.PROGRESS))
|
71
|
+
self.progress_signal.connect(self.update_state, weak=False)
|
72
|
+
|
73
|
+
async def update_state(self, _sender: Any, state: WorkState, details: str) -> None:
|
74
|
+
self.progress_state.update_from_workstate(state)
|
75
|
+
self.progress_state_bind.update_in_view(self.progress_state)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Module for the Tool ouputs ViewModel."""
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import blinker
|
6
|
+
|
7
|
+
from nova.common.job import ToolOutputs
|
8
|
+
from nova.common.signals import Signal, get_signal_id
|
9
|
+
from nova.mvvm.interface import BindingInterface
|
10
|
+
|
11
|
+
|
12
|
+
class ToolOutputsViewModel:
|
13
|
+
"""A viewmodel responsible for tool stdout and stderr."""
|
14
|
+
|
15
|
+
def __init__(self, id: str, binding: BindingInterface):
|
16
|
+
self.tool_outputs = ToolOutputs()
|
17
|
+
self.tool_outputs_bind = binding.new_bind(self.tool_outputs)
|
18
|
+
self.outputs_signal = blinker.signal(get_signal_id(id, Signal.OUTPUTS))
|
19
|
+
self.outputs_signal.connect(self.on_outputs_update, weak=False)
|
20
|
+
|
21
|
+
async def on_outputs_update(self, _sender: Any, outputs: ToolOutputs) -> None:
|
22
|
+
self.tool_outputs = outputs
|
23
|
+
self.tool_outputs_bind.update_in_view(self.tool_outputs)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: nova-trame
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.20.1
|
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
|
@@ -14,8 +14,10 @@ Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
16
16
|
Requires-Dist: altair
|
17
|
+
Requires-Dist: blinker (>=1.9.0,<2.0.0)
|
17
18
|
Requires-Dist: libsass
|
18
19
|
Requires-Dist: mergedeep
|
20
|
+
Requires-Dist: nova-common (>=0.2.0)
|
19
21
|
Requires-Dist: nova-mvvm
|
20
22
|
Requires-Dist: pydantic
|
21
23
|
Requires-Dist: tomli
|
@@ -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=bKWMk9bVhC16DTuEI7o656idMxzLbUi7RBdiaGO6TZY,8377
|
4
4
|
nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
|
5
|
-
nova/trame/view/components/__init__.py,sha256=
|
6
|
-
nova/trame/view/components/data_selector.py,sha256=
|
5
|
+
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
6
|
+
nova/trame/view/components/data_selector.py,sha256=IQipVqf6fG7qVRa4CJSr4jGz7Sru_lYU6K-qczJdstg,9727
|
7
|
+
nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
|
7
8
|
nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
|
8
9
|
nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
|
10
|
+
nova/trame/view/components/progress_bar.py,sha256=Sh5cOPaMWrFq8KTWEDui1dIbK53BPtGG2RZOSKEaoJ4,2186
|
9
11
|
nova/trame/view/components/remote_file_input.py,sha256=ByrBFj8svyWezcardCWrS_4Ag3fgTYNg_11lDW1FIA8,9669
|
12
|
+
nova/trame/view/components/tool_outputs.py,sha256=-6pDURd2l_FK_8EWa9BI3KhU_KJXJ6uyJ_rW4nQVc08,2337
|
10
13
|
nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
|
11
14
|
nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
|
12
15
|
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=0iWCXB8i7Tut1gA66hY9cGrhZPaHC7p-XdADDNy_UVY,12042
|
@@ -16,7 +19,7 @@ nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddy
|
|
16
19
|
nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
|
17
20
|
nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
|
18
21
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
19
|
-
nova/trame/view/theme/assets/core_style.scss,sha256=
|
22
|
+
nova/trame/view/theme/assets/core_style.scss,sha256=5i2cZQbmaQTpcNgsaVRdcubh0HHidqPnnLj8McHNkbY,3367
|
20
23
|
nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
|
21
24
|
nova/trame/view/theme/assets/js/delay_manager.js,sha256=vmb34DZ5YCQIlRW9Tf2M_uvJW6HFCmtlKZ5e_TPR8yg,536
|
22
25
|
nova/trame/view/theme/assets/js/lodash.debounce.min.js,sha256=GLzlQH04WDUNYN7i39ttHHejSdu-CpAvfWgDgKDn-OY,4448
|
@@ -24,10 +27,13 @@ nova/trame/view/theme/assets/js/lodash.throttle.min.js,sha256=9csqjX-M-LVGJnF3z4
|
|
24
27
|
nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
|
25
28
|
nova/trame/view/theme/theme.py,sha256=HUeuVfzEgeYW65W-LcvXzfYNRHu6aQibGwwgHGyh3OA,11765
|
26
29
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
27
|
-
nova/trame/view_model/data_selector.py,sha256=
|
30
|
+
nova/trame/view_model/data_selector.py,sha256=WrdvCE8J_sye19srGBpBZbheu_YzBEEEo_u098WLh9g,2524
|
31
|
+
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
32
|
+
nova/trame/view_model/progress_bar.py,sha256=L7ED6TDn5v2142iu-qt3i-jUg_5JEhLyC476t2OtohU,2467
|
28
33
|
nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
|
29
|
-
|
30
|
-
nova_trame-0.
|
31
|
-
nova_trame-0.
|
32
|
-
nova_trame-0.
|
33
|
-
nova_trame-0.
|
34
|
+
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
35
|
+
nova_trame-0.20.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
36
|
+
nova_trame-0.20.1.dist-info/METADATA,sha256=k1zMQM3iexbogY55bKVC-vQaoFTK9W6MZxKN8MndVW4,1523
|
37
|
+
nova_trame-0.20.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
38
|
+
nova_trame-0.20.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
39
|
+
nova_trame-0.20.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|