Skip to content

Commit 0799152

Browse files
committed
Refactor dashboard components and routes for improved modularity and code organization
1 parent 6d08b94 commit 0799152

File tree

3 files changed

+130
-131
lines changed

3 files changed

+130
-131
lines changed

dashboard/batch_stats.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from datetime import datetime
2+
import pandas as pd
3+
from typing import Dict, Any
4+
5+
6+
def calculate_batch_stats(df: pd.DataFrame, batch_name: str) -> Dict[str, Any]:
7+
"""Calculate statistics for a batch of metrics."""
8+
if df.empty:
9+
return {
10+
"unique_metrics": 0,
11+
"latest_timestamp": "No data",
12+
"avg_score": 0,
13+
"alert_count": 0,
14+
}
15+
16+
# Calculate core stats
17+
avg_score = (
18+
df["metric_score"].fillna(0).mean() if "metric_score" in df.columns else 0
19+
)
20+
alert_count = (
21+
df["metric_alert"].fillna(0).sum() if "metric_alert" in df.columns else 0
22+
)
23+
24+
# Calculate time ago string
25+
latest_timestamp = df["metric_timestamp"].max()
26+
time_ago_str = _format_time_ago(latest_timestamp)
27+
28+
return {
29+
"unique_metrics": len(df["metric_name"].unique()),
30+
"latest_timestamp": time_ago_str,
31+
"avg_score": avg_score,
32+
"alert_count": alert_count,
33+
}
34+
35+
36+
def _format_time_ago(timestamp: str) -> str:
37+
"""Format a timestamp into a human readable time ago string."""
38+
if timestamp == "No data":
39+
return timestamp
40+
41+
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
42+
now = datetime.now(dt.tzinfo)
43+
diff_seconds = (now - dt).total_seconds()
44+
45+
if diff_seconds < 3600: # Less than 1 hour
46+
minutes_ago = round(diff_seconds / 60, 1)
47+
return f"{minutes_ago:.1f} minute{'s' if minutes_ago != 1 else ''} ago"
48+
elif diff_seconds < 86400: # Less than 24 hours
49+
hours_ago = round(diff_seconds / 3600, 1)
50+
return f"{hours_ago:.1f} hour{'s' if hours_ago != 1 else ''} ago"
51+
else: # Days or more
52+
days_ago = round(diff_seconds / 86400, 1)
53+
return f"{days_ago:.1f} day{'s' if days_ago != 1 else ''} ago"

dashboard/components.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,73 @@ def _create_alert_n_form(batch_name):
200200
hx_post=f"/batch/{batch_name}/update-n",
201201
hx_target="#main-content",
202202
)
203+
204+
205+
def create_batch_card(batch_name: str, stats: dict) -> Card:
206+
"""Create a card displaying batch information."""
207+
return Card(
208+
DivLAligned(
209+
Div(
210+
H4(batch_name, cls="mb-2"),
211+
DivLAligned(
212+
Div(
213+
DivLAligned(
214+
UkIcon("activity", cls="text-blue-500"),
215+
P(f"{stats['unique_metrics']} metrics", cls=TextPresets.muted_sm),
216+
cls="space-x-2",
217+
),
218+
DivLAligned(
219+
UkIcon("clock", cls="text-green-500"),
220+
P(f"{stats['latest_timestamp']}", cls=TextPresets.muted_sm),
221+
cls="space-x-2",
222+
),
223+
DivLAligned(
224+
UkIcon("bar-chart", cls="text-purple-500"),
225+
P(f"Avg Score: {stats['avg_score']:.1%}", cls=TextPresets.muted_sm),
226+
cls="space-x-2",
227+
),
228+
DivLAligned(
229+
UkIcon("alert-circle", cls="text-red-500"),
230+
P(f"{stats['alert_count']} alerts", cls=TextPresets.muted_sm),
231+
cls="space-x-2",
232+
),
233+
cls="space-y-2",
234+
)
235+
),
236+
),
237+
Button(
238+
batch_name,
239+
hx_get=f"/batch/{batch_name}",
240+
hx_push_url=f"/batch/{batch_name}",
241+
hx_target="#main-content",
242+
hx_indicator="#loading",
243+
cls=ButtonT.primary,
244+
),
245+
style="justify-content: space-between;",
246+
cls="flex-row items-center",
247+
),
248+
cls="p-6 hover:border-primary transition-colors duration-200",
249+
)
250+
251+
252+
def create_header() -> Div:
253+
"""Create the dashboard header."""
254+
return DivLAligned(
255+
H2(
256+
"Anomstack",
257+
P(
258+
"Painless open source anomaly detection for your metrics 📈📉🚀",
259+
cls=TextPresets.muted_sm,
260+
),
261+
cls="mb-2",
262+
),
263+
A(
264+
DivLAligned(UkIcon("github")),
265+
href="https://github.com/andrewm4894/anomstack",
266+
target="_blank",
267+
cls="uk-button uk-button-secondary",
268+
uk_tooltip="View on GitHub",
269+
),
270+
style="justify-content: space-between;",
271+
cls="mb-6",
272+
)

dashboard/routes.py

Lines changed: 7 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from app import app, rt
1212
from constants import *
1313
from data import get_data
14-
from components import _create_controls
14+
from components import _create_controls, create_batch_card, create_header
1515
from charts import ChartManager
16+
from batch_stats import calculate_batch_stats
1617

1718

1819
log = logging.getLogger("anomstack")
@@ -35,7 +36,7 @@ def index(request: Request):
3536
"""
3637
)
3738

38-
# Get batch stats
39+
# Calculate batch stats
3940
batch_stats = {}
4041
for batch_name in app.state.metric_batches:
4142
if batch_name not in app.state.df_cache:
@@ -50,52 +51,7 @@ def index(request: Request):
5051
else:
5152
df = app.state.df_cache[batch_name]
5253

53-
# Calculate average score and alert count for the batch, handling NaN values
54-
avg_score = (
55-
df["metric_score"].fillna(0).mean()
56-
if not df.empty and "metric_score" in df.columns
57-
else 0
58-
)
59-
alert_count = (
60-
df["metric_alert"].fillna(0).sum()
61-
if not df.empty and "metric_alert" in df.columns
62-
else 0
63-
)
64-
65-
latest_timestamp = df["metric_timestamp"].max() if not df.empty else "No data"
66-
if latest_timestamp != "No data":
67-
from datetime import datetime
68-
69-
# Parse the ISO format timestamp and format it to show date and time
70-
dt = datetime.fromisoformat(latest_timestamp.replace("Z", "+00:00"))
71-
72-
# Calculate time difference
73-
now = datetime.now(dt.tzinfo)
74-
diff_seconds = (now - dt).total_seconds()
75-
76-
# Choose appropriate time unit
77-
if diff_seconds < 3600: # Less than 1 hour
78-
minutes_ago = round(diff_seconds / 60, 1)
79-
time_ago_str = (
80-
f"{minutes_ago:.1f} minute{'s' if minutes_ago != 1 else ''} ago"
81-
)
82-
elif diff_seconds < 86400: # Less than 24 hours
83-
hours_ago = round(diff_seconds / 3600, 1)
84-
time_ago_str = (
85-
f"{hours_ago:.1f} hour{'s' if hours_ago != 1 else ''} ago"
86-
)
87-
else: # Days or more
88-
days_ago = round(diff_seconds / 86400, 1)
89-
time_ago_str = f"{days_ago:.1f} day{'s' if days_ago != 1 else ''} ago"
90-
91-
latest_timestamp = f"{time_ago_str}"
92-
93-
batch_stats[batch_name] = {
94-
"unique_metrics": len(df["metric_name"].unique()),
95-
"latest_timestamp": latest_timestamp,
96-
"avg_score": avg_score,
97-
"alert_count": alert_count # Add alert count to stats
98-
}
54+
batch_stats[batch_name] = calculate_batch_stats(df, batch_name)
9955

10056
# Sort the metric batches by alert count (primary) and avg score (secondary)
10157
sorted_batch_names = sorted(
@@ -108,25 +64,7 @@ def index(request: Request):
10864

10965
main_content = Div(
11066
Card(
111-
DivLAligned(
112-
H2(
113-
"Anomstack",
114-
P(
115-
"Painless open source anomaly detection for your metrics 📈📉🚀",
116-
cls=TextPresets.muted_sm,
117-
),
118-
cls="mb-2",
119-
),
120-
A(
121-
DivLAligned(UkIcon("github")),
122-
href="https://github.com/andrewm4894/anomstack",
123-
target="_blank",
124-
cls="uk-button uk-button-secondary",
125-
uk_tooltip="View on GitHub",
126-
),
127-
style="justify-content: space-between;",
128-
cls="mb-6",
129-
),
67+
create_header(),
13068
# Show warning if no metric batches
13169
(
13270
Div(
@@ -141,73 +79,11 @@ def index(request: Request):
14179
cls="mb-6",
14280
)
14381
if not app.state.metric_batches
144-
else None
145-
),
146-
(
147-
Grid(
148-
*[
149-
Card(
150-
DivLAligned(
151-
Div(
152-
H4(batch_name, cls="mb-2"),
153-
DivLAligned(
154-
Div(
155-
DivLAligned(
156-
UkIcon("activity", cls="text-blue-500"),
157-
P(
158-
f"{batch_stats[batch_name]['unique_metrics']} metrics",
159-
cls=TextPresets.muted_sm,
160-
),
161-
cls="space-x-2",
162-
),
163-
DivLAligned(
164-
UkIcon("clock", cls="text-green-500"),
165-
P(
166-
f"{batch_stats[batch_name]['latest_timestamp']}",
167-
cls=TextPresets.muted_sm,
168-
),
169-
cls="space-x-2",
170-
),
171-
DivLAligned(
172-
UkIcon("bar-chart", cls="text-purple-500"),
173-
P(
174-
f"Avg Score: {batch_stats[batch_name]['avg_score']:.1%}",
175-
cls=TextPresets.muted_sm,
176-
),
177-
cls="space-x-2",
178-
),
179-
DivLAligned(
180-
UkIcon("alert-circle", cls="text-red-500"),
181-
P(
182-
f"{batch_stats[batch_name]['alert_count']} alerts",
183-
cls=TextPresets.muted_sm,
184-
),
185-
cls="space-x-2",
186-
),
187-
cls="space-y-2",
188-
)
189-
),
190-
),
191-
Button(
192-
batch_name,
193-
hx_get=f"/batch/{batch_name}",
194-
hx_push_url=f"/batch/{batch_name}",
195-
hx_target="#main-content",
196-
hx_indicator="#loading",
197-
cls=ButtonT.primary,
198-
),
199-
style="justify-content: space-between;",
200-
cls="flex-row items-center",
201-
),
202-
cls="p-6 hover:border-primary transition-colors duration-200",
203-
)
204-
for batch_name in sorted_batch_names # Use sorted list instead of app.state.metric_batches
205-
],
82+
else Grid(
83+
*[create_batch_card(name, batch_stats[name]) for name in sorted_batch_names],
20684
cols=3,
20785
gap=4,
20886
)
209-
if app.state.metric_batches
210-
else None
21187
),
21288
cls="p-6",
21389
),

0 commit comments

Comments
 (0)