Skip to content

Commit e6e8ec1

Browse files
authored
Merge pull request #197 from andrewm4894/add-ui-dev-helper-db
Add UI dev helper db + chart modal
2 parents 3a7d028 + b16caad commit e6e8ec1

File tree

14 files changed

+1178
-31
lines changed

14 files changed

+1178
-31
lines changed

Makefile

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ dagster-cleanup-menu:
322322
# DASHBOARD OPERATIONS
323323
# =============================================================================
324324

325-
.PHONY: dashboard dashboardd dashboard-uvicorn dashboardd-uvicorn kill-dashboardd
325+
.PHONY: dashboard dashboardd dashboard-uvicorn dashboardd-uvicorn dashboard-local-dev kill-dashboardd seed-local-db
326326

327327
# start dashboard locally
328328
dashboard:
@@ -340,6 +340,46 @@ dashboardd:
340340
dashboardd-uvicorn:
341341
nohup uvicorn dashboard.app:app --host 0.0.0.0 --port 5003 --reload > /dev/null 2>&1 &
342342

343+
# seed local development database with dummy data
344+
seed-local-db:
345+
@echo "🌱 Seeding local development database (python_ingest_simple only)..."
346+
source venv/bin/activate && python scripts/development/seed_local_db.py --metric-batches "python_ingest_simple" --db-path tmpdata/anomstack-local-dev.db --hours 72 --interval 10 --force
347+
348+
seed-local-db-all:
349+
@echo "🌱 Seeding database with ALL metric batches (python_ingest_simple, netdata, posthog, yfinance, currency)..."
350+
source venv/bin/activate && python scripts/development/seed_local_db.py --metric-batches "python_ingest_simple,netdata,posthog,yfinance,currency" --db-path tmpdata/anomstack-all.db --hours 72 --interval 10 --force
351+
352+
seed-local-db-custom:
353+
@echo "🌱 Use: make seed-local-db-custom BATCHES='python_ingest_simple,netdata' DB_PATH='tmpdata/my.db'"
354+
@echo " Default BATCHES: python_ingest_simple"
355+
@echo " Default DB_PATH: tmpdata/anomstack-custom.db"
356+
source venv/bin/activate && python scripts/development/seed_local_db.py --metric-batches "$(or $(BATCHES),python_ingest_simple)" --db-path "$(or $(DB_PATH),tmpdata/anomstack-custom.db)" --hours 72 --interval 10 --force
357+
358+
# start dashboard in local development mode with seeded database
359+
dashboard-local-dev:
360+
@echo "🚀 Starting dashboard in local development mode..."
361+
@echo "📊 Using local development environment with seeded dummy data"
362+
@if [ ! -f "tmpdata/anomstack-local-dev.db" ]; then \
363+
echo "⚠️ Local dev database not found. Creating it first..."; \
364+
$(MAKE) seed-local-db; \
365+
fi
366+
source venv/bin/activate && ANOMSTACK_ENV_FILE_PATH=profiles/local-dev.env uvicorn dashboard.app:app --host 0.0.0.0 --port 5003 --reload
367+
368+
dashboard-local-dev-all:
369+
@echo "🚀 Starting dashboard in local development mode (ALL batches)..."
370+
@echo "📊 Using comprehensive database with python_ingest_simple, netdata, posthog, yfinance, AND currency metrics"
371+
@if [ ! -f "tmpdata/anomstack-all.db" ]; then \
372+
echo "⚠️ All-batches database not found. Creating it first..."; \
373+
$(MAKE) seed-local-db-all; \
374+
fi
375+
source venv/bin/activate && ANOMSTACK_ENV_FILE_PATH=profiles/local-dev-all.env uvicorn dashboard.app:app --host 0.0.0.0 --port 5003 --reload
376+
377+
dashboard-local-dev-custom:
378+
@echo "🚀 Starting dashboard in local development mode (custom database)..."
379+
@echo "💡 Use: make dashboard-local-dev-custom DB_PATH='tmpdata/my.db'"
380+
@echo " Default DB_PATH: tmpdata/anomstack-custom.db"
381+
source venv/bin/activate && ANOMSTACK_DUCKDB_PATH="$(or $(DB_PATH),tmpdata/anomstack-custom.db)" uvicorn dashboard.app:app --host 0.0.0.0 --port 5003 --reload
382+
343383
# kill any running dashboard process
344384
kill-dashboardd:
345385
kill $(shell ps aux | grep dashboard/app.py | grep -v grep | awk '{print $$2}') $(shell lsof -ti :5000)

anomstack/external/duckdb/duckdb.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def save_df_duckdb(df: pd.DataFrame, table_key: str) -> pd.DataFrame:
8787
if "." in table_key:
8888
schema, _ = table_key.split(".")
8989
query(connection=conn, query=f"CREATE SCHEMA IF NOT EXISTS {schema}")
90-
query(connection=conn, query=f"INSERT INTO {table_key} SELECT * FROM df")
90+
# Use explicit column names to avoid position-based mapping issues
91+
columns = ', '.join(df.columns)
92+
query(connection=conn, query=f"INSERT INTO {table_key} ({columns}) SELECT {columns} FROM df")
9193
except Exception:
9294
query(connection=conn, query=f"CREATE TABLE {table_key} AS SELECT * FROM df")
9395
return df
@@ -101,7 +103,9 @@ def save_df_duckdb(df: pd.DataFrame, table_key: str) -> pd.DataFrame:
101103
if "." in table_key:
102104
schema, _ = table_key.split(".")
103105
query(connection=conn, query=f"CREATE SCHEMA IF NOT EXISTS {schema}")
104-
query(connection=conn, query=f"INSERT INTO {table_key} SELECT * FROM df")
106+
# Use explicit column names to avoid position-based mapping issues
107+
columns = ', '.join(df.columns)
108+
query(connection=conn, query=f"INSERT INTO {table_key} ({columns}) SELECT {columns} FROM df")
105109
except Exception:
106110
query(connection=conn, query=f"CREATE TABLE {table_key} AS SELECT * FROM df")
107111
return df

dashboard/app.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,28 @@
2020
from dashboard.constants import POSTHOG_SCRIPT
2121
from dashboard.state import AppState
2222

23-
# load the environment variables
24-
load_dotenv(override=True)
23+
# load the environment variables with custom env file support
24+
def load_env_with_custom_path():
25+
"""Load environment variables from custom path or default .env file."""
26+
from pathlib import Path
27+
28+
env_file_path = os.getenv("ANOMSTACK_ENV_FILE_PATH")
29+
30+
if env_file_path:
31+
env_path = Path(env_file_path)
32+
if env_path.exists():
33+
print(f"🎯 Using custom environment file: {env_file_path}")
34+
load_dotenv(env_path, override=True)
35+
print("✅ Custom environment file loaded")
36+
else:
37+
print(f"❌ Custom environment file not found: {env_file_path}")
38+
print("📄 Falling back to default .env file")
39+
load_dotenv(override=True)
40+
else:
41+
# Standard .env loading
42+
load_dotenv(override=True)
43+
44+
load_env_with_custom_path()
2545

2646
log = logging.getLogger("anomstack_dashboard")
2747

dashboard/charts.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,55 @@ def create_chart(df_metric, chart_index):
6161
config=ChartManager.get_chart_config(),
6262
)
6363

