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,51 @@
|
|
|
1
|
+
from dash import Input, Output, State, callback, get_app
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from ..componentids import (
|
|
5
|
+
DATA_SELECTOR_DROPDOWN,
|
|
6
|
+
DM_DERIVE_SET_SELECTOR,
|
|
7
|
+
DM_DELETE_SET_SELECTOR,
|
|
8
|
+
DM_SAVE_SET_SELECTOR,
|
|
9
|
+
DM_DOWNLOAD_CHECKLIST,
|
|
10
|
+
DM_LIST_UPDATER_STORE,
|
|
11
|
+
ACTIVE_SESSION,
|
|
12
|
+
)
|
|
13
|
+
from algomancy_gui.managergetters import get_scenario_manager
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Callback functions for data management dialogs in the dashboard application.
|
|
17
|
+
|
|
18
|
+
This module contains all the callback functions that handle interactions with
|
|
19
|
+
the data management modals, including deriving, deleting, loading, and saving data.
|
|
20
|
+
Each callback is associated with specific UI components and manages the state
|
|
21
|
+
and data flow between the UI and the backend ScenarioManager.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@callback(
|
|
26
|
+
[
|
|
27
|
+
Output(DATA_SELECTOR_DROPDOWN, "options", allow_duplicate=True),
|
|
28
|
+
Output(DM_DERIVE_SET_SELECTOR, "options", allow_duplicate=True),
|
|
29
|
+
Output(DM_DELETE_SET_SELECTOR, "options", allow_duplicate=True),
|
|
30
|
+
Output(DM_SAVE_SET_SELECTOR, "options", allow_duplicate=True),
|
|
31
|
+
Output(DM_DOWNLOAD_CHECKLIST, "options", allow_duplicate=True),
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
Input(DM_LIST_UPDATER_STORE, "data"),
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
State(ACTIVE_SESSION, "data"),
|
|
38
|
+
],
|
|
39
|
+
prevent_initial_call=True,
|
|
40
|
+
)
|
|
41
|
+
def get_options_for_lists(data, session_id: str):
|
|
42
|
+
sm = get_scenario_manager(get_app().server, session_id)
|
|
43
|
+
|
|
44
|
+
options = [{"label": ds, "value": ds} for ds in sm.get_data_keys()]
|
|
45
|
+
derived_options = [
|
|
46
|
+
{"label": ds, "value": ds}
|
|
47
|
+
for ds in sm.get_data_keys()
|
|
48
|
+
if not sm.get_data(ds).is_master_data()
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
return options, options, options, derived_options, options
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import List, Dict
|
|
2
|
+
from itertools import zip_longest
|
|
3
|
+
|
|
4
|
+
from algomancy_data import InputFileConfiguration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def hamming_distance(s1, s2):
|
|
8
|
+
"""
|
|
9
|
+
Calculates the Hamming distance between two strings.
|
|
10
|
+
|
|
11
|
+
The Hamming distance is defined as the number of differing characters between
|
|
12
|
+
two strings of equal length. If the strings are of different lengths, it will
|
|
13
|
+
compare them up to the length of the shorter string and count differing
|
|
14
|
+
characters, considering any excess characters in the longer string as
|
|
15
|
+
additional differences.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
s1: The first string to compare.
|
|
19
|
+
s2: The second string to compare.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
int: The Hamming distance between the two strings.
|
|
23
|
+
"""
|
|
24
|
+
return sum(c1 != c2 for c1, c2 in zip_longest(s1, s2))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_closest_match(
|
|
28
|
+
file_names: List[str], file_configuration: InputFileConfiguration
|
|
29
|
+
) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Finds the closest match to a file configuration's reference name.
|
|
32
|
+
|
|
33
|
+
Finds the file name from a list that is the closest match to a given file configuration's
|
|
34
|
+
reference name using the Hamming distance as the measure of similarity.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
file_names (List[str]): A list of file names to evaluate.
|
|
38
|
+
file_configuration (InputFileConfiguration): Configuration object containing
|
|
39
|
+
the reference file name for comparison.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: The file name from the list that is the closest match to the reference
|
|
43
|
+
file name.
|
|
44
|
+
"""
|
|
45
|
+
return min(
|
|
46
|
+
file_names,
|
|
47
|
+
key=lambda x: hamming_distance(
|
|
48
|
+
x.lower(), file_configuration.file_name_with_extension.lower()
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_bijective_mapping(mapping: Dict[str, str]) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Determines if the input mapping is bijective.
|
|
56
|
+
|
|
57
|
+
A mapping is bijective if every element of the domain is mapped to a unique
|
|
58
|
+
element of the codomain, and every element of the codomain is uniquely
|
|
59
|
+
mapped back to an element in the domain. This function checks the bijection
|
|
60
|
+
by verifying that the lengths of the mapping's values and keys are equal
|
|
61
|
+
to the mapping itself.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
mapping (Dict[str, str]): A dictionary representing the mapping between
|
|
65
|
+
two sets where keys represent the domain and values represent the
|
|
66
|
+
codomain.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if the mapping is bijective, otherwise False.
|
|
70
|
+
"""
|
|
71
|
+
return len(mapping) == len(set(mapping.values())) and len(mapping) == len(
|
|
72
|
+
set(mapping.keys())
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def match_file_names(
|
|
77
|
+
file_configurations: List[InputFileConfiguration], file_names: List[str]
|
|
78
|
+
) -> Dict[str, str]:
|
|
79
|
+
"""
|
|
80
|
+
Matches file configurations to file names and attempts to create a bijective mapping.
|
|
81
|
+
|
|
82
|
+
This function takes a list of file configurations and file names and tries to match
|
|
83
|
+
each configuration to the closest corresponding name. The result will be a dictionary
|
|
84
|
+
representing a bijective mapping between the file configurations and the file names.
|
|
85
|
+
An exception is raised if such a bijective mapping cannot be established.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_configurations: List of InputFileConfiguration objects to match.
|
|
89
|
+
file_names: List of available file names.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
AssertionError: If the number of file_names is less than or greater than the number
|
|
93
|
+
of file_configurations.
|
|
94
|
+
Exception: If a bijective mapping between file configurations and file names cannot
|
|
95
|
+
be established.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A dictionary mapping file configuration names to file names.
|
|
99
|
+
"""
|
|
100
|
+
assert len(file_names) >= len(file_configurations), "Missing input files"
|
|
101
|
+
assert len(file_names) <= len(file_configurations), "Too many input files"
|
|
102
|
+
|
|
103
|
+
initial_guess = {
|
|
104
|
+
fc.file_name: find_closest_match(file_names, fc) for fc in file_configurations
|
|
105
|
+
}
|
|
106
|
+
if is_bijective_mapping(initial_guess):
|
|
107
|
+
return initial_guess
|
|
108
|
+
|
|
109
|
+
raise Exception("Could not find bijective mapping.")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dash import html
|
|
2
|
+
import dash_bootstrap_components as dbc
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def default_loader(text: str = "Loading... ", scale: float | None = None) -> html.Div:
|
|
6
|
+
"""
|
|
7
|
+
Creates a default loading with animated spinner
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
scale (float): float to control the size of the spinner. min: 0.5, max: 5
|
|
11
|
+
text (str): The text to display below the animation
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
html.Div: A div containing the animated loader
|
|
15
|
+
"""
|
|
16
|
+
assert (
|
|
17
|
+
not scale or 0.5 <= scale <= 5
|
|
18
|
+
), f"Invalid scale for loader: {scale}. (min: 0.5, max: 5)"
|
|
19
|
+
|
|
20
|
+
default_font_size = 1.5
|
|
21
|
+
default_spinner_w = 1.5
|
|
22
|
+
default_spinner_h = 1.5
|
|
23
|
+
|
|
24
|
+
text_style = {}
|
|
25
|
+
spinner_style = {}
|
|
26
|
+
if scale is not None:
|
|
27
|
+
text_style["font-size"] = f"{default_font_size * scale}rem"
|
|
28
|
+
spinner_style["width"] = f"{default_spinner_w * scale}rem"
|
|
29
|
+
spinner_style["height"] = f"{default_spinner_h * scale}rem"
|
|
30
|
+
return html.Div(
|
|
31
|
+
html.H2(
|
|
32
|
+
[text, dbc.Spinner(spinner_style=spinner_style)],
|
|
33
|
+
style=text_style,
|
|
34
|
+
className="default-spinner",
|
|
35
|
+
),
|
|
36
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from typing import Dict, Any, Union
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from waitress import serve
|
|
6
|
+
import dash_auth
|
|
7
|
+
from dash import get_app, Dash, html, dcc
|
|
8
|
+
from dash_bootstrap_components.themes import BOOTSTRAP
|
|
9
|
+
|
|
10
|
+
from .layout import LayoutCreator
|
|
11
|
+
from .contentregistry import ContentRegistry
|
|
12
|
+
from .settingsmanager import SettingsManager
|
|
13
|
+
from .sessionmanager import SessionManager
|
|
14
|
+
from .componentids import ACTIVE_SESSION
|
|
15
|
+
from .appconfiguration import AppConfiguration
|
|
16
|
+
from algomancy_content.librarymanager import LibraryManager as lm
|
|
17
|
+
from algomancy_scenario import ScenarioManager
|
|
18
|
+
from algomancy_utils.logger import MessageStatus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GuiLauncher:
|
|
22
|
+
@staticmethod
|
|
23
|
+
def build(cfg: Union[AppConfiguration, Dict[str, Any]]) -> Dash:
|
|
24
|
+
# Normalize configuration to AppConfiguration for a single source of truth
|
|
25
|
+
if isinstance(cfg, dict):
|
|
26
|
+
cfg_obj = AppConfiguration(**cfg)
|
|
27
|
+
elif isinstance(cfg, AppConfiguration):
|
|
28
|
+
cfg_obj = cfg
|
|
29
|
+
else:
|
|
30
|
+
raise TypeError("DashLauncher.build expects AppConfiguration or dict")
|
|
31
|
+
|
|
32
|
+
if cfg_obj.use_sessions:
|
|
33
|
+
manager: SessionManager = SessionManager.from_config(cfg_obj)
|
|
34
|
+
else:
|
|
35
|
+
manager: ScenarioManager = ScenarioManager.from_config(cfg_obj)
|
|
36
|
+
|
|
37
|
+
# Create the app
|
|
38
|
+
app = GuiLauncher._construct(
|
|
39
|
+
cfg=cfg_obj,
|
|
40
|
+
manager=manager,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# register authentication if enabled
|
|
44
|
+
if cfg_obj.use_authentication:
|
|
45
|
+
if not os.getenv("APP_USERNAME") or not os.getenv("APP_PASSWORD"):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"Environment variables 'APP_USERNAME' and 'APP_PASSWORD' must be set"
|
|
48
|
+
) # todo document where to set username and password
|
|
49
|
+
|
|
50
|
+
# add authentication to the app
|
|
51
|
+
dash_auth.BasicAuth(
|
|
52
|
+
app,
|
|
53
|
+
[[os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")]],
|
|
54
|
+
secret_key="secret-key",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return app
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _construct(
|
|
61
|
+
cfg: AppConfiguration,
|
|
62
|
+
manager: SessionManager | ScenarioManager,
|
|
63
|
+
) -> Dash:
|
|
64
|
+
# Initialize the app
|
|
65
|
+
external_stylesheets = [
|
|
66
|
+
BOOTSTRAP,
|
|
67
|
+
"https://use.fontawesome.com/releases/v5.15.4/css/all.css",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
from pathlib import Path
|
|
71
|
+
|
|
72
|
+
assets_path = Path(os.getcwd()) / Path(cfg.assets_path)
|
|
73
|
+
|
|
74
|
+
app = Dash(
|
|
75
|
+
external_stylesheets=external_stylesheets,
|
|
76
|
+
suppress_callback_exceptions=True,
|
|
77
|
+
assets_folder=str(assets_path),
|
|
78
|
+
)
|
|
79
|
+
app.title = cfg.title
|
|
80
|
+
|
|
81
|
+
# register the scenario manager on the app object
|
|
82
|
+
if isinstance(manager, SessionManager):
|
|
83
|
+
app.server.session_manager = manager
|
|
84
|
+
app.server.use_sessions = True
|
|
85
|
+
default_session_name = app.server.session_manager.start_session_name
|
|
86
|
+
elif isinstance(manager, ScenarioManager):
|
|
87
|
+
app.server.scenario_manager = manager
|
|
88
|
+
app.server.use_sessions = False
|
|
89
|
+
default_session_name = None
|
|
90
|
+
else:
|
|
91
|
+
raise TypeError(
|
|
92
|
+
"DashLauncher._construct expects SessionManager or ScenarioManager"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# register the styling configuration on the app object
|
|
96
|
+
app.server.styling_config = cfg.styling_config
|
|
97
|
+
|
|
98
|
+
# register the settings manager on the app object for access in callbacks
|
|
99
|
+
app.server.settings = SettingsManager(cfg.as_dict())
|
|
100
|
+
|
|
101
|
+
# fetch standard pages
|
|
102
|
+
home_page, data_page, scenario_page, compare_page, overview_page = lm.get_pages(
|
|
103
|
+
cfg.as_dict()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# register the content register functions
|
|
107
|
+
content_registry = ContentRegistry()
|
|
108
|
+
app.server.content_registry = content_registry
|
|
109
|
+
|
|
110
|
+
# register pages
|
|
111
|
+
content_registry.register_pages(
|
|
112
|
+
home_page, data_page, scenario_page, compare_page, overview_page
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# fill and run the app
|
|
116
|
+
app.layout = html.Div(
|
|
117
|
+
[
|
|
118
|
+
LayoutCreator.create_layout(cfg.styling_config),
|
|
119
|
+
dcc.Store(
|
|
120
|
+
id=ACTIVE_SESSION,
|
|
121
|
+
storage_type="session",
|
|
122
|
+
data=default_session_name,
|
|
123
|
+
),
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return app
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def run(
|
|
131
|
+
app: Dash,
|
|
132
|
+
host: str,
|
|
133
|
+
port: int,
|
|
134
|
+
threads: int = 8,
|
|
135
|
+
connection_limit: int = 100,
|
|
136
|
+
debug: bool = False,
|
|
137
|
+
) -> None:
|
|
138
|
+
server = get_app().server
|
|
139
|
+
if hasattr(server, "session_manager"):
|
|
140
|
+
manager = server.session_manager
|
|
141
|
+
elif hasattr(server, "scenario_manager"):
|
|
142
|
+
manager = server.scenario_manager
|
|
143
|
+
else:
|
|
144
|
+
raise Exception("No manager available")
|
|
145
|
+
|
|
146
|
+
algomancy_version = importlib.metadata.version("algomancy")
|
|
147
|
+
manager.log(f"Algomancy version: {algomancy_version}", MessageStatus.INFO)
|
|
148
|
+
|
|
149
|
+
if not debug:
|
|
150
|
+
manager.log(
|
|
151
|
+
"--------------------------------------------------------------------",
|
|
152
|
+
MessageStatus.SUCCESS,
|
|
153
|
+
)
|
|
154
|
+
manager.log(
|
|
155
|
+
f"Starting Dashboard server with Waitress on {host}:{port}...",
|
|
156
|
+
MessageStatus.SUCCESS,
|
|
157
|
+
)
|
|
158
|
+
manager.log(
|
|
159
|
+
f" threads:{threads}, connection limit: {connection_limit}",
|
|
160
|
+
MessageStatus.SUCCESS,
|
|
161
|
+
)
|
|
162
|
+
manager.log(
|
|
163
|
+
"--------------------------------------------------------------------",
|
|
164
|
+
MessageStatus.SUCCESS,
|
|
165
|
+
)
|
|
166
|
+
serve(
|
|
167
|
+
app.server,
|
|
168
|
+
host=host,
|
|
169
|
+
port=port,
|
|
170
|
+
threads=threads,
|
|
171
|
+
connection_limit=connection_limit,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
manager.log(
|
|
175
|
+
f"Starting Dashboard server in debug mode on {host}:{port}...",
|
|
176
|
+
MessageStatus.SUCCESS,
|
|
177
|
+
)
|
|
178
|
+
app.run(
|
|
179
|
+
debug=debug,
|
|
180
|
+
host=host,
|
|
181
|
+
port=port,
|
|
182
|
+
dev_tools_silence_routes_logging=False,
|
|
183
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file is intentionally left empty to make the directory a Python package.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dash import html, get_app
|
|
2
|
+
|
|
3
|
+
from algomancy_gui.contentregistry import ContentRegistry
|
|
4
|
+
|
|
5
|
+
from ..componentids import HOME_PAGE_CONTENT
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def home_page():
|
|
9
|
+
"""
|
|
10
|
+
Creates the home page layout.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
html.Div: A Dash HTML component representing the home page
|
|
14
|
+
"""
|
|
15
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
16
|
+
return html.Div(cr.home_content(), id=HOME_PAGE_CONTENT)
|
algomancy_gui/layout.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import dash_bootstrap_components as dbc
|
|
5
|
+
from dash import html, Output, callback, Input, State, dcc
|
|
6
|
+
from dash.html import Div
|
|
7
|
+
|
|
8
|
+
from algomancy_gui.stylingconfigurator import StylingConfigurator, LayoutSelection
|
|
9
|
+
from algomancy_gui.componentids import (
|
|
10
|
+
SIDEBAR_TOGGLE,
|
|
11
|
+
SIDEBAR,
|
|
12
|
+
PAGE_CONTENT,
|
|
13
|
+
SIDEBAR_COLLAPSED,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from algomancy_gui.home_page.home import home_page
|
|
17
|
+
from algomancy_gui.data_page.data import data_page
|
|
18
|
+
from algomancy_gui.scenario_page.scenarios import scenario_page
|
|
19
|
+
from algomancy_gui.compare_page.compare import compare_page
|
|
20
|
+
from algomancy_gui.admin_page.admin import admin_page
|
|
21
|
+
from algomancy_gui.overview_page.overview import overview_page
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LayoutCreator:
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _create_menu_sidebar(styling: StylingConfigurator):
|
|
27
|
+
# Create toggle button
|
|
28
|
+
toggle_button = html.Button(
|
|
29
|
+
html.I(className="fas fa-chevron-left"),
|
|
30
|
+
id=SIDEBAR_TOGGLE,
|
|
31
|
+
className="btn toggle-sidebar-button",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Create the sidebar (build children list dynamically based on available images)
|
|
35
|
+
sidebar_children: list[Any] = [toggle_button]
|
|
36
|
+
if styling.logo_url is not None or styling.button_url is not None:
|
|
37
|
+
logos = []
|
|
38
|
+
if styling.logo_url is not None:
|
|
39
|
+
logos.append(
|
|
40
|
+
html.Img(
|
|
41
|
+
src=styling.logo_url,
|
|
42
|
+
width="210px",
|
|
43
|
+
className="mb-2 expanded-logo sidebar-content-fade",
|
|
44
|
+
id="sidebar-logo",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if styling.button_url is not None:
|
|
49
|
+
logos.append(
|
|
50
|
+
html.Img(
|
|
51
|
+
src=styling.button_url,
|
|
52
|
+
width="40px",
|
|
53
|
+
className="mb-2 collapsed-logo",
|
|
54
|
+
id="sidebar-icon",
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
sidebar_children.extend(
|
|
58
|
+
[
|
|
59
|
+
html.Div(
|
|
60
|
+
logos,
|
|
61
|
+
className="sidebar-logo-wrapper",
|
|
62
|
+
id="sidebar-logo-wrapper",
|
|
63
|
+
),
|
|
64
|
+
html.Hr(className="bg-light"),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
nav_items = [
|
|
69
|
+
{"icon": "fas fa-home", "label": "Home", "href": "/", "index": 1},
|
|
70
|
+
{"icon": "fas fa-database", "label": "Data", "href": "/data", "index": 2},
|
|
71
|
+
{
|
|
72
|
+
"icon": "fas fa-project-diagram",
|
|
73
|
+
"label": "Scenarios",
|
|
74
|
+
"href": "/scenarios",
|
|
75
|
+
"index": 3,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"icon": "fas fa-chart-line",
|
|
79
|
+
"label": "Compare",
|
|
80
|
+
"href": "/compare",
|
|
81
|
+
"index": 4,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"icon": "fas fa-table",
|
|
85
|
+
"label": "Overview",
|
|
86
|
+
"href": "/overview",
|
|
87
|
+
"index": 5,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"icon": "fas fa-user-shield",
|
|
91
|
+
"label": "Admin",
|
|
92
|
+
"href": "/admin",
|
|
93
|
+
"index": 6,
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
sidebar_nav = dbc.Nav(
|
|
98
|
+
[
|
|
99
|
+
dbc.NavLink(
|
|
100
|
+
[
|
|
101
|
+
html.I(className=f"{item['icon']} me-2"),
|
|
102
|
+
html.Span(
|
|
103
|
+
item["label"],
|
|
104
|
+
id={"type": "sidebar-text", "index": item["index"]},
|
|
105
|
+
className="sidebar-content-fade",
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
href=item["href"],
|
|
109
|
+
className="sidebar-link",
|
|
110
|
+
id={"type": "sidebar-link", "index": item["index"]},
|
|
111
|
+
active="exact",
|
|
112
|
+
)
|
|
113
|
+
for item in nav_items
|
|
114
|
+
],
|
|
115
|
+
vertical=True,
|
|
116
|
+
pills=True,
|
|
117
|
+
className="sidebar-nav",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
sidebar_children.append(sidebar_nav)
|
|
121
|
+
|
|
122
|
+
# Create sidebar
|
|
123
|
+
sidebar = html.Div(
|
|
124
|
+
sidebar_children,
|
|
125
|
+
id=SIDEBAR,
|
|
126
|
+
className="expanded sidebar-layout",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Routing callback
|
|
130
|
+
@callback(Output("page-content", "children"), Input("url", "pathname"))
|
|
131
|
+
def display_page(pathname):
|
|
132
|
+
if pathname == "/":
|
|
133
|
+
return home_page()
|
|
134
|
+
elif pathname == "/data":
|
|
135
|
+
return data_page()
|
|
136
|
+
elif pathname == "/scenarios":
|
|
137
|
+
return scenario_page()
|
|
138
|
+
elif pathname == "/compare":
|
|
139
|
+
return compare_page()
|
|
140
|
+
elif pathname == "/overview":
|
|
141
|
+
return overview_page()
|
|
142
|
+
elif pathname == "/admin":
|
|
143
|
+
return admin_page()
|
|
144
|
+
else:
|
|
145
|
+
return html.H1("404 - Page not found")
|
|
146
|
+
|
|
147
|
+
@callback(
|
|
148
|
+
Output(SIDEBAR, "className"),
|
|
149
|
+
Output(PAGE_CONTENT, "className"),
|
|
150
|
+
Input(SIDEBAR_TOGGLE, "n_clicks"),
|
|
151
|
+
State(SIDEBAR, "className"),
|
|
152
|
+
State(PAGE_CONTENT, "className"),
|
|
153
|
+
)
|
|
154
|
+
def update_sidebar_class(
|
|
155
|
+
n_clicks, current_sidebar_className, current_page_content_className
|
|
156
|
+
):
|
|
157
|
+
if n_clicks is None:
|
|
158
|
+
return current_sidebar_className, current_page_content_className
|
|
159
|
+
is_expanded = "collapsed" not in current_sidebar_className
|
|
160
|
+
if is_expanded:
|
|
161
|
+
return "collapsed sidebar-layout", "collapsed page-content"
|
|
162
|
+
else:
|
|
163
|
+
return "expanded sidebar-layout", "expanded page-content"
|
|
164
|
+
|
|
165
|
+
return sidebar
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _create_sidebar_layout(styling: StylingConfigurator) -> html.Div:
|
|
169
|
+
themed_styling = styling.initiate_theme_colors()
|
|
170
|
+
|
|
171
|
+
layout = html.Div(
|
|
172
|
+
[
|
|
173
|
+
dcc.Location(id="url", refresh=False),
|
|
174
|
+
dcc.Store(id=SIDEBAR_COLLAPSED, data=False),
|
|
175
|
+
html.Div(
|
|
176
|
+
[
|
|
177
|
+
LayoutCreator._create_menu_sidebar(styling),
|
|
178
|
+
html.Div(id=PAGE_CONTENT, className="expanded page-content"),
|
|
179
|
+
],
|
|
180
|
+
className="layout-container",
|
|
181
|
+
),
|
|
182
|
+
],
|
|
183
|
+
style=themed_styling,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return layout
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def create_layout(styling_config: StylingConfigurator) -> Div | None:
|
|
190
|
+
match styling_config.layout_selection:
|
|
191
|
+
case LayoutSelection.SIDEBAR:
|
|
192
|
+
return LayoutCreator._create_sidebar_layout(styling_config)
|
|
193
|
+
case LayoutSelection.TABBED:
|
|
194
|
+
raise NotImplementedError
|
|
195
|
+
case LayoutSelection.FULLSCREEN:
|
|
196
|
+
raise NotImplementedError
|
|
197
|
+
case LayoutSelection.CUSTOM:
|
|
198
|
+
raise NotImplementedError
|
|
199
|
+
return None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dash import html, dcc
|
|
2
|
+
|
|
3
|
+
from algomancy_gui.cqmloader import cqm_loader
|
|
4
|
+
from algomancy_gui.defaultloader import default_loader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_wrapped_content_div(
|
|
8
|
+
content_div: html.Div,
|
|
9
|
+
show_loading: bool,
|
|
10
|
+
cqm: bool,
|
|
11
|
+
spinner_scale: float = 2,
|
|
12
|
+
) -> html.Div:
|
|
13
|
+
if show_loading:
|
|
14
|
+
spinner = (
|
|
15
|
+
cqm_loader(scale=spinner_scale)
|
|
16
|
+
if cqm
|
|
17
|
+
else default_loader(scale=spinner_scale)
|
|
18
|
+
)
|
|
19
|
+
return html.Div(
|
|
20
|
+
dcc.Loading(
|
|
21
|
+
content_div,
|
|
22
|
+
overlay_style={"visibility": "visible", "filter": "blur(2px)"},
|
|
23
|
+
custom_spinner=spinner,
|
|
24
|
+
delay_hide=0,
|
|
25
|
+
delay_show=200,
|
|
26
|
+
className="loading-wrapper",
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
return content_div
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from algomancy_scenario import ScenarioManager
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_scenario_manager(
|
|
5
|
+
server, active_session_name: str | None = None
|
|
6
|
+
) -> ScenarioManager:
|
|
7
|
+
"""Returns the scenario manager.
|
|
8
|
+
When sessions are enabled, this will return the active scenario manager via the session manager.
|
|
9
|
+
When sessions are disabled, this will return the scenario manager which was registered on the server object
|
|
10
|
+
"""
|
|
11
|
+
if hasattr(server, "session_manager"):
|
|
12
|
+
sm: ScenarioManager = server.session_manager.get_scenario_manager(
|
|
13
|
+
active_session_name
|
|
14
|
+
)
|
|
15
|
+
elif hasattr(server, "scenario_manager"):
|
|
16
|
+
sm: ScenarioManager = server.scenario_manager
|
|
17
|
+
else:
|
|
18
|
+
raise Exception("No sessionmanager or scenario manager available")
|
|
19
|
+
return sm
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_manager(server):
|
|
23
|
+
if hasattr(server, "session_manager"):
|
|
24
|
+
return server.session_manager
|
|
25
|
+
elif hasattr(server, "scenario_manager"):
|
|
26
|
+
return server.scenario_manager
|
|
27
|
+
else:
|
|
28
|
+
raise Exception("No sessionmanager or scenario manager available")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file is intentionally left empty to make the directory a Python package.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dash import html, get_app
|
|
2
|
+
|
|
3
|
+
from algomancy_gui.componentids import OVERVIEW_PAGE_CONTENT
|
|
4
|
+
from algomancy_gui.contentregistry import ContentRegistry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def overview_page():
|
|
8
|
+
"""
|
|
9
|
+
Creates the overview page layout with a table of completed scenarios and their KPIs.
|
|
10
|
+
|
|
11
|
+
This page displays a table where rows represent completed scenarios and columns represent KPIs.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
html.Div: A Dash HTML component representing the overview page
|
|
15
|
+
"""
|
|
16
|
+
cr: ContentRegistry = get_app().server.content_registry
|
|
17
|
+
|
|
18
|
+
page = html.Div(cr.overview_content(), id=OVERVIEW_PAGE_CONTENT)
|
|
19
|
+
|
|
20
|
+
return page
|
algomancy_gui/py.typed
ADDED
|
File without changes
|
|
File without changes
|