Subdue empty dashboard tabs#538
Conversation
🪼 branch checks and previews
|
🦄 change detectedThis Pull Request includes changes to the following packages.
|
🪼 branch checks and previews
Install Trackio from this PR (includes built frontend) pip install "https://huggingface.co/buckets/trackio/trackio-wheels/resolve/243d14650ee54dd58eab446d9c066f052ddc520d/trackio-0.25.1-py3-none-any.whl" |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
# Conflicts: # trackio/frontend/src/App.svelte
There was a problem hiding this comment.
Pull request overview
This PR adds “tab availability” detection to the dashboard so optional tabs (e.g., Traces, System, Media, Reports, Files) are visually subdued when there’s no data for the current project/run selection, and the app can auto-navigate away from the bare dashboard route to the first non-empty tab.
Changes:
- Compute and periodically refresh per-tab availability based on selected project/runs and available data sources (logs, traces, system metrics, alerts, files).
- Auto-open the first available tab when landing on the bare dashboard route.
- Update the navbar UI to visually subdue optional tabs detected as empty, and expose an explanatory tooltip.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
trackio/frontend/src/components/Navbar.svelte |
Adds empty-tab styling/tooltip driven by tabAvailability and optionalEmptyTabs. |
trackio/frontend/src/App.svelte |
Implements tab availability computation, auto-open behavior, and polling refresh; passes state into Navbar. |
.changeset/poor-windows-fold.md |
Adds a changeset entry for the feature. |
Comments suppressed due to low confidence (2)
trackio/frontend/src/App.svelte:294
anyRunHasTracesmakes one/get_tracescall per selected run concurrently. For projects with many runs this becomes an N-request fan-out just to compute tab availability, and can overload both browser and backend. Consider switching to a short-circuiting approach (stop after first hit), adding a concurrency limit, and/or using a cheaper aggregate endpoint (or sampling a limited number of runs).
async function anyRunHasTraces(project, runRecords) {
const results = await Promise.all(
runRecords.map(async (run) => {
try {
const traces = await getTraces(project, run, { limit: 1 });
return (traces || []).length > 0;
} catch {
return false;
}
}),
);
return results.some(Boolean);
}
trackio/frontend/src/App.svelte:330
refreshTabAvailabilitycallsgetLogsBatch(selectedProject, runRecords)for all selected runs to detect whether metrics/media/reports exist. Since the default selection is typically "all runs", this can fetch a very large payload and then iterate every log entry just to set boolean flags, and it can run repeatedly via polling/effects. Consider limiting the number of runs/logs inspected (sample/cap), batching likeMetrics.sveltedoes, and/or adding a dedicated lightweight API that returns presence/summary booleans instead of full logs.
] = await Promise.all([
runRecords.length ? getLogsBatch(selectedProject, runRecords) : [],
getAlerts(selectedProject, null, null, null).catch(() => []),
runRecords.length ? anyRunHasSystemMetrics(selectedProject, runRecords) : false,
runRecords.length ? anyRunHasTraces(selectedProject, runRecords) : false,
getProjectFiles(selectedProject).catch(() => []),
]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function anyRunHasSystemMetrics(project, runRecords) { | ||
| const results = await Promise.all( | ||
| runRecords.map(async (run) => { | ||
| try { | ||
| const metrics = await getSystemMetricsForRun(project, run); | ||
| return (metrics || []).length > 0; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }), | ||
| ); | ||
| return results.some(Boolean); | ||
| } | ||
|
|
||
| async function anyRunHasTraces(project, runRecords) { | ||
| const results = await Promise.all( | ||
| runRecords.map(async (run) => { | ||
| try { | ||
| const traces = await getTraces(project, run, { limit: 1 }); | ||
| return (traces || []).length > 0; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }), | ||
| ); | ||
| return results.some(Boolean); |
znation
left a comment
There was a problem hiding this comment.
Other than agreeing with Copilot's suggestion, LGTM.
| with SQLiteStorage._get_connection(db_path) as conn: | ||
| flags["metrics"] = _exists( | ||
| conn, | ||
| "SELECT 1 FROM metrics " | ||
| "WHERE metrics GLOB '*:[0-9]*' OR metrics GLOB '*:-[0-9]*' " | ||
| "LIMIT 1", | ||
| ) |
There was a problem hiding this comment.
Thanks — considered this but keeping the current heuristic. The narrow case it can false-positive on is a project that only logs typed values (histogram/table) where those typed values happen to contain numeric metadata adjacent to a : (e.g. "bins":50). For that to mislead the user, the project must additionally never log any scalar metric in any run — quite rare in practice, and the worst-case outcome is "Metrics tab is highlighted but the page is empty when clicked," not a correctness bug.
The alternative (deserialize each row's JSON with orjson.loads and inspect top-level keys) brings back per-row JSON parsing in Python — exactly the cost this endpoint was introduced to avoid (the original frontend code shipped 3000 logs × N runs over the wire to do this). A bounded-sample variant would either miss data or still parse N rows.
A stricter SQL-only heuristic — "row has a digit and no _type marker" — is appealing but regresses the more common case of users logging scalars + media in the same log() call (e.g. {"loss": 0.5, "img": Image()}): that row contains _type and would be excluded, hiding the Metrics tab from users who clearly have metrics. False negatives on a common path are worse than false positives on a rare path.
| for (const row of metricsRows) { | ||
| if (!metrics && rowHasScalarMetric(row)) metrics = true; | ||
| if (!media && rowHasTypedValue(row, MEDIA_TYPES)) media = true; | ||
| if (!reports && rowHasTypedValue(row, new Set(["trackio.markdown"]))) reports = true; | ||
| if (metrics && media && reports) break; |
| "trackio": patch | ||
| --- | ||
|
|
||
| feat:Subdue empty dashboard tabs |
Python sqlite3 stores orjson bytes as BLOB; GLOB pattern matching against BLOB-typed values is unreliable on older SQLite builds (reproduced on Ubuntu CI Python 3.10.20). Force TEXT comparison. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hoist MARKDOWN_TYPES to module-level constant in staticApi.js to avoid per-row Set allocation in tab-availability scan. - Add missing space in changeset entry: "feat:Subdue" -> "feat: Subdue". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks @znation! Addressed copilot's comments, will merge this in |
Now that we have quite a bit of tabs open, and many applications might only need a subset of tabs (e.g. Traces only, or no Traces, metrics only), this PR visually subdues optional tabs when their content is empty
Specifically, it: