Skip to content

Commit 76429a5

Browse files
Improved UI in ml_service; [skip ci]
Improved UI in ml_service by introducing the main page, along with a sidebar for navigation. The main page includes a welcome message and a description of the application, as well as a brief overview of the features available. The sidebar contains links to different sections of the application. Arguments in pipelines are now styled to be more intuitive and user-friendly. A few of the argument descriptions and default values have been updated for better clarity. One test was updated to reflect the changes in the default environment argument.
1 parent 563da19 commit 76429a5

30 files changed

Lines changed: 1719 additions & 960 deletions

File tree

ml_service/backend/pipelines/models/pipelines_cli_args.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ class ExecuteExperimentWithLatestInput(BaseModel):
114114
"""Model for the input of the execute_experiment_with_latest_input pipeline."""
115115
problem: str
116116
segment: str
117-
env: str | None = "default"
117+
version: str
118+
env: str | None = "dev"
118119
strict: bool = True
119120
logging_level: LOGGING_LEVEL = "INFO"
120121
owner: str | None = "Sebastijan"
121122
clean_up_failure_management: bool = True
122-
version: str
123123
experiment_id: str | None = None
124124
overwrite_existing: bool = False
125125
top_k: int | None = None

ml_service/frontend/app.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""Main multi-page Dash app with collapsible sidebar and separate Home button."""
2+
3+
import dash
4+
import dash_bootstrap_components as dbc
5+
from dash import dcc, html
6+
from dash.dependencies import Input, Output, State
7+
8+
# Import all pages
9+
from ml_service.frontend.configs.data.page import get_layout as data_layout
10+
from ml_service.frontend.configs.data.page import register as data_register
11+
from ml_service.frontend.configs.features.page import get_layout as features_layout
12+
from ml_service.frontend.configs.features.page import register as features_register
13+
from ml_service.frontend.configs.modeling.page import get_layout as modeling_layout
14+
from ml_service.frontend.configs.modeling.page import register as modeling_register
15+
from ml_service.frontend.configs.pipeline_cfg.page import get_layout as pipeline_cfg_layout
16+
from ml_service.frontend.configs.pipeline_cfg.page import register as pipeline_cfg_register
17+
from ml_service.frontend.configs.promotion_thresholds.page import get_layout as promotion_layout
18+
from ml_service.frontend.configs.promotion_thresholds.page import register as promotion_register
19+
from ml_service.frontend.pipelines.page import get_layout as pipelines_layout
20+
from ml_service.frontend.pipelines.page import register as pipelines_register
21+
22+
# Initialize Dash
23+
app = dash.Dash(
24+
__name__,
25+
external_stylesheets=[dbc.themes.BOOTSTRAP, "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"],
26+
suppress_callback_exceptions=True
27+
)
28+
server = app.server
29+
30+
# Page mapping
31+
PAGES = {
32+
"Data Config": data_layout,
33+
"Feature Config": features_layout,
34+
"Modeling Config": modeling_layout,
35+
"Pipeline Config": pipeline_cfg_layout,
36+
"Promotion Thresholds": promotion_layout,
37+
"Pipelines": pipelines_layout,
38+
}
39+
40+
# Register page callbacks
41+
for register_func in [
42+
data_register,
43+
features_register,
44+
modeling_register,
45+
pipeline_cfg_register,
46+
promotion_register,
47+
pipelines_register,
48+
]:
49+
register_func(app)
50+
51+
# Home page
52+
def home_layout() -> dbc.Container:
53+
return dbc.Container(
54+
[
55+
html.H1(
56+
"ML Workflow - Homepage",
57+
style={"marginTop": "1rem", "fontWeight": "bold"}
58+
),
59+
html.P(
60+
"Welcome to the ML Service Frontend Dashboard. "
61+
"Use the sidebar to navigate through different pages.",
62+
style={"fontSize": "1.5rem", "marginTop": "4rem"}
63+
),
64+
html.Ul(
65+
[
66+
html.Li([
67+
html.Strong("Data Config:"),
68+
html.Ul([
69+
html.Li("Create and save data configurations."),
70+
html.Li("Includes interim + processed configs."),
71+
html.Li("Saves to: configs/data/{config_type}/{dataset_name}/{dataset_version}.yaml")
72+
])
73+
]),
74+
html.Li([
75+
html.Strong("Feature Config:"),
76+
html.Ul([
77+
html.Li("Create and save feature set configurations."),
78+
html.Li("Saves to the feature registry."),
79+
html.Li("Registry path: configs/feature_registry/features.yaml")
80+
])
81+
]),
82+
html.Li([
83+
html.Strong("Modeling Config:"),
84+
html.Ul([
85+
html.Li("Create and save modeling configurations."),
86+
html.Li("Includes model specs, search, and training configs."),
87+
html.Li("Saves to: configs/{config_type}/{problem}/{segment}/{version}.yaml")
88+
])
89+
]),
90+
html.Li([
91+
html.Strong("Pipeline Config:"),
92+
html.Ul([
93+
html.Li("Create and save pipeline configurations."),
94+
html.Li("Saves to: configs/pipelines/{data_type}/{algorithm}/{pipeline_version}.yaml")
95+
])
96+
]),
97+
html.Li([
98+
html.Strong("Promotion Thresholds:"),
99+
html.Ul([
100+
html.Li("Create and save promotion thresholds."),
101+
html.Li("Saves to the promotion thresholds registry."),
102+
html.Li("Registry path: configs/promotion/thresholds.yaml")
103+
])
104+
]),
105+
html.Li([
106+
html.Strong("Pipelines:"),
107+
html.Ul([
108+
html.Li("Run ML pipelines."),
109+
html.Li("Includes all of the pipelines found in the pipeline/ directory."),
110+
html.Li("Optional arguments have grey background, required arguments have white background."),
111+
])
112+
]),
113+
],
114+
style={"fontSize": "1.2rem", "marginTop": "5rem", "textAlign": "left", "marginLeft": "10%"}
115+
)
116+
],
117+
fluid=True,
118+
style={"minHeight": "100vh", "paddingTop": "50px", "backgroundColor": "#8fa0d8", "textAlign": "center"}
119+
)
120+
121+
# Sidebar links (excluding Home)
122+
def generate_page_links():
123+
ICONS = {
124+
"Data Config": "database",
125+
"Feature Config": "gear",
126+
"Modeling Config": "bar-chart",
127+
"Pipeline Config": "diagram-3",
128+
"Promotion Thresholds": "graph-up",
129+
"Pipelines": "play-circle"
130+
}
131+
links = []
132+
for name in PAGES:
133+
icon = ICONS.get(name)
134+
content = [html.I(className=f"bi bi-{icon} me-2"), name] if icon else name
135+
links.append(
136+
dbc.NavLink(content, href=f"/{name.replace(' ', '_')}", id=f"nav-{name.replace(' ', '_')}", active="exact")
137+
)
138+
return links
139+
140+
# Main layout with separate Home button and collapsible sidebar
141+
app.layout = dbc.Container(
142+
[
143+
dcc.Location(id="url"),
144+
dbc.Row(
145+
[
146+
dbc.Col(
147+
[
148+
# Separate Home button
149+
dbc.Button("Home", href="/", color="secondary", className="mb-3", style={"width": "100%"}),
150+
151+
# Sidebar toggle button (hamburger icon)
152+
dbc.Button("☰", id="sidebar-toggle", color="primary", className="mb-3", n_clicks=0, style={"width": "100%"}),
153+
154+
# Collapsible sidebar with page links
155+
dbc.Collapse(
156+
dbc.Nav(
157+
generate_page_links(),
158+
vertical=True,
159+
pills=True,
160+
),
161+
id="sidebar-collapse",
162+
is_open=True,
163+
),
164+
],
165+
width=2,
166+
style={"position": "sticky", "marginTop": "5rem", "padding": "10px"}
167+
),
168+
dbc.Col(
169+
html.Div(id="page-content-container", style={"padding": "20px"}),
170+
width=10,
171+
)
172+
],
173+
style={"minHeight": "100vh", "backgroundColor": "#c1cbda"}
174+
)
175+
],
176+
fluid=True
177+
)
178+
179+
# Callback to toggle sidebar collapse
180+
@app.callback(
181+
Output("sidebar-collapse", "is_open"),
182+
Input("sidebar-toggle", "n_clicks"),
183+
State("sidebar-collapse", "is_open")
184+
)
185+
def toggle_sidebar(n, is_open):
186+
if n:
187+
return not is_open
188+
return is_open
189+
190+
# Page routing
191+
@app.callback(
192+
Output("page-content-container", "children"),
193+
Input("url", "pathname")
194+
)
195+
def display_page(pathname):
196+
if not pathname or pathname == "/":
197+
return home_layout()
198+
page_name = pathname.lstrip("/").replace("_", " ")
199+
page_layout_func = PAGES.get(page_name)
200+
if page_layout_func:
201+
return page_layout_func()
202+
return dbc.Container(html.H2("404: Page not found", style={"textAlign": "center", "marginTop": "50px"}))
203+
204+
# Active link highlighting
205+
@app.callback(
206+
[Output(f"nav-{name.replace(' ', '_')}", "active") for name in PAGES],
207+
Input("url", "pathname")
208+
)
209+
def update_active_links(pathname):
210+
if not pathname:
211+
pathname = "/"
212+
current_page = pathname.lstrip("/").replace("_", " ")
213+
return [name == current_page for name in PAGES]
214+
215+
if __name__ == "__main__":
216+
app.run(debug=True, host="0.0.0.0", port=8050)

0 commit comments

Comments
 (0)