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,291 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from typing import Any, Dict, List, Type
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from algomancy_content import LibraryManager as library
|
|
6
|
+
from algomancy_data import InputFileConfiguration, BASE_DATA_BOUND, DataSource
|
|
7
|
+
from algomancy_content.pages.page import (
|
|
8
|
+
HomePage,
|
|
9
|
+
ScenarioPage,
|
|
10
|
+
ComparePage,
|
|
11
|
+
OverviewPage,
|
|
12
|
+
DataPage,
|
|
13
|
+
)
|
|
14
|
+
from algomancy_gui.stylingconfigurator import StylingConfigurator
|
|
15
|
+
from algomancy_scenario import ALGORITHM, BASE_KPI
|
|
16
|
+
from algomancy_scenario.core_configuration import CoreConfiguration
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AppConfiguration(CoreConfiguration):
|
|
20
|
+
"""
|
|
21
|
+
Central configuration object for the Algomancy dashboard.
|
|
22
|
+
|
|
23
|
+
Construct with your choices, validation runs on creation. Use `as_dict()`
|
|
24
|
+
to obtain the dictionary expected by `DashLauncher.build()` and
|
|
25
|
+
`SettingsManager`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
# === session manager configuration ===
|
|
31
|
+
use_sessions: bool = False,
|
|
32
|
+
# === path specifications ===
|
|
33
|
+
assets_path: str = "assets", # gui
|
|
34
|
+
data_path: str = "data",
|
|
35
|
+
# === data manager configuration ===
|
|
36
|
+
has_persistent_state: bool = False,
|
|
37
|
+
save_type: str | None = "json",
|
|
38
|
+
data_object_type: type[BASE_DATA_BOUND] | None = DataSource,
|
|
39
|
+
# === scenario manager configuration ===
|
|
40
|
+
etl_factory: Any | None = None,
|
|
41
|
+
kpi_templates: Dict[str, Type[BASE_KPI]] | None = None,
|
|
42
|
+
algo_templates: Dict[str, Type[ALGORITHM]] | None = None,
|
|
43
|
+
input_configs: List[InputFileConfiguration] | None = None,
|
|
44
|
+
# === auto start/create features ===
|
|
45
|
+
autocreate: bool | None = False,
|
|
46
|
+
default_algo: str | None = None,
|
|
47
|
+
default_algo_params_values: Dict[str, Any] | None = None,
|
|
48
|
+
autorun: bool | None = False,
|
|
49
|
+
# === content functions ===
|
|
50
|
+
home_page: HomePage | str = "standard", # gui
|
|
51
|
+
data_page: DataPage | str = "placeholder", # gui
|
|
52
|
+
scenario_page: ScenarioPage | str = "placeholder", # gui
|
|
53
|
+
compare_page: ComparePage | str = "placeholder", # gui
|
|
54
|
+
overview_page: OverviewPage | str = "standard", # gui
|
|
55
|
+
# === styling configuration ===
|
|
56
|
+
styling_config: Any | None = StylingConfigurator.get_cqm_config(), # gui
|
|
57
|
+
use_cqm_loader: bool = False, # gui
|
|
58
|
+
# === misc dashboard configurations ===
|
|
59
|
+
title: str = "Algomancy Dashboard",
|
|
60
|
+
host: str | None = None, # gui/api
|
|
61
|
+
port: int | None = None, # gui/api
|
|
62
|
+
# === page configurations ===
|
|
63
|
+
compare_default_open: List[str] | None = None, # gui
|
|
64
|
+
compare_ordered_list_components: List[str] | None = None, # gui
|
|
65
|
+
use_data_page_spinner: bool = True, # gui
|
|
66
|
+
use_scenario_page_spinner: bool = True, # gui
|
|
67
|
+
use_compare_page_spinner: bool = True, # gui
|
|
68
|
+
allow_parameter_upload_from_file: bool = False, # gui
|
|
69
|
+
# === authentication ===
|
|
70
|
+
use_authentication: bool = False, # gui
|
|
71
|
+
):
|
|
72
|
+
# initialize core part
|
|
73
|
+
super().__init__(
|
|
74
|
+
use_sessions=use_sessions,
|
|
75
|
+
data_path=data_path,
|
|
76
|
+
has_persistent_state=has_persistent_state,
|
|
77
|
+
save_type=save_type,
|
|
78
|
+
data_object_type=data_object_type,
|
|
79
|
+
etl_factory=etl_factory,
|
|
80
|
+
kpi_templates=kpi_templates,
|
|
81
|
+
algo_templates=algo_templates,
|
|
82
|
+
input_configs=input_configs,
|
|
83
|
+
autocreate=autocreate,
|
|
84
|
+
default_algo=default_algo,
|
|
85
|
+
default_algo_params_values=default_algo_params_values,
|
|
86
|
+
autorun=autorun,
|
|
87
|
+
title=title,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# paths (GUI)
|
|
91
|
+
self.assets_path = assets_path
|
|
92
|
+
|
|
93
|
+
# content + callbacks
|
|
94
|
+
self.home_page = home_page
|
|
95
|
+
self.data_page = data_page
|
|
96
|
+
self.scenario_page = scenario_page
|
|
97
|
+
self.compare_page = compare_page
|
|
98
|
+
self.overview_page = overview_page
|
|
99
|
+
|
|
100
|
+
# styling + misc
|
|
101
|
+
self.styling_config = styling_config
|
|
102
|
+
self.use_cqm_loader = use_cqm_loader
|
|
103
|
+
self.host = host or self._get_default_host()
|
|
104
|
+
self.port = port or 8050
|
|
105
|
+
|
|
106
|
+
# settings pages
|
|
107
|
+
self.compare_default_open = compare_default_open or []
|
|
108
|
+
self.compare_ordered_list_components = compare_ordered_list_components or []
|
|
109
|
+
self.show_loading_on_datapage = use_data_page_spinner
|
|
110
|
+
self.show_loading_on_scenariopage = use_scenario_page_spinner
|
|
111
|
+
self.show_loading_on_comparepage = use_compare_page_spinner
|
|
112
|
+
self.allow_parameter_upload_from_file = allow_parameter_upload_from_file
|
|
113
|
+
|
|
114
|
+
# auth
|
|
115
|
+
self.use_authentication = use_authentication
|
|
116
|
+
|
|
117
|
+
# validate GUI-specific pieces immediately (core validated in super())
|
|
118
|
+
self._validate_gui()
|
|
119
|
+
|
|
120
|
+
# public API
|
|
121
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
# === session manager configuration ===
|
|
124
|
+
"use_sessions": self.use_sessions,
|
|
125
|
+
# === path specifications ===
|
|
126
|
+
"assets_path": self.assets_path,
|
|
127
|
+
"data_path": self.data_path,
|
|
128
|
+
# === data manager configuration ===
|
|
129
|
+
"has_persistent_state": self.has_persistent_state,
|
|
130
|
+
"save_type": self.save_type,
|
|
131
|
+
"data_object_type": self.data_object_type,
|
|
132
|
+
# === scenario manager configuration ===
|
|
133
|
+
"etl_factory": self.etl_factory,
|
|
134
|
+
"kpi_templates": self.kpi_templates,
|
|
135
|
+
"algo_templates": self.algo_templates,
|
|
136
|
+
"input_configs": self.input_configs,
|
|
137
|
+
"autocreate": self.autocreate,
|
|
138
|
+
"default_algo": self.default_algo,
|
|
139
|
+
"default_algo_params_values": self.default_algo_params_values,
|
|
140
|
+
"autorun": self.autorun,
|
|
141
|
+
# === content functions ===
|
|
142
|
+
"home_page": self.home_page,
|
|
143
|
+
"data_page": self.data_page,
|
|
144
|
+
"scenario_page": self.scenario_page,
|
|
145
|
+
"compare_page": self.compare_page,
|
|
146
|
+
"overview_page": self.overview_page,
|
|
147
|
+
# === styling configuration ===
|
|
148
|
+
"styling_config": self.styling_config,
|
|
149
|
+
"use_cqm_loader": self.use_cqm_loader,
|
|
150
|
+
# === misc dashboard configurations ===
|
|
151
|
+
"title": self.title,
|
|
152
|
+
"host": self.host,
|
|
153
|
+
"port": self.port,
|
|
154
|
+
# === page configurations ===
|
|
155
|
+
"compare_default_open": self.compare_default_open,
|
|
156
|
+
"compare_ordered_list_components": self.compare_ordered_list_components,
|
|
157
|
+
"show_loading_on_datapage": self.show_loading_on_datapage,
|
|
158
|
+
"show_loading_on_scenariopage": self.show_loading_on_scenariopage,
|
|
159
|
+
"show_loading_on_comparepage": self.show_loading_on_comparepage,
|
|
160
|
+
"allow_param_upload_by_file": self.allow_parameter_upload_from_file,
|
|
161
|
+
# === authentication ===
|
|
162
|
+
"use_authentication": self.use_authentication,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# validation helpers (GUI layer)
|
|
166
|
+
def _validate_gui(self) -> None:
|
|
167
|
+
self._validate_paths_gui()
|
|
168
|
+
self._validate_values_gui()
|
|
169
|
+
self._validate_pages()
|
|
170
|
+
self._validate_page_configurations()
|
|
171
|
+
|
|
172
|
+
def _validate_pages(self):
|
|
173
|
+
# fetch pages that were passed as str
|
|
174
|
+
home, data, scenario, compare, overview = library.get_pages(self.as_dict())
|
|
175
|
+
|
|
176
|
+
# check home page attributes
|
|
177
|
+
assert hasattr(home, "create_content")
|
|
178
|
+
assert hasattr(home, "register_callbacks"), (
|
|
179
|
+
"home_page.register_callbacks must be a function"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# check data page attributes
|
|
183
|
+
assert hasattr(data, "create_content"), (
|
|
184
|
+
"data_page.create_content must be a function"
|
|
185
|
+
)
|
|
186
|
+
assert hasattr(data, "register_callbacks"), (
|
|
187
|
+
"data_page.register_callbacks must be a function"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# check scenario page attributes
|
|
191
|
+
assert hasattr(scenario, "create_content"), (
|
|
192
|
+
"scenario_page.create_content must be a function"
|
|
193
|
+
)
|
|
194
|
+
assert hasattr(scenario, "register_callbacks"), (
|
|
195
|
+
"scenario_page.register_callbacks must be a function"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# check compare page attributes
|
|
199
|
+
assert hasattr(compare, "create_side_by_side_content"), (
|
|
200
|
+
"compare_page.create_side_by_side_content must be a function"
|
|
201
|
+
)
|
|
202
|
+
assert hasattr(compare, "create_compare_section"), (
|
|
203
|
+
"compare_page.create_compare_section must be a function"
|
|
204
|
+
)
|
|
205
|
+
assert hasattr(compare, "create_details_section"), (
|
|
206
|
+
"compare_page.create_details_section must be a function"
|
|
207
|
+
)
|
|
208
|
+
assert hasattr(compare, "register_callbacks"), (
|
|
209
|
+
"compare_page.register_callbacks must be a function"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# check overview page attributes
|
|
213
|
+
assert hasattr(overview, "create_content"), (
|
|
214
|
+
"overview_page.create_content must be a function"
|
|
215
|
+
)
|
|
216
|
+
assert hasattr(overview, "register_callbacks"), (
|
|
217
|
+
"scenario_page.register_callbacks must be a function"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _validate_page_configurations(self) -> None:
|
|
221
|
+
# basic type checks for collections
|
|
222
|
+
if not isinstance(self.compare_default_open, list):
|
|
223
|
+
raise ValueError("compare_default_open must be a list of strings")
|
|
224
|
+
if not isinstance(self.compare_ordered_list_components, list):
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"compare_ordered_list_components must be a list of strings"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# ensure all strings are valid
|
|
230
|
+
admissible_values = ["side-by-side", "kpis", "compare", "details"]
|
|
231
|
+
for component in self.compare_default_open:
|
|
232
|
+
if not isinstance(component, str):
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"compare_default_open must be a list of strings, but contains {component}"
|
|
235
|
+
)
|
|
236
|
+
if component not in admissible_values:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"compare_default_open contains invalid component: {component}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
for component in self.compare_ordered_list_components:
|
|
242
|
+
if not isinstance(component, str):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"compare_ordered_list_components must be a list of strings, but contains {component}"
|
|
245
|
+
)
|
|
246
|
+
if component not in admissible_values:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"compare_ordered_list_components contains invalid component: {component}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# ensure all strings are unique
|
|
252
|
+
if len(self.compare_default_open) != len(set(self.compare_default_open)):
|
|
253
|
+
raise ValueError("compare_default_open contains duplicate values")
|
|
254
|
+
if len(self.compare_ordered_list_components) != len(
|
|
255
|
+
set(self.compare_ordered_list_components)
|
|
256
|
+
):
|
|
257
|
+
raise ValueError(
|
|
258
|
+
"compare_ordered_list_components contains duplicate values"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def _validate_paths_gui(self) -> None:
|
|
262
|
+
if self.assets_path is None or self.assets_path == "":
|
|
263
|
+
raise ValueError("assets_path must be provided")
|
|
264
|
+
if not os.path.isdir(self.assets_path):
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"assets_path does not exist or is not a directory: {self.assets_path}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _validate_values_gui(self) -> None:
|
|
270
|
+
# booleans allowed to be False, but must not be None if specified
|
|
271
|
+
if self.use_authentication is None:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"Boolean configuration 'use_authentication' must be set to True or False, not None"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# host and port (host may be filled elsewhere; allow None)
|
|
277
|
+
if self.port is not None:
|
|
278
|
+
if not isinstance(self.port, int) or not (1 <= self.port <= 65535):
|
|
279
|
+
raise ValueError("port must be an integer between 1 and 65535")
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def _get_default_host() -> str:
|
|
283
|
+
if platform.system() == "Windows":
|
|
284
|
+
host = "127.0.0.1" # default host for windows
|
|
285
|
+
else:
|
|
286
|
+
host = "0.0.0.1" # default host for linux
|
|
287
|
+
return host
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def from_dict(cls, config: Dict[str, Any]) -> "AppConfiguration":
|
|
291
|
+
return cls(**config)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file is intentionally left empty to make the directory a Python package.
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
compare.py - Compare Dashboard Page
|
|
3
|
+
|
|
4
|
+
This module defines the layout and components for the compare dashboard page.
|
|
5
|
+
It includes scenario selection, KPI improvement displays, and secondary results sections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from dash import html, get_app, callback, Output, Input, State
|
|
11
|
+
import dash_bootstrap_components as dbc
|
|
12
|
+
|
|
13
|
+
from .kpicard import kpi_card
|
|
14
|
+
from ..componentids import (
|
|
15
|
+
PERF_DETAILS_COLLAPSE,
|
|
16
|
+
COMPARE_DETAIL_VIEW,
|
|
17
|
+
PERF_COMPARE_COLLAPSE,
|
|
18
|
+
PERF_PRIMARY_RESULTS,
|
|
19
|
+
PERF_KPI_COLLAPSE,
|
|
20
|
+
KPI_IMPROVEMENT_SECTION,
|
|
21
|
+
PERF_TOGGLE_CHECKLIST_LEFT,
|
|
22
|
+
PERF_TOGGLE_CHECKLIST_RIGHT,
|
|
23
|
+
LEFT_SCENARIO_OVERVIEW,
|
|
24
|
+
LEFT_SCENARIO_DROPDOWN,
|
|
25
|
+
ACTIVE_SESSION,
|
|
26
|
+
RIGHT_SCENARIO_OVERVIEW,
|
|
27
|
+
RIGHT_SCENARIO_DROPDOWN,
|
|
28
|
+
COMPARE_PAGE,
|
|
29
|
+
PERF_SBS_RIGHT_COLLAPSE,
|
|
30
|
+
PERF_SBS_LEFT_COLLAPSE,
|
|
31
|
+
)
|
|
32
|
+
from ..compare_page.scenarioselector import (
|
|
33
|
+
create_side_by_side_viewer,
|
|
34
|
+
create_side_by_side_selector,
|
|
35
|
+
)
|
|
36
|
+
from algomancy_gui.managergetters import get_scenario_manager
|
|
37
|
+
|
|
38
|
+
from ..settingsmanager import SettingsManager
|
|
39
|
+
from ..contentregistry import ContentRegistry
|
|
40
|
+
from algomancy_scenario import ScenarioManager, Scenario
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compare_page():
|
|
44
|
+
return html.Div(id=COMPARE_PAGE, className="compare-page")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@callback(
|
|
48
|
+
Output(COMPARE_PAGE, "children"),
|
|
49
|
+
Input(ACTIVE_SESSION, "data"),
|
|
50
|
+
)
|
|
51
|
+
def render_ordered_components(active_session_name):
|
|
52
|
+
"""Creates the compare page layout with scenario management functionality."""
|
|
53
|
+
|
|
54
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, active_session_name)
|
|
55
|
+
|
|
56
|
+
settings: SettingsManager = get_app().server.settings
|
|
57
|
+
|
|
58
|
+
header = create_header(settings)
|
|
59
|
+
selector = create_side_by_side_selector(sm)
|
|
60
|
+
|
|
61
|
+
orderable_components = {
|
|
62
|
+
"kpis": create_kpi_viewer(),
|
|
63
|
+
"side-by-side": create_side_by_side_viewer(),
|
|
64
|
+
"compare": create_primary_viewer(),
|
|
65
|
+
"details": create_details_viewer(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
order = get_component_order(orderable_components, settings, sm)
|
|
69
|
+
|
|
70
|
+
ordered_components = order_components(header, order, orderable_components, selector)
|
|
71
|
+
|
|
72
|
+
page = html.Div(ordered_components, className="compare-page")
|
|
73
|
+
return page
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def order_components(
|
|
77
|
+
header: dbc.Row,
|
|
78
|
+
order: list[str],
|
|
79
|
+
orderable_components: dict[str, dbc.Row | Any],
|
|
80
|
+
selector: dbc.Row,
|
|
81
|
+
) -> list[dbc.Row]:
|
|
82
|
+
# construct component list
|
|
83
|
+
ordered_components = [
|
|
84
|
+
header,
|
|
85
|
+
selector,
|
|
86
|
+
]
|
|
87
|
+
for comp_id in order:
|
|
88
|
+
ordered_components.append(orderable_components[comp_id])
|
|
89
|
+
return ordered_components
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate(configured_order, orderable_components, sm: ScenarioManager) -> bool:
|
|
93
|
+
for comp_id in configured_order:
|
|
94
|
+
if comp_id not in orderable_components:
|
|
95
|
+
sm.logger.warning(
|
|
96
|
+
f"Invalid component id '{comp_id}' in compare page order list."
|
|
97
|
+
)
|
|
98
|
+
sm.logger.warning(
|
|
99
|
+
f"Expected (possibly a a subset of) {list(orderable_components.keys())}."
|
|
100
|
+
)
|
|
101
|
+
sm.logger.warning("Reverting to default component order.")
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_component_order(
|
|
107
|
+
orderable_components: dict[str, dbc.Row | Any],
|
|
108
|
+
settings: SettingsManager,
|
|
109
|
+
sm: ScenarioManager,
|
|
110
|
+
) -> list[str]:
|
|
111
|
+
# set a default
|
|
112
|
+
default_order = list(orderable_components.keys())
|
|
113
|
+
|
|
114
|
+
# retrieve any custom setting
|
|
115
|
+
configured_order = settings.compare_ordered_list_components
|
|
116
|
+
|
|
117
|
+
# verify the custom setting is valid
|
|
118
|
+
if configured_order and not validate(configured_order, orderable_components, sm):
|
|
119
|
+
configured_order = None
|
|
120
|
+
|
|
121
|
+
# choose ordering
|
|
122
|
+
used_order = configured_order if configured_order else default_order
|
|
123
|
+
return used_order
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_details_viewer() -> dbc.Collapse:
|
|
127
|
+
return dbc.Collapse(
|
|
128
|
+
id=PERF_DETAILS_COLLAPSE,
|
|
129
|
+
children=[
|
|
130
|
+
html.H5("Detail view"),
|
|
131
|
+
html.Div(id=COMPARE_DETAIL_VIEW, className="details-view"),
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_primary_viewer() -> dbc.Collapse:
|
|
137
|
+
return dbc.Collapse(
|
|
138
|
+
id=PERF_COMPARE_COLLAPSE,
|
|
139
|
+
children=[
|
|
140
|
+
html.H4("Compare Results"),
|
|
141
|
+
html.Div(id=PERF_PRIMARY_RESULTS, className="compare-view"),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def create_kpi_viewer() -> dbc.Collapse:
|
|
147
|
+
return dbc.Collapse(
|
|
148
|
+
id=PERF_KPI_COLLAPSE,
|
|
149
|
+
children=[
|
|
150
|
+
html.H4("KPI Improvements"),
|
|
151
|
+
html.Div(id=KPI_IMPROVEMENT_SECTION, className="kpi-cards"),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_header(settings: SettingsManager) -> dbc.Row:
|
|
157
|
+
default_open = settings.compare_default_open
|
|
158
|
+
|
|
159
|
+
return dbc.Row(
|
|
160
|
+
[
|
|
161
|
+
dbc.Col(html.H1("Compare"), width=9),
|
|
162
|
+
dbc.Col(
|
|
163
|
+
dbc.Row(
|
|
164
|
+
[
|
|
165
|
+
dbc.Col(
|
|
166
|
+
dbc.Checklist(
|
|
167
|
+
options=[
|
|
168
|
+
{
|
|
169
|
+
"label": "Show side-by-side",
|
|
170
|
+
"value": "side-by-side",
|
|
171
|
+
},
|
|
172
|
+
{"label": "Show KPI cards", "value": "kpis"},
|
|
173
|
+
],
|
|
174
|
+
id=PERF_TOGGLE_CHECKLIST_LEFT,
|
|
175
|
+
class_name="styled-toggle",
|
|
176
|
+
switch=True,
|
|
177
|
+
value=default_open,
|
|
178
|
+
),
|
|
179
|
+
width=6,
|
|
180
|
+
),
|
|
181
|
+
dbc.Col(
|
|
182
|
+
dbc.Checklist(
|
|
183
|
+
options=[
|
|
184
|
+
{"label": "Show compare view", "value": "compare"},
|
|
185
|
+
{"label": "Show details", "value": "details"},
|
|
186
|
+
],
|
|
187
|
+
id=PERF_TOGGLE_CHECKLIST_RIGHT,
|
|
188
|
+
class_name="styled-toggle",
|
|
189
|
+
switch=True,
|
|
190
|
+
value=default_open,
|
|
191
|
+
),
|
|
192
|
+
width=6,
|
|
193
|
+
),
|
|
194
|
+
]
|
|
195
|
+
),
|
|
196
|
+
width=3,
|
|
197
|
+
),
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@callback(
|
|
203
|
+
Output(LEFT_SCENARIO_OVERVIEW, "children"),
|
|
204
|
+
Input(LEFT_SCENARIO_DROPDOWN, "value"),
|
|
205
|
+
State(ACTIVE_SESSION, "data"),
|
|
206
|
+
prevent_initial_call=True,
|
|
207
|
+
)
|
|
208
|
+
def update_left_scenario_overview(scenario_id: str, session_id: str) -> html.Div | str:
|
|
209
|
+
if not scenario_id:
|
|
210
|
+
return "No scenario selected."
|
|
211
|
+
scenario_manager = get_scenario_manager(
|
|
212
|
+
server=get_app().server, active_session_name=session_id
|
|
213
|
+
)
|
|
214
|
+
s: Optional[Scenario] = scenario_manager.get_by_id(scenario_id)
|
|
215
|
+
|
|
216
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
217
|
+
|
|
218
|
+
if not s:
|
|
219
|
+
return "Scenario not found."
|
|
220
|
+
|
|
221
|
+
return cr.compare_side_by_side(s, "left")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@callback(
|
|
225
|
+
Output(RIGHT_SCENARIO_OVERVIEW, "children"),
|
|
226
|
+
Input(RIGHT_SCENARIO_DROPDOWN, "value"),
|
|
227
|
+
State(ACTIVE_SESSION, "data"),
|
|
228
|
+
prevent_initial_call=True,
|
|
229
|
+
)
|
|
230
|
+
def update_right_scenario_overview(scenario_id, session_id) -> html.Div | str:
|
|
231
|
+
if not scenario_id:
|
|
232
|
+
return "No scenario selected."
|
|
233
|
+
scenario_manager: ScenarioManager = get_scenario_manager(
|
|
234
|
+
get_app().server, session_id
|
|
235
|
+
)
|
|
236
|
+
s = scenario_manager.get_by_id(scenario_id)
|
|
237
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
238
|
+
|
|
239
|
+
if not s:
|
|
240
|
+
return "Scenario not found."
|
|
241
|
+
|
|
242
|
+
return cr.compare_side_by_side(s, "right")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@callback(
|
|
246
|
+
Output(PERF_PRIMARY_RESULTS, "children"),
|
|
247
|
+
Input(LEFT_SCENARIO_DROPDOWN, "value"),
|
|
248
|
+
Input(RIGHT_SCENARIO_DROPDOWN, "value"),
|
|
249
|
+
State(ACTIVE_SESSION, "data"),
|
|
250
|
+
prevent_initial_call=True,
|
|
251
|
+
)
|
|
252
|
+
def update_right_scenario_overview_primary(
|
|
253
|
+
left_scenario_id, right_scenario_id, session_id
|
|
254
|
+
) -> html.Div:
|
|
255
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
|
|
256
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
257
|
+
|
|
258
|
+
# check the inputs
|
|
259
|
+
if not left_scenario_id or not right_scenario_id:
|
|
260
|
+
return html.Div("Select both scenarios to create a detail view.")
|
|
261
|
+
|
|
262
|
+
# retrieve the scenarios
|
|
263
|
+
left_scenario = sm.get_by_id(left_scenario_id)
|
|
264
|
+
right_scenario = sm.get_by_id(right_scenario_id)
|
|
265
|
+
|
|
266
|
+
# check if the scenarios were found
|
|
267
|
+
if not left_scenario or not right_scenario:
|
|
268
|
+
return html.Div("One of the scenarios was not found.")
|
|
269
|
+
|
|
270
|
+
# apply the function
|
|
271
|
+
return cr.compare_compare(left_scenario, right_scenario)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@callback(
|
|
275
|
+
Output(COMPARE_DETAIL_VIEW, "children"),
|
|
276
|
+
Input(LEFT_SCENARIO_DROPDOWN, "value"),
|
|
277
|
+
Input(RIGHT_SCENARIO_DROPDOWN, "value"),
|
|
278
|
+
State(ACTIVE_SESSION, "data"),
|
|
279
|
+
prevent_initial_call=True,
|
|
280
|
+
)
|
|
281
|
+
def update_right_scenario_overview_detail(
|
|
282
|
+
left_scenario_id,
|
|
283
|
+
right_scenario_id,
|
|
284
|
+
session_id: str,
|
|
285
|
+
) -> html.Div | str:
|
|
286
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
|
|
287
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
288
|
+
|
|
289
|
+
if not left_scenario_id or not right_scenario_id:
|
|
290
|
+
return "Select both scenarios to create a detail view."
|
|
291
|
+
|
|
292
|
+
if left_scenario_id == right_scenario_id:
|
|
293
|
+
return "Select two different scenarios to create a detail view."
|
|
294
|
+
|
|
295
|
+
left_scenario = sm.get_by_id(left_scenario_id)
|
|
296
|
+
right_scenario = sm.get_by_id(right_scenario_id)
|
|
297
|
+
if not left_scenario or not right_scenario:
|
|
298
|
+
return "One of the scenarios was not found."
|
|
299
|
+
|
|
300
|
+
return cr.compare_details(left_scenario, right_scenario)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@callback(
|
|
304
|
+
Output(KPI_IMPROVEMENT_SECTION, "children"),
|
|
305
|
+
Input(LEFT_SCENARIO_DROPDOWN, "value"),
|
|
306
|
+
Input(RIGHT_SCENARIO_DROPDOWN, "value"),
|
|
307
|
+
State(ACTIVE_SESSION, "data"),
|
|
308
|
+
)
|
|
309
|
+
def update_kpi_comparison(left_id, right_id, active_session_name):
|
|
310
|
+
if not left_id or not right_id:
|
|
311
|
+
return html.P("Select two completed scenarios to compare KPIs.")
|
|
312
|
+
sm: ScenarioManager = get_scenario_manager(get_app().server, active_session_name)
|
|
313
|
+
|
|
314
|
+
left = sm.get_by_id(left_id)
|
|
315
|
+
right = sm.get_by_id(right_id)
|
|
316
|
+
|
|
317
|
+
if not left or not right:
|
|
318
|
+
return html.P("One or both scenarios not found.")
|
|
319
|
+
|
|
320
|
+
# Example KPI dictionaries
|
|
321
|
+
left_kpis = left.kpis
|
|
322
|
+
right_kpis = right.kpis
|
|
323
|
+
|
|
324
|
+
assert len(left_kpis) == len(right_kpis), "KPIs do not match."
|
|
325
|
+
|
|
326
|
+
cards = []
|
|
327
|
+
for tag, left_kpi in left_kpis.items():
|
|
328
|
+
right_kpi = right_kpis.get(tag)
|
|
329
|
+
|
|
330
|
+
card = kpi_card(
|
|
331
|
+
left_kpi=left_kpi,
|
|
332
|
+
right_kpi=right_kpi,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
cards.append(html.Div(card, className="kpi-card-wrapper"))
|
|
336
|
+
|
|
337
|
+
return html.Div(cards, className="kpi-cards")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@callback(
|
|
341
|
+
Output(PERF_SBS_LEFT_COLLAPSE, "is_open"),
|
|
342
|
+
Output(PERF_SBS_RIGHT_COLLAPSE, "is_open"),
|
|
343
|
+
Output(PERF_KPI_COLLAPSE, "is_open"),
|
|
344
|
+
Input(PERF_TOGGLE_CHECKLIST_LEFT, "value"),
|
|
345
|
+
)
|
|
346
|
+
def listen_to_left_checklist(checked):
|
|
347
|
+
sbs_open = True if "side-by-side" in checked else False
|
|
348
|
+
kpi_open = True if "kpis" in checked else False
|
|
349
|
+
return sbs_open, sbs_open, kpi_open
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@callback(
|
|
353
|
+
Output(PERF_COMPARE_COLLAPSE, "is_open"),
|
|
354
|
+
Output(PERF_DETAILS_COLLAPSE, "is_open"),
|
|
355
|
+
Input(PERF_TOGGLE_CHECKLIST_RIGHT, "value"),
|
|
356
|
+
)
|
|
357
|
+
def listen_to_right_checklist(checked):
|
|
358
|
+
compare_open = True if "compare" in checked else False
|
|
359
|
+
details_open = True if "details" in checked else False
|
|
360
|
+
return compare_open, details_open
|