Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-plants-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trackio": minor
---

feat:Add a settings/CLI page to Trackio
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dist/
build/
tf_test_run/
test.py
screenshots/
40 changes: 40 additions & 0 deletions tests/ui/test_settings_theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from playwright.sync_api import expect, sync_playwright

import trackio


def test_settings_theme_switching_and_persistence(temp_dir):
trackio.init(project="test_theme", name="theme_run")
trackio.log(metrics={"loss": 0.5})
trackio.finish()

app, url, _, _ = trackio.show(block_thread=False, open_browser=False)

try:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_default_timeout(5000)
base_url = url if url.endswith("/") else url + "/"
page.goto(base_url)
page.wait_for_load_state("networkidle")

page.get_by_role("button", name="Settings", exact=True).click()
page.wait_for_load_state("networkidle")
expect(page.locator(".settings-page")).to_be_visible()

page.get_by_role("button", name="Dark", exact=True).click()
assert page.locator("html").get_attribute("data-theme") == "dark"

page.get_by_role("button", name="Light", exact=True).click()
assert page.locator("html").get_attribute("data-theme") is None

page.get_by_role("button", name="Dark", exact=True).click()
page.goto(base_url)
page.wait_for_load_state("networkidle")
assert page.locator("html").get_attribute("data-theme") == "dark"

browser.close()
finally:
trackio.delete_project("test_theme", force=True)
app.close()
6 changes: 3 additions & 3 deletions tests/ui/test_ui_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_runs_plots_images_are_displayed(temp_dir):
page.goto(url if url.endswith("/") else url + "/")
page.wait_for_load_state("networkidle")
nav_links = page.locator(".nav-link")
expect(nav_links).to_have_count(6)
expect(nav_links).to_have_count(7)

run_label = page.locator(".run-name", has_text="test_run")
expect(run_label).to_be_visible()
Expand Down Expand Up @@ -106,7 +106,7 @@ def test_navbar_page_navigation(temp_dir):
page.goto(url if url.endswith("/") else url + "/")
page.wait_for_load_state("networkidle")
nav_links = page.locator(".nav-link")
expect(nav_links).to_have_count(6)
expect(nav_links).to_have_count(7)

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

Expand Down Expand Up @@ -145,7 +145,7 @@ def test_runs_table_shows_run_data(temp_dir):
page.wait_for_load_state("networkidle")

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

Expand Down
13 changes: 11 additions & 2 deletions trackio/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@
} from "./lib/api.js";
import { setColorPalette } from "./lib/stores.js";
import { getPageFromPath, navigateTo, getQueryParam } from "./lib/router.js";
import { applyTheme, detectSystemTheme } from "./lib/theme.js";
import Settings from "./pages/Settings.svelte";
import { initTheme, isDark, onThemeChange } from "./lib/theme.js";

applyTheme(getQueryParam("__theme") || detectSystemTheme());
initTheme();

let darkMode = $state(isDark());
onThemeChange((dark) => { darkMode = dark; });

let currentPage = $state("metrics");
let projects = $state([]);
Expand Down Expand Up @@ -56,6 +60,7 @@
let plotOrder = $state([]);
let tableTruncateLength = $state(250);
let readOnlySource = $state(null);
let spaceId = $state(null);

