algomancy-gui 0.3.16__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.
- algomancy_gui/__init__.py +0 -0
- algomancy_gui/admin_page/__init__.py +1 -0
- algomancy_gui/admin_page/admin.py +362 -0
- algomancy_gui/admin_page/sessions.py +57 -0
- algomancy_gui/appconfiguration.py +291 -0
- algomancy_gui/compare_page/__init__.py +1 -0
- algomancy_gui/compare_page/compare.py +360 -0
- algomancy_gui/compare_page/kpicard.py +236 -0
- algomancy_gui/compare_page/scenarioselector.py +99 -0
- algomancy_gui/componentids.py +177 -0
- algomancy_gui/contentregistry.py +167 -0
- algomancy_gui/cqmloader.py +58 -0
- algomancy_gui/data_page/__init__.py +1 -0
- algomancy_gui/data_page/data.py +77 -0
- algomancy_gui/data_page/datamanagementdeletemodal.py +260 -0
- algomancy_gui/data_page/datamanagementderivemodal.py +201 -0
- algomancy_gui/data_page/datamanagementdownloadmodal.py +193 -0
- algomancy_gui/data_page/datamanagementimportmodal.py +438 -0
- algomancy_gui/data_page/datamanagementsavemodal.py +191 -0
- algomancy_gui/data_page/datamanagementtopbar.py +123 -0
- algomancy_gui/data_page/datamanagementuploadmodal.py +366 -0
- algomancy_gui/data_page/dialogcallbacks.py +51 -0
- algomancy_gui/data_page/filenamematcher.py +109 -0
- algomancy_gui/defaultloader.py +36 -0
- algomancy_gui/gui_launcher.py +183 -0
- algomancy_gui/home_page/__init__.py +1 -0
- algomancy_gui/home_page/home.py +16 -0
- algomancy_gui/layout.py +199 -0
- algomancy_gui/layouthelpers.py +30 -0
- algomancy_gui/managergetters.py +28 -0
- algomancy_gui/overview_page/__init__.py +1 -0
- algomancy_gui/overview_page/overview.py +20 -0
- algomancy_gui/py.typed +0 -0
- algomancy_gui/scenario_page/__init__.py +0 -0
- algomancy_gui/scenario_page/delete_confirmation.py +29 -0
- algomancy_gui/scenario_page/new_scenario_creator.py +104 -0
- algomancy_gui/scenario_page/new_scenario_parameters_window.py +154 -0
- algomancy_gui/scenario_page/scenario_badge.py +36 -0
- algomancy_gui/scenario_page/scenario_cards.py +119 -0
- algomancy_gui/scenario_page/scenarios.py +596 -0
- algomancy_gui/sessionmanager.py +168 -0
- algomancy_gui/settingsmanager.py +43 -0
- algomancy_gui/stylingconfigurator.py +740 -0
- algomancy_gui-0.3.16.dist-info/METADATA +71 -0
- algomancy_gui-0.3.16.dist-info/RECORD +46 -0
- algomancy_gui-0.3.16.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
from dash import (
|
|
2
|
+
callback_context,
|
|
3
|
+
html,
|
|
4
|
+
dcc,
|
|
5
|
+
callback,
|
|
6
|
+
Output,
|
|
7
|
+
Input,
|
|
8
|
+
ALL,
|
|
9
|
+
get_app,
|
|
10
|
+
ctx,
|
|
11
|
+
no_update,
|
|
12
|
+
State,
|
|
13
|
+
)
|
|
14
|
+
from dash.exceptions import PreventUpdate
|
|
15
|
+
|
|
16
|
+
from algomancy_scenario import ScenarioStatus
|
|
17
|
+
from ..componentids import (
|
|
18
|
+
SCENARIO_PROCESS_BUTTON,
|
|
19
|
+
SCENARIO_CREATOR_MODAL,
|
|
20
|
+
SCENARIO_TAG_INPUT,
|
|
21
|
+
SCENARIO_DATA_INPUT,
|
|
22
|
+
SCENARIO_ALGO_INPUT,
|
|
23
|
+
ALGO_PARAMS_WINDOW_ID,
|
|
24
|
+
ALGO_PARAMS_ENTRY_CARD,
|
|
25
|
+
SCENARIO_NEW_BUTTON,
|
|
26
|
+
SCENARIO_DELETE_MODAL,
|
|
27
|
+
SCENARIO_DELETE_BUTTON,
|
|
28
|
+
SCENARIO_CONFIRM_DELETE_BUTTON,
|
|
29
|
+
SCENARIO_CANCEL_DELETE_BUTTON,
|
|
30
|
+
SCENARIO_PAGE,
|
|
31
|
+
ACTIVE_SESSION,
|
|
32
|
+
SCENARIO_LIST_UPDATE_STORE,
|
|
33
|
+
SCENARIO_TO_DELETE,
|
|
34
|
+
SCENARIO_SELECTED_ID_STORE,
|
|
35
|
+
SCENARIO_ALERT,
|
|
36
|
+
SCENARIO_CREATOR_OPEN_BUTTON,
|
|
37
|
+
SCENARIO_PROG_INTERVAL,
|
|
38
|
+
SCENARIO_CURRENTLY_RUNNING_STORE,
|
|
39
|
+
SCENARIO_PROG_TEXT,
|
|
40
|
+
SCENARIO_PROG_BAR,
|
|
41
|
+
SCENARIO_PROG_COLLAPSE,
|
|
42
|
+
SCENARIO_LIST,
|
|
43
|
+
SCENARIO_SELECTED,
|
|
44
|
+
SCENARIO_CARD,
|
|
45
|
+
)
|
|
46
|
+
from .new_scenario_parameters_window import (
|
|
47
|
+
create_algo_parameters_entry_card_body,
|
|
48
|
+
)
|
|
49
|
+
from .scenario_cards import scenario_cards
|
|
50
|
+
from ..contentregistry import ContentRegistry
|
|
51
|
+
from algomancy_gui.managergetters import get_scenario_manager, get_manager
|
|
52
|
+
|
|
53
|
+
from ..layouthelpers import create_wrapped_content_div
|
|
54
|
+
from .delete_confirmation import (
|
|
55
|
+
delete_confirmation_modal,
|
|
56
|
+
)
|
|
57
|
+
from .new_scenario_creator import new_scenario_creator
|
|
58
|
+
|
|
59
|
+
import dash_bootstrap_components as dbc
|
|
60
|
+
|
|
61
|
+
from .scenario_cards import hidden_card
|
|
62
|
+
from algomancy_scenario import ScenarioManager
|
|
63
|
+
from ..settingsmanager import SettingsManager
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def scenario_page():
|
|
67
|
+
return html.Div(id=SCENARIO_PAGE)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- general page setup ---
|
|
71
|
+
def content_div() -> html.Div:
|
|
72
|
+
return html.Div(
|
|
73
|
+
id=SCENARIO_SELECTED,
|
|
74
|
+
className="mt-2 scenario-page-content",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@callback(
|
|
79
|
+
Output(SCENARIO_PAGE, "children"),
|
|
80
|
+
Input(ACTIVE_SESSION, "data"),
|
|
81
|
+
)
|
|
82
|
+
def render_scenario_page(active_session_name):
|
|
83
|
+
"""
|
|
84
|
+
Creates the scenarios page layout with scenario management functionality.
|
|
85
|
+
|
|
86
|
+
This page allows users to create, view, process, and delete scenarios.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
html.Div: A Dash HTML component representing the scenarios page
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
settings: SettingsManager = get_app().server.settings
|
|
93
|
+
content = create_wrapped_content_div(
|
|
94
|
+
content_div(), settings.show_loading_on_scenariopage, settings.use_cqm_loader
|
|
95
|
+
)
|
|
96
|
+
page = [
|
|
97
|
+
html.H2("Manage Scenarios"),
|
|
98
|
+
new_scenario_creator(active_session_name),
|
|
99
|
+
delete_confirmation_modal(),
|
|
100
|
+
dcc.Store(id=SCENARIO_LIST_UPDATE_STORE),
|
|
101
|
+
dcc.Store(id=SCENARIO_TO_DELETE),
|
|
102
|
+
dcc.Store(id=SCENARIO_SELECTED_ID_STORE),
|
|
103
|
+
dbc.Alert(id=SCENARIO_ALERT, dismissable=True, is_open=False, color="danger"),
|
|
104
|
+
# Two-column main content area:
|
|
105
|
+
dbc.Row(
|
|
106
|
+
[
|
|
107
|
+
# Left: Compact scenario list
|
|
108
|
+
dbc.Col(
|
|
109
|
+
[
|
|
110
|
+
# Add the open modal button above the list
|
|
111
|
+
dbc.Button(
|
|
112
|
+
"Create New Scenario",
|
|
113
|
+
id=SCENARIO_CREATOR_OPEN_BUTTON,
|
|
114
|
+
className="mb-1 new-scenario-button",
|
|
115
|
+
),
|
|
116
|
+
dbc.Collapse(
|
|
117
|
+
[
|
|
118
|
+
html.Div(
|
|
119
|
+
[
|
|
120
|
+
dcc.Interval(
|
|
121
|
+
id=SCENARIO_PROG_INTERVAL,
|
|
122
|
+
n_intervals=0,
|
|
123
|
+
interval=1000,
|
|
124
|
+
disabled=False,
|
|
125
|
+
),
|
|
126
|
+
dcc.Store(id=SCENARIO_CURRENTLY_RUNNING_STORE),
|
|
127
|
+
html.P(
|
|
128
|
+
"Processing: placeholder",
|
|
129
|
+
id=SCENARIO_PROG_TEXT,
|
|
130
|
+
className="mt-2",
|
|
131
|
+
),
|
|
132
|
+
dbc.Progress(
|
|
133
|
+
id=SCENARIO_PROG_BAR,
|
|
134
|
+
className="mt-2 scenario-progress-bar",
|
|
135
|
+
label="",
|
|
136
|
+
value=0,
|
|
137
|
+
),
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
],
|
|
141
|
+
id=SCENARIO_PROG_COLLAPSE,
|
|
142
|
+
is_open=False,
|
|
143
|
+
),
|
|
144
|
+
html.H4("Scenarios", className="mt-2"),
|
|
145
|
+
html.Div(
|
|
146
|
+
[
|
|
147
|
+
html.Div(
|
|
148
|
+
[hidden_card()],
|
|
149
|
+
id=SCENARIO_LIST,
|
|
150
|
+
style={
|
|
151
|
+
"overflowY": "auto",
|
|
152
|
+
"maxHeight": "70vh",
|
|
153
|
+
"minWidth": "200px",
|
|
154
|
+
"borderRight": "1px solid #ddd",
|
|
155
|
+
"paddingRight": "12px",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
],
|
|
159
|
+
style={
|
|
160
|
+
"height": "70vh",
|
|
161
|
+
"overflowY": "auto",
|
|
162
|
+
"backgroundColor": "var(--background-color)",
|
|
163
|
+
"borderRadius": "6px",
|
|
164
|
+
},
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
width=2,
|
|
168
|
+
style={"paddingLeft": "0", "paddingRight": "0"},
|
|
169
|
+
),
|
|
170
|
+
# Right: Selected scenario details
|
|
171
|
+
dbc.Col(content, width=10, style={"paddingLeft": "24px"}),
|
|
172
|
+
],
|
|
173
|
+
style={"height": "100%"},
|
|
174
|
+
),
|
|
175
|
+
]
|
|
176
|
+
return page
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@callback(
|
|
180
|
+
Output(SCENARIO_LIST_UPDATE_STORE, "data", allow_duplicate=True),
|
|
181
|
+
Output(SCENARIO_SELECTED, "children", allow_duplicate=True),
|
|
182
|
+
Output(SCENARIO_SELECTED_ID_STORE, "data", allow_duplicate=True),
|
|
183
|
+
Input({"type": SCENARIO_CARD, "index": ALL}, "n_clicks"),
|
|
184
|
+
State(ACTIVE_SESSION, "data"),
|
|
185
|
+
prevent_initial_call=True,
|
|
186
|
+
)
|
|
187
|
+
def select_scenario(card_clicks, session_id: str):
|
|
188
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
|
|
189
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
190
|
+
|
|
191
|
+
triggered = ctx.triggered_id
|
|
192
|
+
if isinstance(triggered, dict) and triggered["type"] == SCENARIO_CARD:
|
|
193
|
+
selected_card_id = triggered["index"]
|
|
194
|
+
s = sm.get_by_id(selected_card_id)
|
|
195
|
+
if s:
|
|
196
|
+
return "scenario selected", cr.scenario_content(s), selected_card_id
|
|
197
|
+
|
|
198
|
+
return no_update, no_update, no_update
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- Page Initialization Callback ---
|
|
202
|
+
@callback(
|
|
203
|
+
Output(SCENARIO_LIST_UPDATE_STORE, "data"),
|
|
204
|
+
Input("url", "pathname"),
|
|
205
|
+
State(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
206
|
+
State(ACTIVE_SESSION, "data"),
|
|
207
|
+
prevent_initial_call=False,
|
|
208
|
+
)
|
|
209
|
+
def initialize_page(pathname, selected_id, session_id):
|
|
210
|
+
"""
|
|
211
|
+
Initializes the scenario page when it is loaded.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
pathname (str): Current URL pathname
|
|
215
|
+
selected_id (str): ID of currently selected scenario
|
|
216
|
+
session_id (str): ID of active session
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
tuple: (
|
|
220
|
+
scenario cards component,
|
|
221
|
+
delete modal visibility,
|
|
222
|
+
ID of scenario to delete,
|
|
223
|
+
selected scenario display,
|
|
224
|
+
selected scenario ID
|
|
225
|
+
)
|
|
226
|
+
"""
|
|
227
|
+
scenario_manager: ScenarioManager = get_scenario_manager(
|
|
228
|
+
get_app().server, session_id
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Only initialize on page load
|
|
232
|
+
if pathname and "scenario" in pathname:
|
|
233
|
+
if scenario_manager.list_scenarios():
|
|
234
|
+
return "page initialized"
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# --- Process Scenario Callback ---
|
|
239
|
+
@callback(
|
|
240
|
+
Output(SCENARIO_PROG_INTERVAL, "disabled", allow_duplicate=True),
|
|
241
|
+
Input({"type": SCENARIO_PROCESS_BUTTON, "index": ALL}, "n_clicks"),
|
|
242
|
+
State(ACTIVE_SESSION, "data"),
|
|
243
|
+
prevent_initial_call=True,
|
|
244
|
+
)
|
|
245
|
+
def process_scenario(process_clicks, session_id):
|
|
246
|
+
"""
|
|
247
|
+
Processes a scenario when the process button is clicked.
|
|
248
|
+
|
|
249
|
+
Depending on the scenario's status, this will:
|
|
250
|
+
- CREATED: enqueue processing
|
|
251
|
+
- QUEUED/PROCESSING: request cancel
|
|
252
|
+
- COMPLETE/FAILED: refresh (reset to CREATED)
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
process_clicks (list): List of click counts for process buttons
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
bool | dash.no_update: Whether the progress interval should be disabled.
|
|
259
|
+
"""
|
|
260
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
|
|
261
|
+
|
|
262
|
+
triggered = ctx.triggered_id
|
|
263
|
+
if (
|
|
264
|
+
isinstance(triggered, dict)
|
|
265
|
+
and triggered["type"] == SCENARIO_PROCESS_BUTTON
|
|
266
|
+
and sum(process_clicks) > 0
|
|
267
|
+
):
|
|
268
|
+
scenario = sm.get_by_id(triggered["index"])
|
|
269
|
+
if not scenario:
|
|
270
|
+
return no_update
|
|
271
|
+
|
|
272
|
+
if scenario.status == ScenarioStatus.CREATED:
|
|
273
|
+
sm.process_scenario_async(scenario)
|
|
274
|
+
return False # enable progress interval
|
|
275
|
+
elif scenario.status in (ScenarioStatus.QUEUED, ScenarioStatus.PROCESSING):
|
|
276
|
+
scenario.cancel(logger=sm.logger)
|
|
277
|
+
return no_update
|
|
278
|
+
elif scenario.status in (ScenarioStatus.COMPLETE, ScenarioStatus.FAILED):
|
|
279
|
+
scenario.refresh(logger=sm.logger)
|
|
280
|
+
return no_update
|
|
281
|
+
|
|
282
|
+
return no_update
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_currently_processing_info(sm):
|
|
286
|
+
value = sm.currently_processing.progress
|
|
287
|
+
label = f"{value:.0f}%" if value > 10 else ""
|
|
288
|
+
message = f"Processing: {sm.currently_processing.tag}" # todo use textwrap to abbreviate tag
|
|
289
|
+
return value, label, message
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@callback(
|
|
293
|
+
[
|
|
294
|
+
Output(SCENARIO_PROG_BAR, "value"),
|
|
295
|
+
Output(SCENARIO_PROG_BAR, "label"),
|
|
296
|
+
Output(SCENARIO_PROG_TEXT, "children"),
|
|
297
|
+
Output(SCENARIO_PROG_COLLAPSE, "is_open"),
|
|
298
|
+
Output(SCENARIO_PROG_INTERVAL, "disabled", allow_duplicate=True),
|
|
299
|
+
Output(SCENARIO_CURRENTLY_RUNNING_STORE, "data"),
|
|
300
|
+
],
|
|
301
|
+
Input(SCENARIO_PROG_INTERVAL, "n_intervals"),
|
|
302
|
+
State(SCENARIO_CURRENTLY_RUNNING_STORE, "data"),
|
|
303
|
+
State(ACTIVE_SESSION, "data"),
|
|
304
|
+
prevent_initial_call=True,
|
|
305
|
+
)
|
|
306
|
+
def update_progress(n_intervals, msg, session_id):
|
|
307
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
|
|
308
|
+
if sm.currently_processing:
|
|
309
|
+
value, label, message = get_currently_processing_info(sm)
|
|
310
|
+
if message != msg:
|
|
311
|
+
return value, label, message, True, False, message
|
|
312
|
+
else:
|
|
313
|
+
return value, label, message, True, False, no_update
|
|
314
|
+
|
|
315
|
+
return 0, "", "", False, True, ""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@callback(
|
|
319
|
+
Output(SCENARIO_CREATOR_MODAL, "is_open"),
|
|
320
|
+
Input(SCENARIO_CREATOR_OPEN_BUTTON, "n_clicks"),
|
|
321
|
+
Input(f"{SCENARIO_CREATOR_MODAL}-cancel", "n_clicks"),
|
|
322
|
+
State(SCENARIO_CREATOR_MODAL, "is_open"),
|
|
323
|
+
prevent_initial_call=True,
|
|
324
|
+
)
|
|
325
|
+
def toggle_scenario_creator_modal(open_click, cancel_click, is_open):
|
|
326
|
+
ctx = callback_context
|
|
327
|
+
if not ctx.triggered:
|
|
328
|
+
return no_update
|
|
329
|
+
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
330
|
+
if triggered_id == SCENARIO_CREATOR_OPEN_BUTTON and not is_open:
|
|
331
|
+
return True
|
|
332
|
+
elif triggered_id in [f"{SCENARIO_CREATOR_MODAL}-cancel"]:
|
|
333
|
+
return False
|
|
334
|
+
return is_open
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@callback(
|
|
338
|
+
Output(SCENARIO_TAG_INPUT, "value"),
|
|
339
|
+
Output(SCENARIO_DATA_INPUT, "value"),
|
|
340
|
+
Output(SCENARIO_ALGO_INPUT, "value"),
|
|
341
|
+
Input(SCENARIO_CREATOR_MODAL, "is_open"),
|
|
342
|
+
prevent_initial_call=True,
|
|
343
|
+
)
|
|
344
|
+
def refresh_on_close(is_open):
|
|
345
|
+
if not is_open:
|
|
346
|
+
return "", "", ""
|
|
347
|
+
return no_update, no_update, no_update
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@callback(
|
|
351
|
+
Output(ALGO_PARAMS_WINDOW_ID, "is_open"),
|
|
352
|
+
Output(ALGO_PARAMS_ENTRY_CARD, "children"),
|
|
353
|
+
Input(SCENARIO_ALGO_INPUT, "value"),
|
|
354
|
+
State(ACTIVE_SESSION, "data"),
|
|
355
|
+
prevent_initial_call=True,
|
|
356
|
+
)
|
|
357
|
+
def open_algo_params_window(algo_name, session_id):
|
|
358
|
+
if algo_name:
|
|
359
|
+
try:
|
|
360
|
+
return True, create_algo_parameters_entry_card_body(algo_name)
|
|
361
|
+
except AssertionError:
|
|
362
|
+
return False, ""
|
|
363
|
+
return False, ""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# --- Scenario Creation Callback ---
|
|
367
|
+
@callback(
|
|
368
|
+
Output(SCENARIO_LIST_UPDATE_STORE, "data", allow_duplicate=True),
|
|
369
|
+
Output(SCENARIO_ALERT, "children", allow_duplicate=True),
|
|
370
|
+
Output(SCENARIO_ALERT, "is_open", allow_duplicate=True),
|
|
371
|
+
Output(SCENARIO_CREATOR_MODAL, "is_open", allow_duplicate=True),
|
|
372
|
+
Output(SCENARIO_PROG_INTERVAL, "disabled", allow_duplicate=True),
|
|
373
|
+
Input(SCENARIO_NEW_BUTTON, "n_clicks"),
|
|
374
|
+
State(SCENARIO_TAG_INPUT, "value"),
|
|
375
|
+
State(SCENARIO_DATA_INPUT, "value"),
|
|
376
|
+
State(SCENARIO_ALGO_INPUT, "value"),
|
|
377
|
+
State({"type": "algo-param-input", "param": ALL}, "value"),
|
|
378
|
+
State(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
379
|
+
State(ACTIVE_SESSION, "data"),
|
|
380
|
+
prevent_initial_call=True,
|
|
381
|
+
)
|
|
382
|
+
def create_scenario(
|
|
383
|
+
create_clicks, tag, dataset, algorithm, algo_param_values, selected_id, session_id
|
|
384
|
+
):
|
|
385
|
+
# Now algo_param_values is a list containing the values of each param input, in DOM order!
|
|
386
|
+
# You can also get their IDs from dash.callback_context.inputs_list for mapping
|
|
387
|
+
|
|
388
|
+
if not tag:
|
|
389
|
+
return no_update, "Tag is required", True, False, no_update
|
|
390
|
+
if not dataset:
|
|
391
|
+
return no_update, "Dataset is required", True, False, no_update
|
|
392
|
+
if not algorithm:
|
|
393
|
+
return no_update, "Algorithm is required", True, False, no_update
|
|
394
|
+
|
|
395
|
+
scenario_manager: ScenarioManager = get_scenario_manager(
|
|
396
|
+
get_app().server, session_id
|
|
397
|
+
)
|
|
398
|
+
interval_disabled = False if scenario_manager.auto_run_scenarios else no_update
|
|
399
|
+
|
|
400
|
+
algo_param_shell, data_param_shell = scenario_manager.get_associated_parameters(
|
|
401
|
+
algorithm
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
param_ids = [s["id"] for s in callback_context.states_list[3]]
|
|
405
|
+
algo_params = {
|
|
406
|
+
pid["param"]: value
|
|
407
|
+
for pid, value in zip(param_ids, algo_param_values)
|
|
408
|
+
if algo_param_shell.contains(pid["param"])
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
scenario_manager.create_scenario(tag, dataset, algorithm, algo_params)
|
|
413
|
+
return "new scenario created", "", False, False, interval_disabled
|
|
414
|
+
except Exception as e:
|
|
415
|
+
get_manager(get_app().server).logger.log_traceback(e)
|
|
416
|
+
return no_update, f"Error: {e}", True, False, no_update
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# --- Delete Modal Open Callback ---
|
|
420
|
+
@callback(
|
|
421
|
+
Output(SCENARIO_DELETE_MODAL, "is_open", allow_duplicate=True),
|
|
422
|
+
Output(SCENARIO_TO_DELETE, "data", allow_duplicate=True),
|
|
423
|
+
Input({"type": SCENARIO_DELETE_BUTTON, "index": ALL}, "n_clicks"),
|
|
424
|
+
State(ACTIVE_SESSION, "data"),
|
|
425
|
+
prevent_initial_call=True,
|
|
426
|
+
)
|
|
427
|
+
def open_delete_modal(delete_clicks, session_id):
|
|
428
|
+
"""
|
|
429
|
+
Opens the delete confirmation modal when a delete button is clicked.
|
|
430
|
+
Prevents opening on refresh or unrelated status updates.
|
|
431
|
+
"""
|
|
432
|
+
ctx = callback_context
|
|
433
|
+
if ctx.triggered and isinstance(ctx.triggered_id, dict):
|
|
434
|
+
if ctx.triggered_id.get("type") == SCENARIO_DELETE_BUTTON:
|
|
435
|
+
try:
|
|
436
|
+
# get the index in delete_clicks that is nonzero
|
|
437
|
+
idx = [i for i, e in enumerate(delete_clicks) if e != 0][0]
|
|
438
|
+
except IndexError:
|
|
439
|
+
return no_update, no_update
|
|
440
|
+
# Check that idx is a valid index in the list
|
|
441
|
+
if 0 <= idx < len(delete_clicks) and delete_clicks[idx]:
|
|
442
|
+
return True, get_scenario_manager(
|
|
443
|
+
get_app().server, session_id
|
|
444
|
+
).list_scenarios()[idx].id
|
|
445
|
+
return no_update, no_update
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# --- Delete Confirmation Callback ---
|
|
449
|
+
@callback(
|
|
450
|
+
Output(SCENARIO_LIST_UPDATE_STORE, "data", allow_duplicate=True),
|
|
451
|
+
Output(SCENARIO_DELETE_MODAL, "is_open", allow_duplicate=True),
|
|
452
|
+
Output(SCENARIO_SELECTED, "children", allow_duplicate=True),
|
|
453
|
+
Output(SCENARIO_SELECTED_ID_STORE, "data", allow_duplicate=True),
|
|
454
|
+
Input(SCENARIO_CONFIRM_DELETE_BUTTON, "n_clicks"),
|
|
455
|
+
State(SCENARIO_TO_DELETE, "data"),
|
|
456
|
+
State(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
457
|
+
State(ACTIVE_SESSION, "data"),
|
|
458
|
+
prevent_initial_call=True,
|
|
459
|
+
)
|
|
460
|
+
def confirm_delete_scenario(
|
|
461
|
+
confirm_clicks, scenario_to_delete, selected_id, session_id
|
|
462
|
+
):
|
|
463
|
+
"""
|
|
464
|
+
Deletes a scenario when the confirm delete button is clicked.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
confirm_clicks (int): Number of clicks on the confirm delete button
|
|
468
|
+
scenario_to_delete (str): ID of scenario marked for deletion
|
|
469
|
+
selected_id (str): ID of currently selected scenario
|
|
470
|
+
session_id (str): ID of active session
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
tuple: (
|
|
474
|
+
updated scenario cards component,
|
|
475
|
+
delete modal visibility,
|
|
476
|
+
selected scenario display,
|
|
477
|
+
selected scenario ID
|
|
478
|
+
)
|
|
479
|
+
"""
|
|
480
|
+
scenario_manager: ScenarioManager = get_scenario_manager(
|
|
481
|
+
get_app().server, session_id
|
|
482
|
+
)
|
|
483
|
+
if scenario_to_delete is not None:
|
|
484
|
+
scenario_manager.delete_scenario(scenario_to_delete)
|
|
485
|
+
|
|
486
|
+
if scenario_to_delete == selected_id:
|
|
487
|
+
return "scenario deleted", False, "No scenario selected.", None
|
|
488
|
+
|
|
489
|
+
return no_update, False, no_update, no_update
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# --- Cancel Delete Callback ---
|
|
493
|
+
@callback(
|
|
494
|
+
Output(SCENARIO_DELETE_MODAL, "is_open", allow_duplicate=True),
|
|
495
|
+
Input(SCENARIO_CANCEL_DELETE_BUTTON, "n_clicks"),
|
|
496
|
+
prevent_initial_call=True,
|
|
497
|
+
)
|
|
498
|
+
def cancel_delete_scenario(cancel_clicks):
|
|
499
|
+
"""
|
|
500
|
+
Closes the delete confirmation modal when the cancel button is clicked.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
cancel_clicks (int): Number of clicks on the cancel delete button
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Delete modal visibility
|
|
507
|
+
"""
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@callback(
|
|
512
|
+
Output(SCENARIO_LIST_UPDATE_STORE, "data", allow_duplicate=True),
|
|
513
|
+
Input(SCENARIO_CURRENTLY_RUNNING_STORE, "data"),
|
|
514
|
+
prevent_initial_call=True,
|
|
515
|
+
)
|
|
516
|
+
def trigger_refresh(msg):
|
|
517
|
+
return "processing update triggered"
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@callback(
|
|
521
|
+
Output(SCENARIO_LIST, "children"),
|
|
522
|
+
Input(SCENARIO_LIST_UPDATE_STORE, "data"),
|
|
523
|
+
State(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
524
|
+
State(ACTIVE_SESSION, "data"),
|
|
525
|
+
prevent_initial_call=True,
|
|
526
|
+
)
|
|
527
|
+
def refresh_cards(message, selected_id, session_id):
|
|
528
|
+
scenario_manager: ScenarioManager = get_scenario_manager(
|
|
529
|
+
get_app().server, session_id
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if selected_id in scenario_manager.list_ids():
|
|
533
|
+
return scenario_cards(scenario_manager, selected_id)
|
|
534
|
+
else:
|
|
535
|
+
return scenario_cards(scenario_manager, None)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@callback(
|
|
539
|
+
[
|
|
540
|
+
Output({"type": SCENARIO_CARD, "index": ALL}, "className"),
|
|
541
|
+
Output(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
542
|
+
],
|
|
543
|
+
[Input({"type": SCENARIO_CARD, "index": ALL}, "n_clicks")],
|
|
544
|
+
[
|
|
545
|
+
State({"type": SCENARIO_CARD, "index": ALL}, "id"),
|
|
546
|
+
State(SCENARIO_SELECTED_ID_STORE, "data"),
|
|
547
|
+
],
|
|
548
|
+
# prevent_initial_call=True
|
|
549
|
+
)
|
|
550
|
+
def handle_scenario_card_click(n_clicks_list, card_ids, selected_scenario_id):
|
|
551
|
+
"""
|
|
552
|
+
Handle scenario card selection - applies the 'selected' class to the clicked card
|
|
553
|
+
and removes it from all other cards.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
n_clicks_list: List of click counts for all scenario cards
|
|
557
|
+
card_ids: List of card IDs
|
|
558
|
+
selected_scenario_id: Currently selected scenario ID
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
tuple: (list of class names for all cards, newly selected scenario ID)
|
|
562
|
+
"""
|
|
563
|
+
# If callback triggered without a click
|
|
564
|
+
if not ctx.triggered:
|
|
565
|
+
# On initial load, set default classes based on stored selection
|
|
566
|
+
if selected_scenario_id:
|
|
567
|
+
return [
|
|
568
|
+
"scenario-card selected"
|
|
569
|
+
if card_id["index"] == selected_scenario_id
|
|
570
|
+
else "scenario-card"
|
|
571
|
+
for card_id in card_ids
|
|
572
|
+
], selected_scenario_id
|
|
573
|
+
else:
|
|
574
|
+
# No card selected initially
|
|
575
|
+
return ["scenario-card"] * len(card_ids), None
|
|
576
|
+
|
|
577
|
+
# Get the ID of the clicked card
|
|
578
|
+
triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
|
579
|
+
if not triggered_id:
|
|
580
|
+
raise PreventUpdate
|
|
581
|
+
|
|
582
|
+
# Extract the card index from the JSON string
|
|
583
|
+
import json
|
|
584
|
+
|
|
585
|
+
triggered_component = json.loads(triggered_id)
|
|
586
|
+
clicked_card_id = triggered_component["index"]
|
|
587
|
+
|
|
588
|
+
# Set the clicked card as selected and all others as not selected
|
|
589
|
+
new_class_names = []
|
|
590
|
+
for card_id in card_ids:
|
|
591
|
+
if card_id["index"] == clicked_card_id:
|
|
592
|
+
new_class_names.append("scenario-card selected")
|
|
593
|
+
else:
|
|
594
|
+
new_class_names.append("scenario-card")
|
|
595
|
+
|
|
596
|
+
return new_class_names, clicked_card_id
|