Skip to content

Commit e8a897d

Browse files
Add a settings/CLI page to Trackio (#494)
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
1 parent ba5d3fb commit e8a897d

12 files changed

Lines changed: 670 additions & 9 deletions

File tree

.changeset/dirty-plants-end.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:Add a settings/CLI page to Trackio

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ dist/
1313
build/
1414
tf_test_run/
1515
test.py
16+
screenshots/

tests/ui/test_settings_theme.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from playwright.sync_api import expect, sync_playwright
2+
3+
import trackio
4+
5+
6+
def test_settings_theme_switching_and_persistence(temp_dir):
7+
trackio.init(project="test_theme", name="theme_run")
8+
trackio.log(metrics={"loss": 0.5})
9+
trackio.finish()
10+
11+
app, url, _, _ = trackio.show(block_thread=False, open_browser=False)
12+
13+
try:
14+
with sync_playwright() as p:
15+
browser = p.chromium.launch()
16+
page = browser.new_page()
17+
page.set_default_timeout(5000)
18+
base_url = url if url.endswith("/") else url + "/"
19+
page.goto(base_url)
20+
page.wait_for_load_state("networkidle")
21+
22+
page.get_by_role("button", name="Settings", exact=True).click()
23+
page.wait_for_load_state("networkidle")
24+
expect(page.locator(".settings-page")).to_be_visible()
25+
26+
page.get_by_role("button", name="Dark", exact=True).click()
27+
assert page.locator("html").get_attribute("data-theme") == "dark"
28+
29+
page.get_by_role("button", name="Light", exact=True).click()
30+
assert page.locator("html").get_attribute("data-theme") is None
31+
32+
page.get_by_role("button", name="Dark", exact=True).click()
33+
page.goto(base_url)
34+
page.wait_for_load_state("networkidle")
35+
assert page.locator("html").get_attribute("data-theme") == "dark"
36+
37+
browser.close()
38+
finally:
39+
trackio.delete_project("test_theme", force=True)
40+
app.close()

tests/ui/test_ui_display.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_runs_plots_images_are_displayed(temp_dir):
2626
page.goto(url if url.endswith("/") else url + "/")
2727
page.wait_for_load_state("networkidle")
2828
nav_links = page.locator(".nav-link")
29-
expect(nav_links).to_have_count(6)
29+
expect(nav_links).to_have_count(7)
3030

3131
run_label = page.locator(".run-name", has_text="test_run")
3232
expect(run_label).to_be_visible()
@@ -106,7 +106,7 @@ def test_navbar_page_navigation(temp_dir):
106106
page.goto(url if url.endswith("/") else url + "/")
107107
page.wait_for_load_state("networkidle")
108108
nav_links = page.locator(".nav-link")
109-
expect(nav_links).to_have_count(6)
109+
expect(nav_links).to_have_count(7)
110110

111111
expect(page.locator(".metrics-page")).to_be_visible()
112112

@@ -145,7 +145,7 @@ def test_runs_table_shows_run_data(temp_dir):
145145
page.wait_for_load_state("networkidle")
146146

147147
nav_links = page.locator(".nav-link")
148-
expect(nav_links).to_have_count(6)
148+
expect(nav_links).to_have_count(7)
149149
page.get_by_role("button", name="Runs", exact=True).click()
150150
page.wait_for_load_state("networkidle")
151151

trackio/frontend/src/App.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
} from "./lib/api.js";
2323
import { setColorPalette } from "./lib/stores.js";
2424
import { getPageFromPath, navigateTo, getQueryParam } from "./lib/router.js";
25-
import { applyTheme, detectSystemTheme } from "./lib/theme.js";
25+
import Settings from "./pages/Settings.svelte";
26+
import { initTheme, isDark, onThemeChange } from "./lib/theme.js";
2627

27-
applyTheme(getQueryParam("__theme") || detectSystemTheme());
28+
initTheme();
29+
30+
let darkMode = $state(isDark());
31+
onThemeChange((dark) => { darkMode = dark; });
2832

2933
let currentPage = $state("metrics");
3034
let projects = $state([]);
@@ -56,6 +60,7 @@
5660
let plotOrder = $state([]);
5761
let tableTruncateLength = $state(250);
5862
let readOnlySource = $state(null);
63+
let spaceId = $state(null);
5964

6065
function handleNavigate(page) {
6166
currentPage = page;
@@ -256,6 +261,7 @@
256261
if (settings.plot_order) plotOrder = settings.plot_order;
257262
if (settings.table_truncate_length) tableTruncateLength = settings.table_truncate_length;
258263
if (settings.media_dir) setMediaDir(settings.media_dir);
264+
if (settings.space_id) spaceId = settings.space_id;
259265
}
260266
} catch {
261267
// settings endpoint may not be available
@@ -333,6 +339,7 @@
333339
bind:filterText
334340
{metricColumns}
335341
{logoUrls}
342+
{darkMode}
336343
/>
337344
{/if}
338345

@@ -377,6 +384,8 @@
377384
<RunDetail project={selectedProject} />
378385
{:else if currentPage === "files"}
379386
<Files project={selectedProject} />
387+
{:else if currentPage === "settings"}
388+
<Settings {spaceId} selectedProject={selectedProject} {projects} />
380389
{/if}
381390
</div>
382391
</div>

trackio/frontend/src/components/Navbar.svelte

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
{link.label}
2828
</button>
2929
{/each}
30+
<button
31+
class="nav-link settings-btn"
32+
class:active={currentPage === "settings"}
33+
onclick={() => handleClick("settings")}
34+
title="CLI & Settings"
35+
>
36+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
37+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
38+
<circle cx="12" cy="12" r="3"/>
39+
</svg>
40+
<span>Settings</span>
41+
</button>
3042
</div>
3143
</nav>
3244

@@ -41,12 +53,14 @@
4153
min-height: 44px;
4254
}
4355
.nav-spacer {
44-
flex: 1;
56+
flex: 1 1 0;
57+
min-width: 0;
4558
}
4659
.nav-tabs {
4760
display: flex;
4861
gap: 0;
49-
padding-right: 16px;
62+
flex-shrink: 0;
63+
padding-right: 8px;
5064
}
5165
.nav-link {
5266
padding: 10px 16px;
@@ -68,4 +82,12 @@
6882
border-bottom-color: var(--body-text-color, #1f2937);
6983
font-weight: 500;
7084
}
85+
.settings-btn {
86+
display: flex;
87+
align-items: center;
88+
gap: 6px;
89+
}
90+
.settings-btn svg {
91+
flex-shrink: 0;
92+
}
7193
</style>

trackio/frontend/src/components/Sidebar.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import GradioSlider from "./GradioSlider.svelte";
77
import GradioTextbox from "./GradioTextbox.svelte";
88
import { buildColorMap, getColorForIndex } from "../lib/stores.js";
9-
import { isDark } from "../lib/theme.js";
109

1110
let {
1211
open = $bindable(true),
@@ -31,6 +30,7 @@
3130
readOnlySource = null,
3231
projectLocked = false,
3332
logoUrls = { light: "/static/trackio/trackio_logo_type_light_transparent.png", dark: "/static/trackio/trackio_logo_type_dark_transparent.png" },
33+
darkMode = false,
3434
} = $props();
3535

3636
let navTick = $state(0);
@@ -105,7 +105,7 @@
105105
<div class="sidebar-scroll">
106106
<div class="logo-section">
107107
<img
108-
src={isDark() ? logoUrls.dark : logoUrls.light}
108+
src={darkMode ? logoUrls.dark : logoUrls.light}
109109
alt="Trackio"
110110
class="logo"
111111
/>

trackio/frontend/src/lib/router.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function getPageFromPath() {
1818
return "run-detail";
1919
case "files":
2020
return "files";
21+
case "settings":
22+
return "settings";
2123
default:
2224
return "metrics";
2325
}
@@ -33,6 +35,7 @@ export function navigateTo(page) {
3335
runs: "/runs",
3436
"run-detail": "/run",
3537
files: "/files",
38+
settings: "/settings",
3639
};
3740
const path = pathMap[page] || "/";
3841
const search = params.toString();

trackio/frontend/src/lib/theme.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
const THEME_KEY = "trackio_theme_preference";
2+
3+
let _listeners = [];
4+
5+
export function onThemeChange(fn) {
6+
_listeners.push(fn);
7+
return () => {
8+
_listeners = _listeners.filter((f) => f !== fn);
9+
};
10+
}
11+
12+
function _notify() {
13+
const dark = isDark();
14+
_listeners.forEach((fn) => fn(dark));
15+
}
16+
117
const darkOverrides = {
218
"--neutral-50": "#fafafa",
319
"--neutral-100": "#f4f4f5",
@@ -65,6 +81,7 @@ export function applyTheme(themeName) {
6581
root.style.removeProperty(key);
6682
});
6783
}
84+
_notify();
6885
}
6986

7087
export function isDark() {
@@ -80,3 +97,53 @@ export function detectSystemTheme() {
8097
}
8198
return "default";
8299
}
100+
101+
export function getThemePreference() {
102+
return localStorage.getItem(THEME_KEY) || "system";
103+
}
104+
105+
export function setThemePreference(pref) {
106+
localStorage.setItem(THEME_KEY, pref);
107+
applyThemeFromPreference(pref);
108+
_ensureSystemListener(pref === "system");
109+
}
110+
111+
export function applyThemeFromPreference(pref) {
112+
if (pref === "system") {
113+
applyTheme(detectSystemTheme());
114+
} else if (pref === "dark") {
115+
applyTheme("dark");
116+
} else {
117+
applyTheme("default");
118+
}
119+
}
120+
121+
let _systemListenerAttached = false;
122+
123+
function _onSystemThemeChange() {
124+
if (getThemePreference() === "system") {
125+
applyTheme(detectSystemTheme());
126+
}
127+
}
128+
129+
function _ensureSystemListener(needed) {
130+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
131+
if (needed && !_systemListenerAttached) {
132+
mq.addEventListener("change", _onSystemThemeChange);
133+
_systemListenerAttached = true;
134+
} else if (!needed && _systemListenerAttached) {
135+
mq.removeEventListener("change", _onSystemThemeChange);
136+
_systemListenerAttached = false;
137+
}
138+
}
139+
140+
export function initTheme() {
141+
const urlTheme = new URLSearchParams(window.location.search).get("__theme");
142+
if (urlTheme) {
143+
applyTheme(urlTheme);
144+
return;
145+
}
146+
const pref = getThemePreference();
147+
applyThemeFromPreference(pref);
148+
_ensureSystemListener(pref === "system");
149+
}

0 commit comments

Comments
 (0)