function handleNavigate(page) {
currentPage = page;
Expand Down Expand Up @@ -256,6 +261,7 @@
if (settings.plot_order) plotOrder = settings.plot_order;
if (settings.table_truncate_length) tableTruncateLength = settings.table_truncate_length;
if (settings.media_dir) setMediaDir(settings.media_dir);
if (settings.space_id) spaceId = settings.space_id;
}
} catch {
// settings endpoint may not be available
Expand Down Expand Up @@ -333,6 +339,7 @@
bind:filterText
{metricColumns}
{logoUrls}
{darkMode}
/>
{/if}

Expand Down Expand Up @@ -377,6 +384,8 @@
<RunDetail project={selectedProject} />
{:else if currentPage === "files"}
<Files project={selectedProject} />
{:else if currentPage === "settings"}
<Settings {spaceId} selectedProject={selectedProject} {projects} />
{/if}
</div>
</div>
Expand Down
26 changes: 24 additions & 2 deletions trackio/frontend/src/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
{link.label}
</button>
{/each}
<button
class="nav-link settings-btn"
class:active={currentPage === "settings"}
onclick={() => handleClick("settings")}
title="CLI & Settings"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span>Settings</span>
</button>
</div>
</nav>

Expand All @@ -41,12 +53,14 @@
min-height: 44px;
}
.nav-spacer {
flex: 1;
flex: 1 1 0;
min-width: 0;
}
.nav-tabs {
display: flex;
gap: 0;
padding-right: 16px;
flex-shrink: 0;
padding-right: 8px;
}
.nav-link {
padding: 10px 16px;
Expand All @@ -68,4 +82,12 @@
border-bottom-color: var(--body-text-color, #1f2937);
font-weight: 500;
}
.settings-btn {
display: flex;
align-items: center;
gap: 6px;
}
.settings-btn svg {
flex-shrink: 0;
}
</style>
4 changes: 2 additions & 2 deletions trackio/frontend/src/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import GradioSlider from "./GradioSlider.svelte";
import GradioTextbox from "./GradioTextbox.svelte";
import { buildColorMap, getColorForIndex } from "../lib/stores.js";
import { isDark } from "../lib/theme.js";

let {
open = $bindable(true),
Expand All @@ -31,6 +30,7 @@
readOnlySource = null,
projectLocked = false,
logoUrls = { light: "/static/trackio/trackio_logo_type_light_transparent.png", dark: "/static/trackio/trackio_logo_type_dark_transparent.png" },
darkMode = false,
} = $props();

let navTick = $state(0);
Expand Down Expand Up @@ -105,7 +105,7 @@
<div class="sidebar-scroll">
<div class="logo-section">
<img
src={isDark() ? logoUrls.dark : logoUrls.light}
src={darkMode ? logoUrls.dark : logoUrls.light}
alt="Trackio"
class="logo"
/>
Expand Down
3 changes: 3 additions & 0 deletions trackio/frontend/src/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export function getPageFromPath() {
return "run-detail";
case "files":
return "files";
case "settings":
return "settings";
default:
return "metrics";
}
Expand All @@ -33,6 +35,7 @@ export function navigateTo(page) {
runs: "/runs",
"run-detail": "/run",
files: "/files",
settings: "/settings",
};
const path = pathMap[page] || "/";
const search = params.toString();
Expand Down
67 changes: 67 additions & 0 deletions trackio/frontend/src/lib/theme.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
const THEME_KEY = "trackio_theme_preference";

let _listeners = [];

export function onThemeChange(fn) {
_listeners.push(fn);
return () => {
_listeners = _listeners.filter((f) => f !== fn);
};
}

function _notify() {
const dark = isDark();
_listeners.forEach((fn) => fn(dark));
}

const darkOverrides = {
"--neutral-50": "#fafafa",
"--neutral-100": "#f4f4f5",
Expand Down Expand Up @@ -65,6 +81,7 @@ export function applyTheme(themeName) {
root.style.removeProperty(key);
});
}
_notify();
}

export function isDark() {
Expand All @@ -80,3 +97,53 @@ export function detectSystemTheme() {
}
return "default";
}

export function getThemePreference() {
return localStorage.getItem(THEME_KEY) || "system";
}

export function setThemePreference(pref) {
localStorage.setItem(THEME_KEY, pref);
applyThemeFromPreference(pref);
_ensureSystemListener(pref === "system");
}

export function applyThemeFromPreference(pref) {
if (pref === "system") {
applyTheme(detectSystemTheme());
} else if (pref === "dark") {
applyTheme("dark");
} else {
applyTheme("default");
}
}

let _systemListenerAttached = false;

function _onSystemThemeChange() {
if (getThemePreference() === "system") {
applyTheme(detectSystemTheme());
}
}

function _ensureSystemListener(needed) {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
if (needed && !_systemListenerAttached) {
mq.addEventListener("change", _onSystemThemeChange);
_systemListenerAttached = true;
} else if (!needed && _systemListenerAttached) {
mq.removeEventListener("change", _onSystemThemeChange);
_systemListenerAttached = false;
}
}

export function initTheme() {
const urlTheme = new URLSearchParams(window.location.search).get("__theme");
if (urlTheme) {
applyTheme(urlTheme);
return;
}
const pref = getThemePreference();
applyThemeFromPreference(pref);
_ensureSystemListener(pref === "system");
}
Loading
Loading