Skip to content

Commit f338fc1

Browse files
authored
added suspense to index page (tensorzero#3102)
* added suspense to index page * fixed improper memoization of gateway connection info
1 parent d516d87 commit f338fc1

File tree

5 files changed

+130
-75
lines changed

5 files changed

+130
-75
lines changed

ui/app/components/layout/TensorZeroStatusIndicator.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useTensorZeroStatusFetcher } from "~/routes/api/tensorzero/status";
22
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
3+
import { useMemo } from "react";
34

45
/**
56
* A component that displays the status of the TensorZero Gateway.
@@ -11,26 +12,26 @@ export default function TensorZeroStatusIndicator() {
1112
// Extract server version from status if available
1213
const serverVersion = status?.version || "";
1314

14-
// Check if versions match (ignoring patch version)
15-
const versionsMatch = () => {
15+
// Check if versions match (ignoring patch version) - memoized to prevent re-renders
16+
const versionsMatch = useMemo(() => {
1617
if (!serverVersion) return true; // No data yet, don't show warning
1718
// We can do an exact match for now
1819
return uiVersion === serverVersion;
19-
};
20+
}, [uiVersion, serverVersion]);
2021

21-
const getStatusColor = () => {
22+
const statusColor = useMemo(() => {
2223
if (isLoading || status === undefined) return "bg-gray-300"; // Loading or initial state
2324
if (!status) return "bg-red-500"; // Could not connect (explicit null/failed state)
24-
if (!versionsMatch()) return "bg-yellow-500"; // Version mismatch
25+
if (!versionsMatch) return "bg-yellow-500"; // Version mismatch
2526
return "bg-green-500"; // Everything is good
26-
};
27+
}, [isLoading, status, versionsMatch]);
2728

2829
return (
2930
<div className="px-3 py-2 text-xs">
3031
<div className="text-fg-muted flex flex-col gap-1 truncate">
3132
<div className="flex items-center gap-2">
3233
<div
33-
className={`h-2 w-2 rounded-full ${getStatusColor()} mr-1 inline-block`}
34+
className={`h-2 w-2 rounded-full ${statusColor} mr-1 inline-block`}
3435
/>
3536
{isLoading
3637
? "Checking status..."
@@ -40,7 +41,7 @@ export default function TensorZeroStatusIndicator() {
4041
? `TensorZero Gateway ${serverVersion}`
4142
: "Failed to connect to Gateway"}
4243
</div>
43-
{status && !versionsMatch() && (
44+
{status && !versionsMatch && (
4445
<div className="ml-5">
4546
<Tooltip>
4647
<TooltipTrigger asChild>

ui/app/components/layout/app.sidebar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const navigation: NavigationSection[] = [
104104

105105
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
106106
const { state } = useSidebar();
107-
const isActivePath = useActivePath();
107+
const activePathUtils = useActivePath();
108108

109109
return (
110110
<Sidebar collapsible="icon" {...props}>
@@ -132,7 +132,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
132132
<SidebarMenuButton
133133
asChild
134134
tooltip={state === "collapsed" ? "Dashboard" : undefined}
135-
isActive={isActivePath("/")}
135+
isActive={activePathUtils.isActive("/")}
136136
>
137137
<Link to="/" className="flex items-center gap-2">
138138
<Home className="h-4 w-4" />
@@ -153,7 +153,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
153153
<SidebarMenuButton
154154
asChild
155155
tooltip={state === "collapsed" ? item.title : undefined}
156-
isActive={isActivePath(item.url)}
156+
isActive={activePathUtils.isActive(item.url)}
157157
>
158158
<Link
159159
to={item.url}

ui/app/hooks/use-active-path.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { useLocation } from "react-router";
2+
import { useMemo } from "react";
23

34
export function useActivePath() {
45
const location = useLocation();
6+
const pathname = location.pathname;
57

6-
return (path: string) => {
7-
if (path === "/") {
8-
return location.pathname === "/";
9-
}
10-
return location.pathname.startsWith(path);
11-
};
8+
// Return a stable object with the function as a property
9+
// This avoids creating new function references
10+
return useMemo(
11+
() => ({
12+
isActive: (path: string) => {
13+
if (path === "/") {
14+
return pathname === "/";
15+
}
16+
return pathname.startsWith(path);
17+
},
18+
}),
19+
[pathname],
20+
);
1221
}

ui/app/routes/api/tensorzero/status.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useFetcher } from "react-router";
22
import { getTensorZeroClient } from "~/utils/tensorzero.server";
3-
import { useEffect } from "react";
3+
import { useEffect, useMemo } from "react";
44
import { logger } from "~/utils/logger";
55

66
export async function loader() {
@@ -20,14 +20,14 @@ export async function loader() {
2020
export function useTensorZeroStatusFetcher() {
2121
const statusFetcher = useFetcher();
2222
const status = statusFetcher.data;
23+
const isLoading = statusFetcher.state === "loading";
2324

2425
useEffect(() => {
2526
if (statusFetcher.state === "idle" && !statusFetcher.data) {
2627
statusFetcher.load("/api/tensorzero/status");
2728
}
28-
// TODO: Fix and stop ignoring lint rule
2929
// eslint-disable-next-line react-hooks/exhaustive-deps
30-
}, []);
30+
}, []); // Empty dependency array - only run on mount
3131

32-
return { status, isLoading: statusFetcher.state === "loading" };
32+
return useMemo(() => ({ status, isLoading }), [status, isLoading]);
3333
}

ui/app/routes/index.tsx

Lines changed: 99 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Link, type RouteHandle } from "react-router";
1+
import { Link, type RouteHandle, Await } from "react-router";
2+
import * as React from "react";
23
import { Card } from "~/components/ui/card";
34
import { PageLayout } from "~/components/layout/PageLayout";
45
import {
@@ -21,10 +22,9 @@ import {
2122
countInferencesByFunction,
2223
countEpisodes,
2324
} from "~/utils/clickhouse/inference.server";
24-
import { getAllFunctionConfigs } from "~/utils/config/index.server";
25+
import { getConfig, getAllFunctionConfigs } from "~/utils/config/index.server";
2526
import { getDatasetCounts } from "~/utils/clickhouse/datasets.server";
2627
import { countTotalEvaluationRuns } from "~/utils/clickhouse/evaluations.server";
27-
import { useConfig } from "~/context/config";
2828
import type { Route } from "./+types/index";
2929
import {
3030
countDynamicEvaluationProjects,
@@ -39,7 +39,7 @@ interface DirectoryCardProps {
3939
source: string;
4040
icon: React.ComponentType<{ className?: string; size?: number }>;
4141
title: string;
42-
description: string;
42+
description: string | Promise<string>;
4343
}
4444

4545
function DirectoryCard({
@@ -62,7 +62,19 @@ function DirectoryCard({
6262
{title}
6363
</h3>
6464
<p className="text-fg-secondary overflow-hidden text-xs text-ellipsis whitespace-nowrap">
65-
{description}
65+
{typeof description === "string" ? (
66+
description
67+
) : (
68+
<React.Suspense
69+
fallback={
70+
<span className="bg-bg-tertiary inline-block h-3 w-16 animate-pulse rounded"></span>
71+
}
72+
>
73+
<Await resolve={description}>
74+
{(resolvedDescription) => resolvedDescription}
75+
</Await>
76+
</React.Suspense>
77+
)}
6678
</p>
6779
</div>
6880
</Card>
@@ -93,55 +105,88 @@ function FooterLink({ source, icon: Icon, children }: FooterLinkProps) {
93105
}
94106

95107
export async function loader() {
96-
const [
97-
countsInfo,
98-
numEpisodes,
99-
datasetCounts,
100-
numEvaluationRuns,
101-
numDynamicEvaluationRuns,
102-
numDynamicEvaluationRunProjects,
103-
functionConfigs,
104-
] = await Promise.all([
105-
countInferencesByFunction(),
106-
countEpisodes(),
107-
getDatasetCounts({}),
108-
countTotalEvaluationRuns(),
109-
countDynamicEvaluationRuns(),
110-
countDynamicEvaluationProjects(),
111-
getAllFunctionConfigs(),
112-
]);
113-
const totalInferences = countsInfo.reduce((acc, curr) => acc + curr.count, 0);
114-
const numFunctions = Object.keys(functionConfigs).length;
115-
const numVariants = Object.values(functionConfigs).reduce((acc, config) => {
116-
return acc + (config ? Object.keys(config.variants || {}).length : 0);
117-
}, 0);
118-
const numDatasets = datasetCounts.length;
108+
// Create the promises
109+
const countsInfoPromise = countInferencesByFunction();
110+
const numEpisodesPromise = countEpisodes();
111+
const datasetCountsPromise = getDatasetCounts({});
112+
const numEvaluationRunsPromise = countTotalEvaluationRuns();
113+
const numDynamicEvaluationRunsPromise = countDynamicEvaluationRuns();
114+
const numDynamicEvaluationRunProjectsPromise =
115+
countDynamicEvaluationProjects();
116+
const configPromise = getConfig();
117+
const functionConfigsPromise = getAllFunctionConfigs();
118+
119+
// Create derived promises - these will be stable references
120+
const totalInferencesDesc = countsInfoPromise.then((countsInfo) => {
121+
const total = countsInfo.reduce((acc, curr) => acc + curr.count, 0);
122+
return `${total.toLocaleString()} inferences`;
123+
});
124+
125+
const numFunctionsDesc = functionConfigsPromise.then((functionConfigs) => {
126+
const numFunctions = Object.keys(functionConfigs).length;
127+
return `${numFunctions} functions`;
128+
});
129+
130+
const numVariantsDesc = functionConfigsPromise.then((functionConfigs) => {
131+
const numVariants = Object.values(functionConfigs).reduce(
132+
(acc, funcConfig) => {
133+
return (
134+
acc + (funcConfig ? Object.keys(funcConfig.variants || {}).length : 0)
135+
);
136+
},
137+
0,
138+
);
139+
return `${numVariants} variants`;
140+
});
141+
142+
const numEpisodesDesc = numEpisodesPromise.then(
143+
(numEpisodes) => `${numEpisodes.toLocaleString()} episodes`,
144+
);
145+
146+
const numDatasetsDesc = datasetCountsPromise.then(
147+
(datasetCounts) => `${datasetCounts.length} datasets`,
148+
);
149+
150+
const numEvaluationRunsDesc = numEvaluationRunsPromise.then(
151+
(runs) => `evaluations, ${runs} runs`,
152+
);
153+
154+
// We need to create a special promise for the static evaluations that includes the config count
155+
const staticEvaluationsDesc = Promise.all([
156+
configPromise,
157+
numEvaluationRunsPromise,
158+
]).then(([config, runs]) => {
159+
const numEvaluations = Object.keys(config.evaluations || {}).length;
160+
return `${numEvaluations} evaluations, ${runs} runs`;
161+
});
162+
163+
const dynamicEvaluationsDesc = Promise.all([
164+
numDynamicEvaluationRunProjectsPromise,
165+
numDynamicEvaluationRunsPromise,
166+
]).then(([projects, runs]) => `${projects} projects, ${runs} runs`);
119167

120168
return {
121-
totalInferences,
122-
numFunctions,
123-
numVariants,
124-
numEpisodes,
125-
numDatasets,
126-
numEvaluationRuns,
127-
numDynamicEvaluationRuns,
128-
numDynamicEvaluationRunProjects,
169+
totalInferencesDesc,
170+
numFunctionsDesc,
171+
numVariantsDesc,
172+
numEpisodesDesc,
173+
numDatasetsDesc,
174+
numEvaluationRunsDesc,
175+
staticEvaluationsDesc,
176+
dynamicEvaluationsDesc,
129177
};
130178
}
131179

132180
export default function Home({ loaderData }: Route.ComponentProps) {
133181
const {
134-
totalInferences,
135-
numFunctions,
136-
numVariants,
137-
numEpisodes,
138-
numDatasets,
139-
numEvaluationRuns,
140-
numDynamicEvaluationRuns,
141-
numDynamicEvaluationRunProjects,
182+
totalInferencesDesc,
183+
numFunctionsDesc,
184+
numVariantsDesc,
185+
numEpisodesDesc,
186+
numDatasetsDesc,
187+
staticEvaluationsDesc,
188+
dynamicEvaluationsDesc,
142189
} = loaderData;
143-
const config = useConfig();
144-
const numEvaluations = Object.keys(config.evaluations).length;
145190

146191
return (
147192
<PageLayout>
@@ -157,19 +202,19 @@ export default function Home({ loaderData }: Route.ComponentProps) {
157202
source="/observability/inferences"
158203
icon={Inferences}
159204
title="Inferences"
160-
description={`${totalInferences.toLocaleString()} inferences`}
205+
description={totalInferencesDesc}
161206
/>
162207
<DirectoryCard
163208
source="/observability/episodes"
164209
icon={Episodes}
165210
title="Episodes"
166-
description={`${numEpisodes.toLocaleString()} episodes`}
211+
description={numEpisodesDesc}
167212
/>
168213
<DirectoryCard
169214
source="/observability/functions"
170215
icon={Functions}
171216
title="Functions"
172-
description={`${numFunctions} functions`}
217+
description={numFunctionsDesc}
173218
/>
174219
</div>
175220
</div>
@@ -183,7 +228,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
183228
source="/optimization/supervised-fine-tuning"
184229
icon={SupervisedFineTuning}
185230
title="Supervised Fine-tuning"
186-
description={`${numFunctions} functions`}
231+
description={numFunctionsDesc}
187232
/>
188233
</div>
189234
</div>
@@ -195,25 +240,25 @@ export default function Home({ loaderData }: Route.ComponentProps) {
195240
source="/playground"
196241
icon={Playground}
197242
title="Playground"
198-
description={`${numVariants} variants`}
243+
description={numVariantsDesc}
199244
/>
200245
<DirectoryCard
201246
source="/datasets"
202247
icon={Dataset}
203248
title="Datasets"
204-
description={`${numDatasets} datasets`}
249+
description={numDatasetsDesc}
205250
/>
206251
<DirectoryCard
207252
source="/evaluations"
208253
icon={GridCheck}
209254
title="Static Evaluations"
210-
description={`${numEvaluations} evaluations, ${numEvaluationRuns} runs`}
255+
description={staticEvaluationsDesc}
211256
/>
212257
<DirectoryCard
213258
source="/dynamic_evaluations"
214259
icon={SequenceChecks}
215260
title="Dynamic Evaluations"
216-
description={`${numDynamicEvaluationRunProjects} projects, ${numDynamicEvaluationRuns} runs`}
261+
description={dynamicEvaluationsDesc}
217262
/>
218263
</div>
219264
</div>

0 commit comments

Comments
 (0)