Skip to content

Commit 4e0ddfa

Browse files
authored
feat(dashboard): Add dashboard breakdown controls (#485)
## Summary - add CI build-count breakdown charts with selectable repo, branch, and author grouping - add cost stack grouping by repo, author, and team, with chip-based focus for one selected value - bump CI Dashboard version to 0.4.2 ## Validation - `uv run --extra dev coverage run -m pytest` - `uv run --extra dev coverage report --fail-under=90` (TOTAL 91%) - `uv run --extra dev ruff check src tests` - `npm test` - `npm run build` ## Notes - Cost stack grouping is requested on demand through `group_by=repo|author|team` instead of fetching every grouping at once.
1 parent 680aa57 commit 4e0ddfa

14 files changed

Lines changed: 685 additions & 62 deletions

File tree

ci-dashboard/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ci-dashboard"
7-
version = "0.4.1"
7+
version = "0.4.2"
88
description = "CI dashboard jobs and API scaffold"
99
readme = "README.md"
1010
requires-python = ">=3.11"

ci-dashboard/src/ci_dashboard.egg-info/PKG-INFO

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.4
22
Name: ci-dashboard
3-
Version: 0.4.1
3+
Version: 0.4.2
44
Summary: CI dashboard jobs and API scaffold
55
Requires-Python: >=3.11
66
Description-Content-Type: text/markdown

ci-dashboard/src/ci_dashboard/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
__all__ = ["__version__"]
44

5-
__version__ = "0.4.1"
5+
__version__ = "0.4.2"

ci-dashboard/src/ci_dashboard/api/queries/builds.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
MIGRATION_IMPROVED_LIMIT = 10
2626
MIGRATION_REGRESSED_LIMIT = 10
2727
BUILD_TREND_JOB_RANKING_LIMIT = 10
28+
BUILD_COUNT_BREAKDOWN_LIMIT = 8
2829
MIGRATION_FIXED_BASELINE_START = date(2025, 12, 15)
2930
MIGRATION_FIXED_BASELINE_END = date(2026, 1, 14)
3031
MIGRATION_FIXED_RECENT_START = date(2026, 4, 15)
@@ -358,6 +359,163 @@ def get_cloud_migration_summary(engine: Engine, filters: CommonFilters) -> dict[
358359
}
359360

360361

362+
def get_build_count_breakdown_trend(engine: Engine, filters: CommonFilters) -> dict[str, Any]:
363+
return {
364+
"repo": _get_build_count_dimension_trend(
365+
engine,
366+
filters,
367+
dimension_key="repo",
368+
dimension_expr="COALESCE(NULLIF(b.repo_full_name, ''), '(unknown repo)')",
369+
empty_label="(unknown repo)",
370+
),
371+
"branch": _get_build_count_dimension_trend(
372+
engine,
373+
filters,
374+
dimension_key="branch",
375+
dimension_expr=f"COALESCE(NULLIF({branch_expr('b')}, ''), '(unknown branch)')",
376+
empty_label="(unknown branch)",
377+
),
378+
"author": _get_build_count_dimension_trend(
379+
engine,
380+
filters,
381+
dimension_key="author",
382+
dimension_expr="COALESCE(NULLIF(b.author, ''), '(unknown author)')",
383+
empty_label="(unknown author)",
384+
),
385+
"meta": {
386+
**filters.meta(),
387+
"limit": BUILD_COUNT_BREAKDOWN_LIMIT,
388+
},
389+
}
390+
391+
392+
def _get_build_count_dimension_trend(
393+
engine: Engine,
394+
filters: CommonFilters,
395+
*,
396+
dimension_key: str,
397+
dimension_expr: str,
398+
empty_label: str,
399+
) -> dict[str, Any]:
400+
with engine.begin() as connection:
401+
where_clause, params = build_common_where(filters, table_alias="b")
402+
builds_table = builds_table_expr(connection, filters, alias="b")
403+
bucket = bucket_expr(connection, "b.start_time", filters.granularity)
404+
rows = connection.execute(
405+
text(
406+
f"""
407+
SELECT
408+
{bucket} AS bucket_start,
409+
{dimension_expr} AS dimension_name,
410+
COUNT(*) AS build_count
411+
FROM {builds_table}
412+
WHERE {where_clause}
413+
GROUP BY bucket_start, {dimension_expr}
414+
ORDER BY bucket_start, build_count DESC, dimension_name ASC
415+
"""
416+
),
417+
params,
418+
).mappings()
419+
data_rows = [dict(row) for row in rows]
420+
if filters.granularity == "week":
421+
data_rows = filter_complete_week_rows(
422+
data_rows,
423+
start_date=filters.start_date,
424+
end_date=filters.end_date,
425+
)
426+
427+
totals: dict[str, int] = {}
428+
buckets: set[str] = set()
429+
counts_by_dimension: dict[str, dict[str, int]] = {}
430+
for row in data_rows:
431+
bucket_start = str(row["bucket_start"])
432+
dimension_name = str(row["dimension_name"] or empty_label)
433+
build_count = int(row["build_count"] or 0)
434+
buckets.add(bucket_start)
435+
totals[dimension_name] = totals.get(dimension_name, 0) + build_count
436+
counts_by_dimension.setdefault(dimension_name, {})[bucket_start] = build_count
437+
438+
ordered_buckets = sorted(buckets)
439+
if not ordered_buckets:
440+
return {
441+
"items": [],
442+
"series": [],
443+
"meta": {
444+
**filters.meta(),
445+
"dimension": dimension_key,
446+
"limit": BUILD_COUNT_BREAKDOWN_LIMIT,
447+
},
448+
}
449+
450+
total_builds = sum(totals.values())
451+
ordered_dimensions = sorted(
452+
totals,
453+
key=lambda name: (-totals[name], name),
454+
)
455+
visible_dimensions = ordered_dimensions[:BUILD_COUNT_BREAKDOWN_LIMIT]
456+
overflow_dimensions = ordered_dimensions[BUILD_COUNT_BREAKDOWN_LIMIT:]
457+
458+
items = [
459+
{
460+
"name": dimension_name,
461+
"value": totals[dimension_name],
462+
"share_pct": rate_pct(totals[dimension_name], total_builds),
463+
}
464+
for dimension_name in visible_dimensions
465+
]
466+
if overflow_dimensions:
467+
other_total = sum(totals[dimension_name] for dimension_name in overflow_dimensions)
468+
items.append(
469+
{
470+
"name": "Others",
471+
"value": other_total,
472+
"share_pct": rate_pct(other_total, total_builds),
473+
}
474+
)
475+
476+
series = [
477+
{
478+
"key": f"{dimension_key}:{dimension_name}",
479+
"label": dimension_name,
480+
"type": "bar",
481+
"points": [
482+
[bucket_start, counts_by_dimension.get(dimension_name, {}).get(bucket_start, 0)]
483+
for bucket_start in ordered_buckets
484+
],
485+
}
486+
for dimension_name in visible_dimensions
487+
]
488+
if overflow_dimensions:
489+
series.append(
490+
{
491+
"key": f"{dimension_key}:Others",
492+
"label": "Others",
493+
"type": "bar",
494+
"points": [
495+
[
496+
bucket_start,
497+
sum(
498+
counts_by_dimension.get(dimension_name, {}).get(bucket_start, 0)
499+
for dimension_name in overflow_dimensions
500+
),
501+
]
502+
for bucket_start in ordered_buckets
503+
],
504+
}
505+
)
506+
507+
return {
508+
"items": items,
509+
"series": series,
510+
"meta": {
511+
**filters.meta(),
512+
"dimension": dimension_key,
513+
"limit": BUILD_COUNT_BREAKDOWN_LIMIT,
514+
"total_builds": total_builds,
515+
},
516+
}
517+
518+
361519
def get_longest_avg_success_jobs(engine: Engine, filters: CommonFilters) -> dict[str, Any]:
362520
with engine.begin() as connection:
363521
where_clause, params = build_common_where(filters, table_alias="b")

ci-dashboard/src/ci_dashboard/api/queries/cost.py

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ci_dashboard.api.queries.base import CommonFilters, bucket_expr, rate_pct, to_number
1313

1414
COST_STACK_LIMIT = 8
15+
VALID_COST_STACK_GROUPS = frozenset({"repo", "author", "team"})
1516
UNMATCHED_RESOURCE_LIMIT = 20
1617
UNMATCHED_RESOURCE_MAX_WINDOW_DAYS = 31
1718
ENGINEERING_GROUP_NAME = "Engineering Group"
@@ -255,76 +256,85 @@ def _get_engineering_share_by_level_threshold(
255256
)
256257

257258

258-
def get_repo_group_cost_stack(engine: Engine, filters: CommonFilters) -> dict[str, Any]:
259+
def get_repo_group_cost_stack(
260+
engine: Engine,
261+
filters: CommonFilters,
262+
*,
263+
group_by: str = "repo",
264+
) -> dict[str, Any]:
265+
if group_by not in VALID_COST_STACK_GROUPS:
266+
group_by = "repo"
267+
259268
with engine.begin() as connection:
260269
where_clause, params = _build_cost_where(filters, table_alias="c")
261270
bucket = bucket_expr(connection, "c.usage_date", filters.granularity)
271+
dimension = _cost_stack_dimension(connection, group_by)
262272
top_rows = connection.execute(
263273
text(
264274
f"""
265275
SELECT
266-
COALESCE(NULLIF(c.repo, ''), '(no repo)') AS repo_name,
276+
{dimension["expr"]} AS dimension_name,
267277
SUM(c.list_cost) AS list_cost
268-
FROM cost_attribution_daily c
278+
FROM {dimension["from_clause"]}
269279
WHERE {where_clause}
270-
GROUP BY repo_name
271-
ORDER BY list_cost DESC, repo_name
280+
GROUP BY dimension_name
281+
ORDER BY list_cost DESC, dimension_name
272282
LIMIT :limit
273283
"""
274284
),
275-
{**params, "limit": COST_STACK_LIMIT},
285+
{**params, **dimension["params"], "limit": COST_STACK_LIMIT},
276286
).mappings()
277-
top_repos = [str(row["repo_name"]) for row in top_rows]
278-
if not top_repos:
287+
top_dimensions = [str(row["dimension_name"] or dimension["empty_label"]) for row in top_rows]
288+
if not top_dimensions:
279289
return {
280290
"series": [],
281291
"items": [],
282-
"meta": {**filters.meta(), "limit": COST_STACK_LIMIT},
292+
"meta": {**filters.meta(), "limit": COST_STACK_LIMIT, "group_by": group_by},
283293
}
284294

285295
dimension_conditions = []
286296
dimension_params: dict[str, Any] = {}
287-
for index, repo_name in enumerate(top_repos):
288-
repo_key = f"repo_{index}"
297+
for index, dimension_name in enumerate(top_dimensions):
298+
dimension_key = f"dimension_{index}"
289299
dimension_conditions.append(
290-
f"COALESCE(NULLIF(c.repo, ''), '(no repo)') = :{repo_key}"
300+
f"{dimension['expr']} = :{dimension_key}"
291301
)
292-
dimension_params[repo_key] = repo_name
302+
dimension_params[dimension_key] = dimension_name
293303

294304
rows = connection.execute(
295305
text(
296306
f"""
297307
SELECT
298308
{bucket} AS bucket_start,
299-
COALESCE(NULLIF(c.repo, ''), '(no repo)') AS repo_name,
309+
{dimension["expr"]} AS dimension_name,
300310
SUM(c.list_cost) AS list_cost
301-
FROM cost_attribution_daily c
311+
FROM {dimension["from_clause"]}
302312
WHERE {where_clause}
303313
AND ({" OR ".join(dimension_conditions)})
304-
GROUP BY bucket_start, repo_name
305-
ORDER BY bucket_start, repo_name
314+
GROUP BY bucket_start, dimension_name
315+
ORDER BY bucket_start, dimension_name
306316
"""
307317
),
308-
{**params, **dimension_params},
318+
{**params, **dimension["params"], **dimension_params},
309319
).mappings()
310320
data_rows = [dict(row) for row in rows]
311321

312322
buckets = _bucket_starts(filters, data_rows)
313323
values_by_key = {
314-
_repo_key(repo_name, index): {bucket: 0.0 for bucket in buckets}
315-
for index, repo_name in enumerate(top_repos)
324+
_cost_stack_key(group_by, dimension_name, index): {bucket: 0.0 for bucket in buckets}
325+
for index, dimension_name in enumerate(top_dimensions)
316326
}
317327
labels_by_key = {
318-
_repo_key(repo_name, index): repo_name
319-
for index, repo_name in enumerate(top_repos)
328+
_cost_stack_key(group_by, dimension_name, index): dimension_name
329+
for index, dimension_name in enumerate(top_dimensions)
320330
}
321-
repo_key_by_name = {
322-
repo_name: _repo_key(repo_name, index)
323-
for index, repo_name in enumerate(top_repos)
331+
key_by_name = {
332+
dimension_name: _cost_stack_key(group_by, dimension_name, index)
333+
for index, dimension_name in enumerate(top_dimensions)
324334
}
325335
for row in data_rows:
326-
repo_name = str(row["repo_name"])
327-
key = repo_key_by_name[repo_name]
336+
dimension_name = str(row["dimension_name"] or dimension["empty_label"])
337+
key = key_by_name[dimension_name]
328338
values_by_key[key][str(row["bucket_start"])] = _money(row["list_cost"])
329339

330340
return {
@@ -344,7 +354,7 @@ def get_repo_group_cost_stack(engine: Engine, filters: CommonFilters) -> dict[st
344354
}
345355
for key in values_by_key
346356
],
347-
"meta": {**filters.meta(), "limit": COST_STACK_LIMIT},
357+
"meta": {**filters.meta(), "limit": COST_STACK_LIMIT, "group_by": group_by},
348358
}
349359

350360

@@ -1157,10 +1167,61 @@ def _null_safe_eq(connection: Connection, left_expr: str, right_expr: str) -> st
11571167
return f"{left_expr} <=> {right_expr}"
11581168

11591169

1160-
def _repo_key(repo_name: str, index: int) -> str:
1161-
if repo_name == "(no repo)":
1170+
def _cost_stack_dimension(connection: Connection, group_by: str) -> dict[str, Any]:
1171+
if group_by not in VALID_COST_STACK_GROUPS:
1172+
group_by = "repo"
1173+
1174+
if group_by == "author":
1175+
return {
1176+
"expr": "COALESCE(NULLIF(c.author, ''), '(unknown author)')",
1177+
"from_clause": "cost_attribution_daily c",
1178+
"params": {},
1179+
"empty_label": "(unknown author)",
1180+
}
1181+
if group_by == "team":
1182+
team_match = _like_prefix_expr(connection, "c_group.path", "target_group.path")
1183+
return {
1184+
"expr": "COALESCE(NULLIF(target_group.name, ''), '(no team)')",
1185+
"from_clause": f"""
1186+
cost_attribution_daily c
1187+
LEFT JOIN roster_groups c_group
1188+
ON c_group.id = c.group_id
1189+
LEFT JOIN (
1190+
SELECT id, path
1191+
FROM roster_groups
1192+
WHERE name = :cost_stack_root_group_name
1193+
AND is_active = 1
1194+
ORDER BY id
1195+
LIMIT 1
1196+
) root_group
1197+
ON 1=1
1198+
LEFT JOIN roster_groups target_parent
1199+
ON target_parent.is_active = 1
1200+
AND target_parent.parent_id = root_group.id
1201+
LEFT JOIN roster_groups target_group
1202+
ON target_group.is_active = 1
1203+
AND target_group.parent_id = target_parent.id
1204+
AND {team_match}
1205+
""",
1206+
"params": {"cost_stack_root_group_name": ENGINEERING_GROUP_NAME},
1207+
"empty_label": "(no team)",
1208+
}
1209+
return {
1210+
"expr": "COALESCE(NULLIF(c.repo, ''), '(no repo)')",
1211+
"from_clause": "cost_attribution_daily c",
1212+
"params": {},
1213+
"empty_label": "(no repo)",
1214+
}
1215+
1216+
1217+
def _cost_stack_key(group_by: str, dimension_name: str, index: int) -> str:
1218+
if group_by == "repo" and dimension_name == "(no repo)":
11621219
return "repo__no_repo"
1163-
return f"repo__{index}"
1220+
if group_by == "author" and dimension_name == "(unknown author)":
1221+
return "author__unknown_author"
1222+
if group_by == "team" and dimension_name == "(no team)":
1223+
return "team__no_team"
1224+
return f"{group_by}__{index}"
11641225

11651226

11661227
def _cost_source_value(vendor: str, account_id: str) -> str:

0 commit comments

Comments
 (0)