Skip to content

Commit 8e32caf

Browse files
committed
Inlcude all workflows in HTML dashboard
1 parent cdfe94f commit 8e32caf

6 files changed

Lines changed: 255 additions & 35 deletions

File tree

src/rushti/commands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1635,7 +1635,11 @@ def _stats_visualize(args) -> None:
16351635
dynamodb_task_results_table=settings.stats.dynamodb_task_results_table,
16361636
dynamodb_endpoint_url=settings.stats.dynamodb_endpoint_url or None,
16371637
)
1638-
data = get_visualization_data(args.workflow, stats_db)
1638+
data = get_visualization_data(
1639+
args.workflow,
1640+
stats_db,
1641+
include_all_workflows=True,
1642+
)
16391643

16401644
print(f"Generating visualizations for workflow: {args.workflow}")
16411645
if not data.get("exists"):

src/rushti/dashboard.py

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def _prepare_dashboard_data(
195195
runs: List[Dict[str, Any]],
196196
task_results: List[Dict[str, Any]],
197197
default_runs: int,
198+
selected_workflow: Optional[str] = None,
198199
) -> Dict[str, Any]:
199200
"""Prepare all data for the dashboard template.
200201
@@ -330,16 +331,38 @@ def _prepare_dashboard_data(
330331
}
331332
)
332333

333-
# Taskfile metadata from the most recent run
334-
latest = runs[0] if runs else {}
334+
# Workflow metadata from the most recent run per workflow.
335+
workflow_meta: Dict[str, Dict[str, Any]] = {}
336+
for run in enriched_runs:
337+
wf = run.get("workflow") or ""
338+
if wf not in workflow_meta:
339+
workflow_meta[wf] = {
340+
"taskfile_name": run.get("taskfile_name", ""),
341+
"taskfile_description": run.get("taskfile_description", ""),
342+
"taskfile_author": run.get("taskfile_author", ""),
343+
"run_count": 0,
344+
}
345+
workflow_meta[wf]["run_count"] += 1
346+
347+
workflows = list(workflow_meta.keys())
348+
effective_workflow = selected_workflow if selected_workflow in workflow_meta else ""
349+
if not effective_workflow and workflows:
350+
effective_workflow = workflows[0]
351+
352+
workflow_run_count = workflow_meta.get(effective_workflow, {}).get("run_count", 0)
353+
effective_default_runs = min(default_runs, workflow_run_count)
335354

336355
return {
337-
"workflow": latest.get("workflow", ""),
338-
"taskfile_name": latest.get("taskfile_name", ""),
339-
"taskfile_description": latest.get("taskfile_description", ""),
340-
"taskfile_author": latest.get("taskfile_author", ""),
356+
"workflow": effective_workflow,
357+
"workflows": workflows,
358+
"workflow_meta": workflow_meta,
359+
"taskfile_name": workflow_meta.get(effective_workflow, {}).get("taskfile_name", ""),
360+
"taskfile_description": workflow_meta.get(effective_workflow, {}).get(
361+
"taskfile_description", ""
362+
),
363+
"taskfile_author": workflow_meta.get(effective_workflow, {}).get("taskfile_author", ""),
341364
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
342-
"default_runs": min(default_runs, len(runs)),
365+
"default_runs": effective_default_runs,
343366
"total_runs": len(runs),
344367
"runs": enriched_runs,
345368
"task_results": slim_task_results,
@@ -367,7 +390,12 @@ def generate_dashboard(
367390
:param dag_url: Optional relative URL to the DAG visualization HTML
368391
:return: Path to the generated HTML file
369392
"""
370-
data = _prepare_dashboard_data(runs, task_results, default_runs)
393+
data = _prepare_dashboard_data(
394+
runs,
395+
task_results,
396+
default_runs,
397+
selected_workflow=workflow,
398+
)
371399
data_json = json.dumps(data, default=str)
372400