64+
@staticmethod
65+
def create_expanded_chart(df_metric, chart_index):
66+
"""
67+
Create an expanded chart for modal display with enhanced interactivity.
68+
"""
69+
# Enhanced config for expanded view
70+
enhanced_config = {
71+
"displayModeBar": True,
72+
"modeBarButtonsToRemove": [
73+
"select2d",
74+
"lasso2d",
75+
],
76+
"responsive": True,
77+
"scrollZoom": True,
78+
"staticPlot": False,
79+
"fillFrame": True,
80+
"displaylogo": False,
81+
"modeBarButtonsToAdd": [],
82+
"toImageButtonOptions": {
83+
"format": "png",
84+
"filename": f"metric_chart_{chart_index}",
85+
"height": 600,
86+
"scale": 1
87+
}
88+
}
89+
90+
fig = plot_time_series(
91+
df_metric,
92+
small_charts=False, # Always use large size for expanded view
93+
dark_mode=app.state.dark_mode,
94+
show_markers=app.state.show_markers,
95+
line_width=app.state.line_width,
96+
show_legend=True, # Always show legend in expanded view
97+
)
98+
99+
# Update layout for expanded view with larger height and full width
100+
fig.update_layout(
101+
height=600,
102+
autosize=True,
103+
margin=dict(l=40, r=40, t=40, b=40)
104+
)
105+
106+
return fig.to_html(
107+
div_id=f"plotly-chart-expanded-{chart_index}",
108+
include_plotlyjs=False,
109+
full_html=False,
110+
config=enhanced_config,
111+
)
112+
64113
@staticmethod
65114
def create_chart_placeholder(metric_name, index, batch_name) -> Card:
66115
"""

dashboard/data.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datetime import datetime, timedelta
1111
import logging
1212
import re
13+
from typing import Union
1314

1415
import pandas as pd
1516

@@ -20,7 +21,7 @@
2021
log = logging.getLogger("anomstack_dashboard")
2122

2223

23-
def parse_time_spec(spec_str: str) -> dict:
24+
def parse_time_spec(spec_str: Union[str, int, None]) -> dict:
2425
"""
2526
Parse a time specification string into a dictionary with type and value.
2627
Supports formats:

dashboard/routes/batch.py

Lines changed: 128 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from dashboard.components import create_controls
1818
from dashboard.constants import DEFAULT_LAST_N, DEFAULT_LOAD_N_CHARTS
1919
from dashboard.data import get_data
20+
from monsterui.all import Modal, ModalTitle
2021

2122

2223
def get_batch_data(batch_name: str) -> pd.DataFrame:
@@ -123,27 +124,137 @@ def get(batch_name: str, chart_index: int):
123124
fig = ChartManager.create_chart(df_metric, chart_index)
124125
app.state.chart_cache[batch_name][chart_index] = fig
125126

126-
return Card(
127-
Style(
127+
modal_id = f"modal-{batch_name}-{chart_index}"
128+
129+
return Div(
130+
Card(
131+
Style(
132+
"""
133+
.uk-card-header { padding: 1rem; }
134+
.uk-card-body { padding: 1rem; }
135+
.chart-card-container { position: relative; }
136+
.chart-expand-btn {
137+
position: absolute;
138+
top: 0.75rem;
139+
right: 0.75rem;
140+
z-index: 20;
141+
background: rgba(255, 255, 255, 0.95);
142+
border: 1px solid #e5e7eb;
143+
border-radius: 0.375rem;
144+
padding: 0.375rem;
145+
cursor: pointer;
146+
transition: all 0.2s;
147+
display: flex;
148+
align-items: center;
149+
justify-content: center;
150+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
151+
min-width: 44px;
152+
min-height: 44px;
153+
}
154+
.chart-expand-btn:hover {
155+
background: rgba(255, 255, 255, 1);
156+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
157+
transform: scale(1.05);
158+
}
159+
/* Mobile optimizations */
160+
@media (max-width: 768px) {
161+
.chart-expand-btn {
162+
top: 0.5rem;
163+
right: 0.5rem;
164+
min-width: 48px;
165+
min-height: 48px;
166+
padding: 0.5rem;
167+
}
168+
}
169+
.dark .chart-expand-btn {
170+
background: rgba(17, 24, 39, 0.95);
171+
border-color: #374151;
172+
color: #e5e7eb;
173+
}
174+
.dark .chart-expand-btn:hover {
175+
background: rgba(17, 24, 39, 1);
176+
}
128177
"""
129-
.uk-card-header { padding: 1rem; }
130-
.uk-card-body { padding: 1rem; }
131-
"""
132-
),
133-
Safe(app.state.chart_cache[batch_name][chart_index]),
134-
header=Div(
135-
H4(metric_name, cls="mb-1"),
136-
DivLAligned(
137-
P(
138-
f"Anomaly Rate: {anomaly_rate:.1%}",
139-
cls="text-sm text-muted-foreground",
178+
),
179+
Safe(app.state.chart_cache[batch_name][chart_index]),
180+
# Expand button overlay positioned on entire card
181+
Button(
182+
UkIcon("expand", height=16, width=16),
183+
cls="chart-expand-btn",
184+
uk_toggle=f"target: #{modal_id}",
185+
title="Click to expand chart",
186+
type="button"
187+
),
188+
header=Div(
189+
H4(metric_name, cls="mb-1"),
190+
DivLAligned(
191+
P(
192+
f"Anomaly Rate: {anomaly_rate:.1%}",
193+
cls="text-sm text-muted-foreground",
194+
),
195+
P(f"Avg Score: {avg_score:.1%}", cls="text-sm text-muted-foreground"),
196+
style="gap: 1rem;",
140197
),
141-
P(f"Avg Score: {avg_score:.1%}", cls="text-sm text-muted-foreground"),
142-
style="gap: 1rem;",
143198
),
199+
id=f"chart-{chart_index}",
200+
cls="mb-1 chart-card-container",
144201
),
145-
id=f"chart-{chart_index}",
146-
cls="mb-1",
202+
# Modal for expanded chart view
203+
Modal(
204+
ModalTitle(f"{metric_name} - Expanded View"),
205+
Div(
206+
# Expanded chart will be loaded here
207+
Div(
208+
id=f"expanded-chart-{chart_index}",
209+
hx_get=f"/batch/{batch_name}/chart/{chart_index}/expanded",
210+
hx_trigger="load",
211+
cls="min-h-96"
212+
),
213+
# Chart statistics
214+
Div(
215+
DivLAligned(
216+
P(
217+
f"Anomaly Rate: {anomaly_rate:.1%}",
218+
cls="text-sm text-muted-foreground",
219+
),
220+
P(f"Avg Score: {avg_score:.1%}", cls="text-sm text-muted-foreground"),
221+
style="gap: 1rem;",
222+
),
223+
cls="mt-4 p-4 bg-muted rounded-lg"
224+
),
225+
cls="space-y-4"
226+
),
227+
id=modal_id,
228+
cls="uk-modal-full"
229+
)
230+
)
231+
232+
233+
@rt("/batch/{batch_name}/chart/{chart_index}/expanded")
234+
def get_expanded_chart(batch_name: str, chart_index: int):
235+
"""Get expanded chart for modal display.
236+
237+
Args:
238+
batch_name (str): The name of the batch.
239+
chart_index (int): The index of the chart.
240+
241+
Returns:
242+
Div: The expanded chart.
243+
"""
244+
df = app.state.df_cache[batch_name]
245+
metric_stats = app.state.stats_cache[batch_name]
246+
metric_name = metric_stats[chart_index]["metric_name"]
247+
248+
# Generate expanded chart (larger and with more interactive features)
249+
df_metric = df[df["metric_name"] == metric_name]
250+
df_metric = extract_metadata(df_metric, "anomaly_explanation")
251+
252+
# Create expanded chart with enhanced configuration
253+
expanded_fig = ChartManager.create_expanded_chart(df_metric, chart_index)
254+
255+
return Div(
256+
Safe(expanded_fig),
257+
cls="w-full"
147258
)
148259

149260

dashboard/static/styles.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,44 @@ body {
7575
}
7676
}
7777

78+
/* Mobile modal optimizations */
79+
@media (max-width: 768px) {
80+
.uk-modal-full .uk-modal-dialog {
81+
padding: 1rem;
82+
}
83+
84+
.uk-modal-title {
85+
font-size: 1.25rem;
86+
line-height: 1.3;
87+
margin-bottom: 1rem;
88+
}
89+
90+
/* Ensure charts are touch-friendly on mobile */
91+
.plotly .modebar {
92+
position: absolute;
93+
top: 10px;
94+
right: 10px;
95+
}
96+
97+
.plotly .modebar-btn {
98+
width: 32px;
99+
height: 32px;
100+
}
101+
}
102+
103+
/* Touch device optimizations */
104+
@media (hover: none) and (pointer: coarse) {
105+
.chart-expand-btn {
106+
opacity: 1 !important;
107+
transform: none !important;
108+
}
109+
110+
.chart-expand-btn:active {
111+
transform: scale(0.95);
112+
background: rgba(59, 130, 246, 0.1);
113+
}
114+
}
115+
78116
.loading-indicator {
79117
display: none;
80118
position: fixed;

0 commit comments

Comments
 (0)