Skip to content

Commit 7d1c0b9

Browse files
abidlabscursoragentgradio-pr-botclaude
authored
Fix dashboard UX issues: smoothing in share URL, run selection, and run filtering (#527)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 454741f commit 7d1c0b9

9 files changed

Lines changed: 94 additions & 29 deletions

File tree

.changeset/better-towns-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trackio": patch
3+
---
4+
5+
feat:Fix dashboard UX issues: smoothing in share URL, run selection, and run filtering

examples/fake-training.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def generate_grad_norm_curve(epoch, max_epochs):
4545
return max(0.1, base_value + noise)
4646

4747

48-
for run in range(3):
48+
for run in range(5):
4949
wandb.init(
5050
project=f"fake-training-{PROJECT_ID}",
5151
name=f"test-run-{run}",

tests/e2e-local/test_bulk_logging.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import time
23

34
import trackio
@@ -27,7 +28,8 @@ def test_rapid_bulk_logging(temp_dir):
2728
trackio.log({"metric": i, "value": i * 3}, step=i)
2829
time_to_run_1000_logs = time.time() - start_time
2930

30-
assert time_to_run_1000_logs < 0.2, (
31+
max_seconds = 0.5 if sys.platform.startswith("win") else 0.2
32+
assert time_to_run_1000_logs < max_seconds, (
3133
f"1000 calls of trackio.log() took {time_to_run_1000_logs} seconds, which is too long"
3234
)
3335
trackio.finish()

trackio/frontend/package-lock.json

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

trackio/frontend/src/App.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,13 @@
147147
148148
if (JSON.stringify(runs) !== JSON.stringify(newRuns)) {
149149
const prevSelected = selectedRuns;
150+
const prevOrdered = runs.map(runKey);
150151
runs = newRuns;
151-
selectedRuns = reconcileSelectedRuns(prevSelected, newRuns.map(runKey));
152+
selectedRuns = reconcileSelectedRuns(
153+
prevSelected,
154+
newRuns.map(runKey),
155+
prevOrdered,
156+
);
152157
}
153158
} catch (e) {
154159
console.error("Failed to load runs:", e);
@@ -458,6 +463,7 @@
458463
<Runs
459464
project={selectedProject}
460465
{runs}
466+
{filterText}
461467
onRunsChanged={refreshRunsAndMutation}
462468
runMutationAllowed={mutationStatus.allowed}
463469
/>

trackio/frontend/src/components/Sidebar.svelte

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import GradioTextbox from "./GradioTextbox.svelte";
88
import { buildColorMap, getColorForIndex } from "../lib/stores.js";
99
import { latestOnlySelection } from "../lib/selection.js";
10+
import { filterMetricsByRegex } from "../lib/dataProcessing.js";
1011
1112
let {
1213
open = $bindable(true),
@@ -78,11 +79,11 @@
7879
};
7980
}
8081
81-
let filteredRuns = $derived(
82-
filterText
83-
? runs.filter((r) => r.name.toLowerCase().includes(filterText.toLowerCase()))
84-
: runs,
85-
);
82+
let filteredRuns = $derived.by(() => {
83+
if (!filterText || !filterText.trim()) return runs;
84+
const matches = new Set(filterMetricsByRegex(runs.map((r) => r.name), filterText));
85+
return runs.filter((r) => matches.has(r.name));
86+
});
8687
8788
let runColorMap = $derived(buildColorMap(runs));
8889
let filteredRunIds = $derived(filteredRuns.map((r) => r.id ?? r.name));
@@ -158,6 +159,9 @@
158159
if (runIds.length) {
159160
params.set("run_ids", runIds.join(","));
160161
}
162+
if (smoothing != null && smoothing !== 10) {
163+
params.set("smoothing", smoothing.toString());
164+
}
161165
if (!showHeaders) {
162166
params.set("accordion", "hidden");
163167
}
@@ -237,6 +241,17 @@
237241
{/if}
238242
</div>
239243
244+
{#if variant === "compact" && currentPage === "runs"}
245+
<div class="section">
246+
<GradioTextbox
247+
label="Run Filter"
248+
info="Filter runs using regex patterns. Leave empty to show all runs."
249+
placeholder="e.g., baseline|exp-.*|v2"
250+
bind:value={filterText}
251+
/>
252+
</div>
253+
{/if}
254+
240255
{#if variant === "full"}
241256
{#if currentPage === "metrics" && spacesMode}
242257
<div class="section">

trackio/frontend/src/lib/selection.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ export function latestOnlySelection(filteredRunIds) {
33
return [filteredRunIds[0]];
44
}
55

6-
export function reconcileSelectedRuns(prevSelected, newOrderedIds) {
6+
export function reconcileSelectedRuns(prevSelected, newOrderedIds, prevOrderedIds) {
77
const prev = prevSelected ?? [];
88
const ordered = newOrderedIds ?? [];
9+
const prevOrdered = prevOrderedIds ?? [];
910
const newIdSet = new Set(ordered);
1011
const kept = prev.filter((r) => newIdSet.has(r));
1112

12-
if (kept.length === 0 && prev.length === 0) {
13+
if (prev.length === 0 || kept.length === 0) {
1314
return [...ordered];
1415
}
1516

16-
const prevSet = new Set(prev);
17-
const additions = ordered.filter((r) => !prevSet.has(r));
18-
return [...kept, ...additions];
17+
const allPrevSelected =
18+
prevOrdered.length > 0 && prev.length === prevOrdered.length;
19+
if (allPrevSelected) {
20+
const keptSet = new Set(kept);
21+
const additions = ordered.filter((r) => !keptSet.has(r));
22+
return [...kept, ...additions];
23+
}
24+
25+
return kept;
1926
}

trackio/frontend/src/lib/selection.test.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,37 @@ describe("reconcileSelectedRuns", () => {
2222
expect(reconcileSelectedRuns([], ["a", "b", "c"])).toEqual(["a", "b", "c"]);
2323
});
2424

25-
test("keeps the previously selected run and appends new runs as selected", () => {
26-
expect(reconcileSelectedRuns(["a"], ["a", "b", "c"])).toEqual(["a", "b", "c"]);
25+
test("keeps a partial selection without auto-selecting new runs", () => {
26+
expect(reconcileSelectedRuns(["a"], ["a", "b", "c"], ["a", "b"])).toEqual(["a"]);
2727
});
2828

29-
test("preserves the chosen run when the run list is unchanged on refresh", () => {
29+
test("auto-selects new runs when all previous runs were selected", () => {
30+
expect(
31+
reconcileSelectedRuns(["a", "b"], ["a", "b", "c"], ["a", "b"]),
32+
).toEqual(["a", "b", "c"]);
33+
});
34+
35+
test("preserves the chosen runs when the run list is unchanged on refresh", () => {
3036
const prev = ["b"];
3137
const next = ["a", "b", "c"];
32-
expect(reconcileSelectedRuns(prev, next)).toEqual(["b", "a", "c"]);
33-
expect(reconcileSelectedRuns(["b", "a", "c"], next)).toEqual(["b", "a", "c"]);
38+
expect(reconcileSelectedRuns(prev, next, next)).toEqual(["b"]);
39+
expect(reconcileSelectedRuns(["b", "a", "c"], next, next)).toEqual([
40+
"b",
41+
"a",
42+
"c",
43+
]);
3444
});
3545

3646
test("drops runs that no longer exist on the server", () => {
37-
expect(reconcileSelectedRuns(["a", "b", "c"], ["a", "c"])).toEqual(["a", "c"]);
47+
expect(
48+
reconcileSelectedRuns(["a", "b", "c"], ["a", "c"], ["a", "b", "c"]),
49+
).toEqual(["a", "c"]);
50+
});
51+
52+
test("falls back to all runs when none of the previously selected runs exist anymore", () => {
53+
expect(reconcileSelectedRuns(["a"], ["b"], ["a"])).toEqual(["b"]);
54+
expect(
55+
reconcileSelectedRuns(["x", "y"], ["a", "b", "c"], ["x", "y"]),
56+
).toEqual(["a", "b", "c"]);
3857
});
3958
});

trackio/frontend/src/pages/Runs.svelte

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import { getProjectSummary, getRunSummary, deleteRun, renameRun } from "../lib/api.js";
55
import { navigateTo, setQueryParam } from "../lib/router.js";
66
import { buildColorMap } from "../lib/stores.js";
7+
import { filterMetricsByRegex } from "../lib/dataProcessing.js";
78
89
let {
910
project = null,
1011
runs = [],
12+
filterText = "",
1113
onRunsChanged = null,
1214
runMutationAllowed = true,
1315
} = $props();
@@ -22,6 +24,12 @@
2224
let renameValue = $state("");
2325
let renameInput = $state(null);
2426
27+
let filteredRuns = $derived.by(() => {
28+
if (!filterText || !filterText.trim()) return runsData;
29+
const matches = new Set(filterMetricsByRegex(runsData.map((r) => r.name), filterText));
30+
return runsData.filter((r) => matches.has(r.name));
31+
});
32+
2533
async function loadRuns() {
2634
if (!project) {
2735
runsData = [];
@@ -110,6 +118,11 @@
110118
<p>Refresh this page or wait for the dashboard to poll; new runs appear in the table with step counts.</p>
111119
</div>
112120
{:else}
121+
{#if filterText}
122+
<div class="filter-count-row">
123+
<span class="filter-count">{filteredRuns.length} of {runsData.length} runs</span>
124+
</div>
125+
{/if}
113126
<table class="runs-table">
114127
<thead>
115128
<tr>
@@ -120,7 +133,7 @@
120133
</tr>
121134
</thead>
122135
<tbody>
123-
{#each runsData as run, i}
136+
{#each filteredRuns as run, i}
124137
<tr>
125138
<td class="actions-cell">
126139
<div class="actions-wrap">
@@ -219,6 +232,13 @@
219232
background: none;
220233
padding: 0;
221234
}
235+
.filter-count-row {
236+
margin-bottom: 12px;
237+
}
238+
.filter-count {
239+
font-size: var(--text-sm, 12px);
240+
color: var(--body-text-color-subdued, #6b7280);
241+
}
222242
.runs-table {
223243
width: 100%;
224244
border-collapse: collapse;

0 commit comments

Comments
 (0)