Skip to content

Commit 3c515af

Browse files
committed
Subdue empty dashboard tabs
1 parent e3642f6 commit 3c515af

2 files changed

Lines changed: 207 additions & 2 deletions

File tree

trackio/frontend/src/App.svelte

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
getAllProjects,
1616
getRunsForProject,
1717
getAlerts,
18+
getLogsBatch,
19+
getTraces,
20+
getSystemMetricsForRun,
21+
getProjectFiles,
1822
getRunMutationStatus,
1923
getSettings,
2024
getReadOnlySource,
@@ -88,6 +92,37 @@
8892
let spaceId = $state(null);
8993
let availableSystemDevices = $state([]);
9094
let selectedSystemDevices = $state([]);
95+
let tabAvailability = $state({});
96+
let tabAvailabilityRequestId = 0;
97+
let lastTabAvailabilityRefreshAt = 0;
98+
let shouldOpenFirstNonEmptyTab = false;
99+
let openedFirstNonEmptyTab = false;
100+
const TAB_AVAILABILITY_POLL_INTERVAL_MS = 15000;
101+
102+
const OPTIONAL_EMPTY_TABS = new Set([
103+
"system",
104+
"traces",
105+
"media",
106+
"reports",
107+
"files",
108+
]);
109+
const AUTO_OPEN_TAB_ORDER = [
110+
"metrics",
111+
"system",
112+
"traces",
113+
"media",
114+
"reports",
115+
"runs",
116+
"files",
117+
];
118+
const RESERVED_METRIC_KEYS = new Set([
119+
"project",
120+
"run",
121+
"timestamp",
122+
"step",
123+
"time",
124+
"metrics",
125+
]);
91126
92127
function runKey(run) {
93128
return run?.id ?? run?.name;
@@ -98,10 +133,16 @@
98133
);
99134
100135
function handleNavigate(page) {
136+
openedFirstNonEmptyTab = true;
101137
currentPage = page;
102138
navigateTo(page);
103139
}
104140
141+
function isBareDashboardPath() {
142+
const pathname = window.location.pathname.replace(/\/+$/, "") || "/";
143+
return pathname === "/";
144+
}
145+
105146
function lockedProjectName() {
106147
return getQueryParam("project") || getQueryParam("selected_project");
107148
}
@@ -171,6 +212,139 @@
171212
}
172213
}
173214
215+
function initialAvailability() {
216+
return {
217+
metrics: false,
218+
system: false,
219+
traces: false,
220+
media: false,
221+
reports: false,
222+
runs: false,
223+
files: false,
224+
};
225+
}
226+
227+
function logHasScalarMetric(log) {
228+
return Object.entries(log || {}).some(([key, value]) => (
229+
!RESERVED_METRIC_KEYS.has(key) &&
230+
typeof value === "number" &&
231+
Number.isFinite(value)
232+
));
233+
}
234+
235+
function logHasMedia(log) {
236+
return Object.values(log || {}).some((value) => (
237+
value &&
238+
typeof value === "object" &&
239+
["trackio.image", "trackio.video", "trackio.audio", "trackio.table"].includes(value._type)
240+
));
241+
}
242+
243+
function logHasMarkdownReport(log) {
244+
return Object.values(log || {}).some((value) => (
245+
value &&
246+
typeof value === "object" &&
247+
value._type === "trackio.markdown"
248+
));
249+
}
250+
251+
async function anyRunHasSystemMetrics(project, runRecords) {
252+
const results = await Promise.all(
253+
runRecords.map(async (run) => {
254+
try {
255+
const metrics = await getSystemMetricsForRun(project, run);
256+
return (metrics || []).length > 0;
257+
} catch {
258+
return false;
259+
}
260+
}),
261+
);
262+
return results.some(Boolean);
263+
}
264+
265+
async function anyRunHasTraces(project, runRecords) {
266+
const results = await Promise.all(
267+
runRecords.map(async (run) => {
268+
try {
269+
const traces = await getTraces(project, run, { limit: 1 });
270+
return (traces || []).length > 0;
271+
} catch {
272+
return false;
273+
}
274+
}),
275+
);
276+
return results.some(Boolean);
277+
}
278+
279+
async function refreshTabAvailability({ force = false } = {}) {
280+
const now = Date.now();
281+
if (
282+
!force &&
283+
now - lastTabAvailabilityRefreshAt < TAB_AVAILABILITY_POLL_INTERVAL_MS
284+
) {
285+
return;
286+
}
287+
lastTabAvailabilityRefreshAt = now;
288+
const requestId = ++tabAvailabilityRequestId;
289+
if (!selectedProject) {
290+
tabAvailability = initialAvailability();
291+
return;
292+
}
293+
294+
const availability = {
295+
...initialAvailability(),
296+
runs: runs.length > 0,
297+
};
298+
const runRecords = selectedRunRecords;
299+
300+
try {
301+
const [
302+
logsBatch,
303+
projectAlerts,
304+
hasSystem,
305+
hasTraces,
306+
projectFiles,
307+
] = await Promise.all([
308+
runRecords.length ? getLogsBatch(selectedProject, runRecords) : [],
309+
getAlerts(selectedProject, null, null, null).catch(() => []),
310+
runRecords.length ? anyRunHasSystemMetrics(selectedProject, runRecords) : false,
311+
runRecords.length ? anyRunHasTraces(selectedProject, runRecords) : false,
312+
getProjectFiles(selectedProject).catch(() => []),
313+
]);
314+
if (requestId !== tabAvailabilityRequestId) return;
315+
316+
const selectedRunNames = new Set(runRecords.map((run) => run.name));
317+
const selectedAlerts = (projectAlerts || []).filter(
318+
(alert) => !alert.run || selectedRunNames.has(alert.run),
319+
);
320+
for (const entry of logsBatch || []) {
321+
for (const log of entry.logs || []) {
322+
availability.metrics ||= logHasScalarMetric(log);
323+
availability.media ||= logHasMedia(log);
324+
availability.reports ||= logHasMarkdownReport(log);
325+
}
326+
}
327+
availability.system = hasSystem;
328+
availability.traces = hasTraces;
329+
availability.reports ||= selectedAlerts.length > 0;
330+
availability.files = (projectFiles || []).length > 0;
331+
tabAvailability = availability;
332+
333+
if (shouldOpenFirstNonEmptyTab && !openedFirstNonEmptyTab && isBareDashboardPath()) {
334+
const first = AUTO_OPEN_TAB_ORDER.find((page) => availability[page]);
335+
if (first && first !== currentPage) {
336+
currentPage = first;
337+
navigateTo(first);
338+
}
339+
openedFirstNonEmptyTab = true;
340+
}
341+
} catch (e) {
342+
if (requestId !== tabAvailabilityRequestId) return;
343+
console.error("Failed to load tab availability:", e);
344+
tabAvailability = availability;
345+
}
346+
}
347+
174348
function startPolling() {
175349
if (pollTimer) clearInterval(pollTimer);
176350
pollTimer = setInterval(async () => {
@@ -180,6 +354,7 @@
180354
await refreshProjects();
181355
await refreshRuns();
182356
await refreshAlerts();
357+
await refreshTabAvailability();
183358
}, getAppPollIntervalMs());
184359
}
185360
@@ -268,6 +443,7 @@
268443
showHeaders = false;
269444
}
270445
446+
shouldOpenFirstNonEmptyTab = isBareDashboardPath();
271447
currentPage = getPageFromPath();
272448
273449
window.addEventListener("popstate", () => {
@@ -309,6 +485,7 @@
309485
await refreshRuns();
310486
311487
await refreshAlerts();
488+
await refreshTabAvailability({ force: true });
312489
} catch (e) {
313490
console.error("Failed to load projects:", e);
314491
} finally {
@@ -339,6 +516,13 @@
339516
if (projectLocked) applyLockedProject();
340517
});
341518
519+
$effect(() => {
520+
selectedProject;
521+
selectedRuns;
522+
runs;
523+
if (appBootstrapReady) refreshTabAvailability({ force: true });
524+
});
525+
342526
let urlRunsFromQueryApplied = $state(false);
343527
344528
$effect(() => {
@@ -421,7 +605,12 @@
421605
422606
<div class="main">
423607
{#if !navbarHidden}
424-
<Navbar {currentPage} onNavigate={handleNavigate} />
608+
<Navbar
609+
{currentPage}
610+
{tabAvailability}
611+
optionalEmptyTabs={OPTIONAL_EMPTY_TABS}
612+
onNavigate={handleNavigate}
613+
/>
425614
{/if}
426615
427616
<div class="page-content">

trackio/frontend/src/components/Navbar.svelte

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<script>
2-
let { currentPage = "metrics", onNavigate } = $props();
2+
let {
3+
currentPage = "metrics",
4+
tabAvailability = {},
5+
optionalEmptyTabs = new Set(),
6+
onNavigate,
7+
} = $props();
38
49
const links = [
510
{ id: "metrics", label: "Metrics" },
@@ -14,6 +19,10 @@
1419
function handleClick(id) {
1520
onNavigate?.(id);
1621
}
22+
23+
function isOptionalEmpty(id) {
24+
return optionalEmptyTabs.has(id) && tabAvailability[id] === false;
25+
}
1726
</script>
1827
1928
<nav class="navbar">
@@ -23,7 +32,9 @@
2332
<button
2433
class="nav-link"
2534
class:active={currentPage === link.id}
35+
class:empty={isOptionalEmpty(link.id)}
2636
onclick={() => handleClick(link.id)}
37+
title={isOptionalEmpty(link.id) ? `${link.label} is empty for this selection` : link.label}
2738
>
2839
{link.label}
2940
</button>
@@ -75,8 +86,13 @@
7586
transition: color 0.15s;
7687
font-weight: 400;
7788
}
89+
.nav-link.empty:not(.active) {
90+
color: var(--body-text-color-subdued, #9ca3af);
91+
opacity: 0.48;
92+
}
7893
.nav-link:hover {
7994
color: var(--body-text-color, #1f2937);
95+
opacity: 1;
8096
}
8197
.nav-link.active {
8298
color: var(--body-text-color, #1f2937);

0 commit comments

Comments
 (0)