Skip to content

Commit 2edef9d

Browse files
committed
fix: charts data mismatch & project api key generation
1 parent f710cb7 commit 2edef9d

10 files changed

Lines changed: 178 additions & 217 deletions

webapp/src/api/schemas.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,25 @@ export const ProjectInputsSchema = z.object({
4747
});
4848
export type ProjectInputs = z.infer<typeof ProjectInputsSchema>;
4949

50+
// `token` is only populated on creation; list responses serialize it as null.
5051
export const ProjectTokenSchema = z.object({
5152
id: z.string(),
5253
project_id: z.string(),
53-
last_used: z.string().nullable(),
54-
name: z.string(),
55-
token: z.string(),
54+
last_used: z.string().nullish(),
55+
name: z.string().nullish(),
56+
token: z.string().nullish(),
5657
access: z.number(),
58+
revoked: z.boolean().nullish(),
5759
});
5860
export type IProjectToken = z.infer<typeof ProjectTokenSchema>;
5961

62+
export const AccessLevel = {
63+
READ: 1,
64+
WRITE: 2,
65+
READ_WRITE: 3,
66+
} as const;
67+
export type AccessLevel = (typeof AccessLevel)[keyof typeof AccessLevel];
68+
6069
// Backend's `Optional[...]` fields are serialized as JSON `null` (Pydantic
6170
// default, no `exclude_none`). Zod's `.optional()` accepts `undefined`
6271
// only — `.nullish()` accepts `null | undefined`, which is what we need