373401
# Build conditional DAG link HTML
@@ -623,6 +651,10 @@ def generate_dashboard(
623651
</div>
624652
<div class="header-right">
625653
{dag_link_html}
654+
<div class="run-selector">
655+
<label for="workflowSelect">Workflow</label>
656+
<select id="workflowSelect" onchange="onWorkflowChange()"></select>
657+
</div>
626658
<div class="run-selector">
627659
<label for="runCount">Show last</label>
628660
<select id="runCount" onchange="updateDashboard()"></select>
@@ -758,33 +790,78 @@ def generate_dashboard(
758790
];
759791
760792
function init() {{
761-
// Build run count selector options
793+
// Build workflow selector first, then run selector for the selected workflow.
794+
const workflowSel = document.getElementById('workflowSelect');
795+
DATA.workflows.forEach(wf => {{
796+
const opt = document.createElement('option');
797+
const wfRunCount = (DATA.workflow_meta[wf] && DATA.workflow_meta[wf].run_count) || 0;
798+
opt.value = wf;
799+
opt.textContent = `${{wf}} (${{wfRunCount}})`;
800+
workflowSel.appendChild(opt);
801+
}});
802+
if (DATA.workflow && DATA.workflows.includes(DATA.workflow)) {{
803+
workflowSel.value = DATA.workflow;
804+
}}
805+
806+
rebuildRunCountSelector(DATA.default_runs);
807+
808+
document.getElementById('generatedAt').textContent = 'Generated: ' + DATA.generated_at;
809+
updateDashboard();
810+
}}
811+
812+
function getSelectedWorkflow() {{
813+
return document.getElementById('workflowSelect').value;
814+
}}
815+
816+
function getRunsForWorkflow(workflow) {{
817+
return DATA.runs.filter(r => (r.workflow || '') === workflow);
818+
}}
819+
820+
function rebuildRunCountSelector(preferredCount) {{
762821
const sel = document.getElementById('runCount');
763-
const total = DATA.total_runs;
822+
const total = getRunsForWorkflow(getSelectedWorkflow()).length;
823+
sel.innerHTML = '';
824+
825+
if (total === 0) {{
826+
const opt = document.createElement('option');
827+
opt.value = 0;
828+
opt.textContent = '0';
829+
sel.appendChild(opt);
830+
sel.value = 0;
831+
return;
832+
}}
833+
764834
const options = [];
765835
for (let i = 1; i <= Math.min(total, 5); i++) options.push(i);
766836
if (total > 5) options.push(5);
767837
if (total > 10) options.push(10);
768838
if (total > 20) options.push(20);
769839
if (total > 5) options.push(total);
770-
// Deduplicate and sort
840+
771841
const unique = [...new Set(options)].sort((a, b) => a - b);
772842
unique.forEach(n => {{
773843
const opt = document.createElement('option');
774844
opt.value = n;
775845
opt.textContent = n === total ? `All (${{total}})` : n;
776846
sel.appendChild(opt);
777847
}});
778-
sel.value = DATA.default_runs;
779848
780-
document.getElementById('generatedAt').textContent = 'Generated: ' + DATA.generated_at;
849+
const defaultCount = preferredCount || DATA.default_runs || unique[0];
850+
const boundedCount = Math.min(defaultCount, total);
851+
sel.value = String(boundedCount);
852+
}}
853+
854+
function onWorkflowChange() {{
855+
rebuildRunCountSelector();
781856
updateDashboard();
782857
}}
783858
784859
function getSelectedRuns() {{
785860
const n = parseInt(document.getElementById('runCount').value);
861+
const workflowRuns = getRunsForWorkflow(getSelectedWorkflow());
862+
if (!n) return [];
786863
// Runs are ordered DESC (newest first); take last N and reverse for chronological display
787-
return DATA.runs.slice(0, n).reverse();
864+
return workflowRuns.slice(0, n).reverse();
788865
}}
789866
790867
function statusBadge(status) {{
@@ -815,25 +892,29 @@ def generate_dashboard(
815892
816893
function updateDashboard() {{
817894
destroyCharts();
895+
const selectedWorkflow = getSelectedWorkflow();
896+
const selectedWorkflowMeta = DATA.workflow_meta[selectedWorkflow] || {{}};
897+
const workflowTotalRuns = getRunsForWorkflow(selectedWorkflow).length;
818898
const runs = getSelectedRuns();
819899
820900
// Header subtitle
821901
document.getElementById('headerSubtitle').textContent =
822-
`${{DATA.workflow}}${{DATA.taskfile_name ? ' ' + DATA.taskfile_name : ''}}`;
902+
`${{selectedWorkflow}}${{selectedWorkflowMeta.taskfile_name ? ' - ' + selectedWorkflowMeta.taskfile_name : ''}}`;
823903
824904
// Metadata
825905
const meta = document.getElementById('metadata');
826906
let metaHtml = '';
827-
if (DATA.taskfile_description) metaHtml += `<span>${{DATA.taskfile_description}}</span>`;
828-
if (DATA.taskfile_author) metaHtml += `<span>Author: <strong>${{DATA.taskfile_author}}</strong></span>`;
907+
if (selectedWorkflowMeta.taskfile_description) metaHtml += `<span>${{selectedWorkflowMeta.taskfile_description}}</span>`;
908+
if (selectedWorkflowMeta.taskfile_author) metaHtml += `<span>Author: <strong>${{selectedWorkflowMeta.taskfile_author}}</strong></span>`;
909+
metaHtml += `<span>Workflow runs: <strong>${{workflowTotalRuns}}</strong></span>`;
829910
metaHtml += `<span>Total runs in DB: <strong>${{DATA.total_runs}}</strong></span>`;
830911
if (runs.length > 0) {{
831912
metaHtml += `<span>Date range: <strong>${{formatDate(runs[0].start_time)}} — ${{formatDate(runs[runs.length-1].start_time)}}</strong></span>`;
832913
}}
833914
meta.innerHTML = metaHtml;
834915
835916
// Summary cards
836-
updateSummaryCards(runs);
917+
updateSummaryCards(runs, workflowTotalRuns);
837918
838919
// Charts
839920
renderRunDurationChart(runs);
@@ -850,7 +931,7 @@ def generate_dashboard(
850931
updateConfigDetails(runs);
851932
}}
852933
853-
function updateSummaryCards(runs) {{
934+
function updateSummaryCards(runs, workflowTotalRuns) {{
854935
const cards = document.getElementById('summaryCards');
855936
if (runs.length === 0) {{ cards.innerHTML = '<div class="no-data">No runs available</div>'; return; }}
856937
@@ -866,7 +947,7 @@ def generate_dashboard(
866947
<div class="card">
867948
<div class="card-label">Runs Shown</div>
868949
<div class="card-value">${{runs.length}}</div>
869-
<div class="card-sub">of ${{DATA.total_runs}} total</div>
950+
<div class="card-sub">of ${{workflowTotalRuns}} in workflow</div>
870951
</div>
871952
<div class="card">
872953
<div class="card-label">Tasks per Run</div>

src/rushti/db_admin.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -859,13 +859,16 @@ def show_task_history(
859859
def get_visualization_data(
860860
workflow: str,
861861
backend: Any = DEFAULT_DB_PATH,
862+
include_all_workflows: bool = False,
862863
) -> Dict[str, Any]:
863864
"""Get all data needed for the HTML dashboard visualization.
864865
865866
Returns all runs and their task results for a workflow.
866867
867868
:param workflow: Workflow name to query
868869
:param backend: Stats backend (StatsDatabase or DynamoDBStatsDatabase), or SQLite db path
870+
:param include_all_workflows: When True, return runs/tasks for all workflows
871+
but keep workflow as the initial/default selection.
869872
:return: Dictionary with 'runs' and 'task_results' lists, or error info
870873
"""
871874
if isinstance(backend, str):
@@ -877,18 +880,28 @@ def get_visualization_data(
877880
conn.row_factory = sqlite3.Row
878881
cursor = conn.cursor()
879882

880-
cursor.execute(
881-
"""
882-
SELECT run_id, workflow, taskfile_path, start_time, end_time,
883-
duration_seconds, status, task_count, success_count, failure_count,
884-
taskfile_name, taskfile_description, taskfile_author,
885-
max_workers, retries, result_file, exclusive, optimize
886-
FROM runs
887-
WHERE workflow = ?
888-
ORDER BY start_time DESC
889-
""",
890-
(workflow,),
891-
)
883+
if include_all_workflows:
884+
cursor.execute("""
885+
SELECT run_id, workflow, taskfile_path, start_time, end_time,
886+
duration_seconds, status, task_count, success_count, failure_count,
887+
taskfile_name, taskfile_description, taskfile_author,
888+
max_workers, retries, result_file, exclusive, optimize
889+
FROM runs
890+
ORDER BY start_time DESC
891+
""")
892+
else:
893+
cursor.execute(
894+
"""
895+
SELECT run_id, workflow, taskfile_path, start_time, end_time,
896+
duration_seconds, status, task_count, success_count, failure_count,
897+
taskfile_name, taskfile_description, taskfile_author,
898+
max_workers, retries, result_file, exclusive, optimize
899+
FROM runs
900+
WHERE workflow = ?
901+
ORDER BY start_time DESC
902+
""",
903+
(workflow,),
904+
)
892905
runs_rows = cursor.fetchall()
893906

894907
if not runs_rows:
@@ -900,13 +913,23 @@ def get_visualization_data(
900913

901914
runs = []
902915
run_ids = []
916+
has_selected_workflow = False
903917
for row in runs_rows:
904918
run = dict(row)
905919
for field in ["exclusive", "optimize"]:
906920
if field in run and run[field] is not None:
907921
run[field] = bool(run[field])
908922
runs.append(run)
909923
run_ids.append(run["run_id"])
924+
if run.get("workflow") == workflow:
925+
has_selected_workflow = True
926+
927+
if include_all_workflows and workflow and not has_selected_workflow:
928+
conn.close()
929+
return {
930+
"exists": False,
931+
"message": f"No runs found for workflow: {workflow}",
932+
}
910933

911934
placeholders = ",".join("?" * len(run_ids))
912935
cursor.execute(
@@ -932,7 +955,10 @@ def get_visualization_data(
932955
"task_results": task_results,
933956
}
934957

935-
runs_summary = backend.get_runs_for_workflow(workflow) or []
958+
if include_all_workflows:
959+
runs_summary = backend.get_all_runs() or []
960+
else:
961+
runs_summary = backend.get_runs_for_workflow(workflow) or []
936962
if not runs_summary:
937963
return {
938964
"exists": False,
@@ -941,16 +967,20 @@ def get_visualization_data(
941967

942968
runs: List[Dict[str, Any]] = []
943969
task_results: List[Dict[str, Any]] = []
970+
has_selected_workflow = False
944971

945972
for summary in runs_summary:
946973
run_id = summary.get("run_id")
947974
if not run_id:
948975
continue
949976

950977
run_info = backend.get_run_info(run_id) or {}
978+
run_workflow = run_info.get("workflow") or summary.get("workflow") or workflow
979+
if run_workflow == workflow:
980+
has_selected_workflow = True
951981
run = {
952982
"run_id": run_id,
953-
"workflow": run_info.get("workflow") or summary.get("workflow") or workflow,
983+
"workflow": run_workflow,
954984
"taskfile_path": run_info.get("taskfile_path"),
955985
"start_time": run_info.get("start_time") or summary.get("start_time"),
956986
"end_time": run_info.get("end_time") or summary.get("end_time"),
@@ -1020,6 +1050,12 @@ def get_visualization_data(
10201050
"message": f"No runs found for workflow: {workflow}",
10211051
}
10221052

1053+
if include_all_workflows and workflow and not has_selected_workflow:
1054+
return {
1055+
"exists": False,
1056+
"message": f"No runs found for workflow: {workflow}",
1057+
}
1058+
10231059
return {
10241060
"exists": True,
10251061
"workflow": workflow,

0 commit comments

Comments
 (0)