Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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/poor-windows-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trackio": minor
---

feat:Subdue empty dashboard tabs
191 changes: 190 additions & 1 deletion trackio/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
getAllProjects,
getRunsForProject,
getAlerts,
getLogsBatch,
getTraces,
getSystemMetricsForRun,
getProjectFiles,
getRunMutationStatus,
getSettings,
getReadOnlySource,
Expand Down Expand Up @@ -88,6 +92,37 @@
let spaceId = $state(null);
let availableSystemDevices = $state([]);
let selectedSystemDevices = $state([]);
let tabAvailability = $state({});
let tabAvailabilityRequestId = 0;
let lastTabAvailabilityRefreshAt = 0;
let shouldOpenFirstNonEmptyTab = false;
let openedFirstNonEmptyTab = false;
const TAB_AVAILABILITY_POLL_INTERVAL_MS = 15000;

const OPTIONAL_EMPTY_TABS = new Set([
"system",
"traces",
"media",
"reports",
"files",
]);
const AUTO_OPEN_TAB_ORDER = [
"metrics",
"system",
"traces",
"media",
"reports",
"runs",
"files",
];
const RESERVED_METRIC_KEYS = new Set([
"project",
"run",
"timestamp",
"step",
"time",
"metrics",
]);

function runKey(run) {
return run?.id ?? run?.name;
Expand All @@ -98,10 +133,16 @@
);

function handleNavigate(page) {
openedFirstNonEmptyTab = true;
currentPage = page;
navigateTo(page);
}

function isBareDashboardPath() {
const pathname = window.location.pathname.replace(/\/+$/, "") || "/";
return pathname === "/";
}

function lockedProjectName() {
return getQueryParam("project") || getQueryParam("selected_project");
}
Expand Down Expand Up @@ -171,6 +212,139 @@
}
}

function initialAvailability() {
return {
metrics: false,
system: false,
traces: false,
media: false,
reports: false,
runs: false,
files: false,
};
}

function logHasScalarMetric(log) {
return Object.entries(log || {}).some(([key, value]) => (
!RESERVED_METRIC_KEYS.has(key) &&
typeof value === "number" &&
Number.isFinite(value)
));
}

function logHasMedia(log) {
return Object.values(log || {}).some((value) => (
value &&
typeof value === "object" &&
["trackio.image", "trackio.video", "trackio.audio", "trackio.table"].includes(value._type)
));
}

function logHasMarkdownReport(log) {
return Object.values(log || {}).some((value) => (
value &&
typeof value === "object" &&
value._type === "trackio.markdown"
));
}

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);
}

async function refreshTabAvailability({ force = false } = {}) {
const now = Date.now();
if (
!force &&
now - lastTabAvailabilityRefreshAt < TAB_AVAILABILITY_POLL_INTERVAL_MS
) {
return;
}
lastTabAvailabilityRefreshAt = now;
const requestId = ++tabAvailabilityRequestId;
if (!selectedProject) {
tabAvailability = initialAvailability();
return;
}

const availability = {
...initialAvailability(),
runs: runs.length > 0,
};
const runRecords = selectedRunRecords;

try {
const [
logsBatch,
projectAlerts,
hasSystem,
hasTraces,
projectFiles,
] = 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(() => []),
]);
if (requestId !== tabAvailabilityRequestId) return;

const selectedRunNames = new Set(runRecords.map((run) => run.name));
const selectedAlerts = (projectAlerts || []).filter(
(alert) => !alert.run || selectedRunNames.has(alert.run),
);
for (const entry of logsBatch || []) {
for (const log of entry.logs || []) {
availability.metrics ||= logHasScalarMetric(log);
availability.media ||= logHasMedia(log);
availability.reports ||= logHasMarkdownReport(log);
}
}
availability.system = hasSystem;
availability.traces = hasTraces;
availability.reports ||= selectedAlerts.length > 0;
availability.files = (projectFiles || []).length > 0;
tabAvailability = availability;

if (shouldOpenFirstNonEmptyTab && !openedFirstNonEmptyTab && isBareDashboardPath()) {
const first = AUTO_OPEN_TAB_ORDER.find((page) => availability[page]);
if (first && first !== currentPage) {
currentPage = first;
navigateTo(first);
}
openedFirstNonEmptyTab = true;
}
} catch (e) {
if (requestId !== tabAvailabilityRequestId) return;
console.error("Failed to load tab availability:", e);
tabAvailability = availability;
}
}

function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollTimer = setInterval(async () => {
Expand All @@ -180,6 +354,7 @@
await refreshProjects();
await refreshRuns();
await refreshAlerts();
await refreshTabAvailability();
}, getAppPollIntervalMs());
}

Expand Down Expand Up @@ -268,6 +443,7 @@
showHeaders = false;
}

shouldOpenFirstNonEmptyTab = isBareDashboardPath();
currentPage = getPageFromPath();

window.addEventListener("popstate", () => {
Expand Down Expand Up @@ -309,6 +485,7 @@
await refreshRuns();

await refreshAlerts();
await refreshTabAvailability({ force: true });
} catch (e) {
console.error("Failed to load projects:", e);
} finally {
Expand Down Expand Up @@ -339,6 +516,13 @@
if (projectLocked) applyLockedProject();
});

$effect(() => {
selectedProject;
selectedRuns;
runs;
if (appBootstrapReady) refreshTabAvailability({ force: true });
});

let urlRunsFromQueryApplied = $state(false);

$effect(() => {
Expand Down Expand Up @@ -421,7 +605,12 @@

<div class="main">
{#if !navbarHidden}
<Navbar {currentPage} onNavigate={handleNavigate} />
<Navbar
{currentPage}
{tabAvailability}
optionalEmptyTabs={OPTIONAL_EMPTY_TABS}
onNavigate={handleNavigate}
/>
{/if}

<div class="page-content">
Expand Down
18 changes: 17 additions & 1 deletion trackio/frontend/src/components/Navbar.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<script>
let { currentPage = "metrics", onNavigate } = $props();
let {
currentPage = "metrics",
tabAvailability = {},
optionalEmptyTabs = new Set(),
onNavigate,
} = $props();

const links = [
{ id: "metrics", label: "Metrics" },
Expand All @@ -14,6 +19,10 @@
function handleClick(id) {
onNavigate?.(id);
}

function isOptionalEmpty(id) {
return optionalEmptyTabs.has(id) && tabAvailability[id] === false;
}
</script>

<nav class="navbar">
Expand All @@ -23,7 +32,9 @@
<button
class="nav-link"
class:active={currentPage === link.id}
class:empty={isOptionalEmpty(link.id)}
onclick={() => handleClick(link.id)}
title={isOptionalEmpty(link.id) ? `${link.label} is empty for this selection` : link.label}
>
{link.label}
</button>
Expand Down Expand Up @@ -75,8 +86,13 @@
transition: color 0.15s;
font-weight: 400;
}
.nav-link.empty:not(.active) {
color: var(--body-text-color-subdued, #9ca3af);
opacity: 0.48;
}
.nav-link:hover {
color: var(--body-text-color, #1f2937);
opacity: 1;
}
.nav-link.active {
color: var(--body-text-color, #1f2937);
Expand Down
Loading