webapp/src/components/createExperimentModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function CreateExperimentModal({
2424
projectId: string;
2525
isOpen: boolean;
2626
onClose: () => void;
27-
onExperimentCreated: () => void;
27+
onExperimentCreated?: () => void | Promise<void>;
2828
}) {
2929
const [isCopied, setIsCopied] = useState(false);
3030
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -82,7 +82,7 @@ export default function CreateExperimentModal({
8282
const newExperiment = await createExperiment(experimentData);
8383
setCreatedExperiment(newExperiment);
8484
setIsCreated(true);
85-
await onExperimentCreated();
85+
await onExperimentCreated?.();
8686
toast.success(
8787
`Experiment ${experimentData.name} created successfully`,
8888
);

webapp/src/components/emissions-time-series.tsx

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
1818
import { ExportCsvButton } from "@/components/export-csv-button";
1919
import { getEmissionsTimeSeries } from "@/api/runs";
2020
import { exportEmissionsTimeSeriesCsv } from "@/utils/export";
21+
import { format } from "date-fns";
2122
import { Cpu, HardDrive, Server } from "lucide-react";
2223

2324
interface EmissionsTimeSeriesChartProps {
@@ -48,26 +49,31 @@ export default function EmissionsTimeSeriesChart({
4849
React.useState<keyof typeof chartConfig>("emissions_rate");
4950
const [emissionTimeSeries, setEmissionTimeSeries] =
5051
React.useState<EmissionsTimeSeries | null>(null);
51-
const [isLoading, setIsLoading] = React.useState(true);
52+
const [isLoading, setIsLoading] = React.useState(false);
5253

5354
React.useEffect(() => {
54-
async function fetchData() {
55+
if (!runId) return;
56+
let cancelled = false;
57+
(async () => {
5558
setIsLoading(true);
5659
try {
5760
const data = await getEmissionsTimeSeries(runId);
58-
setEmissionTimeSeries(data);
61+
if (!cancelled) setEmissionTimeSeries(data);
5962
} catch (error) {
6063
console.error("Failed to fetch emissions time series:", error);
6164
} finally {
62-
setIsLoading(false);
65+
if (!cancelled) setIsLoading(false);
6366
}
64-
}
65-
66-
if (runId) {
67-
fetchData();
68-
}
67+
})();
68+
return () => {
69+
cancelled = true;
70+
};
6971
}, [runId]);
7072

73+
if (!runId) {
74+
return null;
75+
}
76+
7177
if (isLoading) {
7278
return <div>Loading...</div>;
7379
}
@@ -196,13 +202,9 @@ export default function EmissionsTimeSeriesChart({
196202
axisLine={false}
197203
tickMargin={8}
198204
minTickGap={32}
199-
tickFormatter={(value) => {
200-
const date = new Date(value);
201-
return date.toLocaleDateString("en-US", {
202-
month: "short",
203-
day: "numeric",
204-
});
205-
}}
205+
tickFormatter={(value) =>
206+
format(new Date(value), "MMM d, HH:mm")
207+
}
206208
/>
207209
<YAxis
208210
tickLine={false}
@@ -213,15 +215,12 @@ export default function EmissionsTimeSeriesChart({
213215
content={
214216
<ChartTooltipContent
215217
className="w-[150px]"
216-
labelFormatter={(value) => {
217-
return new Date(
218-
value,
219-
).toLocaleDateString("en-US", {
220-
month: "short",
221-
day: "numeric",
222-
year: "numeric",
223-
});
224-
}}
218+
labelFormatter={(value) =>
219+
format(
220+
new Date(value),
221+
"MMM d, yyyy HH:mm",
222+
)
223+
}
225224
/>
226225
}
227226
/>

webapp/src/components/experiment-bar-chart.tsx

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@/components/ui/chart";
1717
import { exportExperimentsToCsv } from "@/utils/export";
1818
import { Loader2 } from "lucide-react";
19-
import { useEffect, useState } from "react";
19+
import { useMemo, useState } from "react";
2020
import { ExportCsvButton } from "./export-csv-button";
2121

2222
interface ExperimentsBarChartProps {
@@ -47,37 +47,38 @@ export default function ExperimentsBarChart({
4747
localLoading = false,
4848
projectName,
4949
}: ExperimentsBarChartProps) {
50-
const [selectedBar, setSelectedBar] = useState(0);
5150
const [isExporting, setIsExporting] = useState(false);
5251

53-
const handleBarClick = (data: ExperimentReport, index: number) => {
54-
onExperimentClick(data.experiment_id);
55-
};
56-
useEffect(() => {
57-
const highlightedBar = experimentsReportData.findIndex(
58-
(experiment) => experiment.experiment_id === selectedExperimentId,
59-
);
60-
setSelectedBar(highlightedBar);
61-
}, [selectedExperimentId, experimentsReportData]);
52+
const selectedBar = useMemo(
53+
() =>
54+
experimentsReportData.findIndex(
55+
(experiment) =>
56+
experiment.experiment_id === selectedExperimentId,
57+
),
58+
[experimentsReportData, selectedExperimentId],
59+
);
6260

63-
const CustomBar = (props: any) => {
64-
const { fill, x, y, width, height, index } = props;
65-
const barFill =
66-
selectedBar === index
67-
? "var(--color-desktop)"
68-
: "var(--color-mobile)";
69-
return (
70-
<rect
71-
x={x}
72-
y={y}
73-
width={width}
74-
height={height}
75-
fill={barFill}
76-
onClick={() => handleBarClick(props.payload, index)}
77-
cursor="pointer"
78-
/>
79-
);
80-
};
61+
const CustomBar = useMemo(
62+
() => (props: any) => {
63+
const { x, y, width, height, index, payload } = props;
64+
const barFill =
65+
selectedBar === index
66+
? "var(--color-desktop)"
67+
: "var(--color-mobile)";
68+
return (
69+
<rect
70+
x={x}
71+
y={y}
72+
width={width}
73+
height={height}
74+
fill={barFill}
75+
onClick={() => onExperimentClick(payload.experiment_id)}
76+
cursor="pointer"
77+
/>
78+
);
79+
},
80+
[selectedBar, onExperimentClick],
81+
);
8182
return (
8283
<Card>
8384
<CardHeader className="flex flex-row items-center justify-between">

webapp/src/components/project-dashboard-base.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface ProjectDashboardBaseProps {
5656
selectedRunId: string;
5757
onExperimentClick: (experimentId: string) => void;
5858
onRunClick: (runId: string) => void;
59+
onExperimentCreated?: () => void;
5960
headerContent?: ReactNode;
6061
isLoading?: boolean;
6162
}
@@ -74,6 +75,7 @@ export default function ProjectDashboardBase({
7475
selectedRunId,
7576
onExperimentClick,
7677
onRunClick,
78+
onExperimentCreated,
7779
headerContent,
7880
isLoading = false,
7981
}: ProjectDashboardBaseProps) {
@@ -83,12 +85,6 @@ export default function ProjectDashboardBase({
8385
setIsExperimentModalOpen(true);
8486
};
8587

86-
const refreshExperimentList = async () => {
87-
// In Vite+React Router, we don't have router.refresh()
88-
// The parent component handles data refresh via callbacks
89-
window.location.reload();
90-
};
91-
9288
const experimentName = experimentsReportData.find(
9389
(experiment) => experiment.experiment_id === selectedExperimentId,
9490
)?.name;
@@ -289,7 +285,7 @@ export default function ProjectDashboardBase({
289285
projectId={project.id}
290286
isOpen={isExperimentModalOpen}
291287
onClose={() => setIsExperimentModalOpen(false)}
292-
onExperimentCreated={refreshExperimentList}
288+
onExperimentCreated={onExperimentCreated}
293289
/>
294290
</div>
295291
)}

webapp/src/components/project-dashboard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export default function ProjectDashboard({
227227
selectedRunId={selectedRunId}
228228
onExperimentClick={onExperimentClick}
229229
onRunClick={onRunClick}
230+
onExperimentCreated={onRefresh}
230231
projectExperiments={projectExperiments}
231232
headerContent={headerContent}
232233
isLoading={isLoading}

webapp/src/components/projectTokens/custom-row-token.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export default function CustomRowToken({
4444
return (
4545
<CustomRow
4646
rowKey={projectToken.id}
47-
firstColumn={projectToken.name}
48-
secondColumn={projectToken.token}
47+
firstColumn={projectToken.name ?? "-"}
48+
secondColumn={projectToken.token ?? "•••••••• (hidden)"}
4949
onDelete={() => handleDelete(projectToken)}
5050
deleteDisabled={isDeleting}
5151
/>

webapp/src/components/projectTokens/projectTokenTable.tsx

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Card } from "@/components/ui/card";
22
import { Table, TableBody } from "@/components/ui/table";
3-
import { IProjectToken } from "@/api/schemas";
3+
import { AccessLevel, IProjectToken } from "@/api/schemas";
44
import { getProjectTokens, createProjectToken } from "@/api/projectTokens";
55
import CustomRowToken from "@/components/projectTokens/custom-row-token";
6-
import { useState, useEffect, useRef } from "react";
6+
import { useMemo, useState, useEffect, useRef } from "react";
77
import { Loader2, ClipboardCopy, ClipboardCheck } from "lucide-react";
88
import { Button } from "@/components/ui/button";
99
import { Input } from "@/components/ui/input";
@@ -43,23 +43,21 @@ export const ProjectTokensTable = ({ projectId }: { projectId: string }) => {
4343
if (isSubmitting) return;
4444

4545
setIsSubmitting(true);
46-
4746
try {
48-
const access = 2;
49-
const newToken = await toast
50-
.promise(createProjectToken(projectId, tokenName, access), {
51-
loading: `Creating token ${tokenName}...`,
52-
success: `Token ${tokenName} created successfully`,
53-
error: (error) =>
54-
`Failed to create token: ${error instanceof Error ? error.message : "Unknown error"}`,
55-
})
56-
.unwrap();
57-
58-
setCreatedToken(newToken.token);
47+
const newToken = await createProjectToken(
48+
projectId,
49+
tokenName,
50+
AccessLevel.WRITE,
51+
);
52+
toast.success(`Token ${tokenName} created successfully`);
53+
setCreatedToken(newToken.token ?? null);
5954
setTokenName("");
6055
refreshTokens();
6156
} catch (error) {
6257
console.error("Failed to create token:", error);
58+
toast.error(
59+
`Failed to create token: ${error instanceof Error ? error.message : "Unknown error"}`,
60+
);
6361
} finally {
6462
setIsSubmitting(false);
6563
}
@@ -89,6 +87,20 @@ export const ProjectTokensTable = ({ projectId }: { projectId: string }) => {
8987
setIsCreatingToken(false);
9088
};
9189

90+
const sortedTokens = useMemo(
91+
() =>
92+
tokens
93+
? tokens
94+
.slice()
95+
.sort((a, b) =>
96+
(a.name ?? "")
97+
.toLowerCase()
98+
.localeCompare((b.name ?? "").toLowerCase()),
99+
)
100+
: null,
101+
[tokens],
102+
);
103+
92104
return (
93105
<div className="flex-col p-4 md:gap-8 md:p-4 justify-between">
94106
<div className="flex-1 mb-4">
@@ -176,15 +188,15 @@ export const ProjectTokensTable = ({ projectId }: { projectId: string }) => {
176188
<Card>
177189
<Table>
178190
<TableBody>
179-
{tokens === null ? (
191+
{sortedTokens === null ? (
180192
<tr>
181193
<td colSpan={3} className="text-center py-6">
182194
<div className="flex justify-center">
183195
<Loader2 className="h-8 w-8 animate-spin text-primary" />
184196
</div>
185197
</td>
186198
</tr>
187-
) : tokens.length === 0 ? (
199+
) : sortedTokens.length === 0 ? (
188200
<tr>
189201
<td colSpan={3} className="text-center py-6">
190202
<p className="text-muted-foreground">
@@ -197,19 +209,13 @@ export const ProjectTokensTable = ({ projectId }: { projectId: string }) => {
197209
</td>
198210
</tr>
199211
) : (
200-
tokens
201-
.sort((a, b) =>
202-
a.name
203-
.toLowerCase()
204-
.localeCompare(b.name.toLowerCase()),
205-
)
206-
.map((projectToken, index) => (
207-
<CustomRowToken
208-
key={index}
209-
projectToken={projectToken}
210-
onTokenDeleted={refreshTokens}
211-
/>
212-
))
212+
sortedTokens.map((projectToken) => (
213+
<CustomRowToken
214+
key={projectToken.id}
215+
projectToken={projectToken}
216+
onTokenDeleted={refreshTokens}
217+
/>
218+
))
213219
)}
214220
</TableBody>
215221
</Table>

0 commit comments

Comments
 (0)