Skip to content

Commit af650b9

Browse files
authored
feat(ui): list experiments of a project in the webapp (#817)
* feat: experiments list * fix: remove experiment creation from public page Signed-off-by: inimaz <93inigo93@gmail.com> * fix: prettier issue with package-lock.json Signed-off-by: inimaz <93inigo93@gmail.com> --------- Signed-off-by: inimaz <93inigo93@gmail.com>
1 parent 5495560 commit af650b9

File tree

12 files changed

+198
-34
lines changed

12 files changed

+198
-34
lines changed

webapp/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webapp/src/app/(dashboard)/[organizationId]/projects/[projectId]/page.tsx

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import {
88
getEquivalentTvTime,
99
} from "@/helpers/constants";
1010
import { getDefaultDateRange } from "@/helpers/date-utils";
11-
import { getProjectEmissionsByExperiment } from "@/server-functions/experiments";
11+
import {
12+
getExperiments,
13+
getProjectEmissionsByExperiment,
14+
} from "@/server-functions/experiments";
1215
import { getOneProject } from "@/server-functions/projects";
16+
import { Experiment } from "@/types/experiment";
1317
import { ExperimentReport } from "@/types/experiment-report";
1418
import { Project } from "@/types/project";
1519
import { use, useCallback, useEffect, useState } from "react";
@@ -51,7 +55,11 @@ export default function ProjectPage({
5155
emissions: { label: "kg eq CO2", value: 0 },
5256
duration: { label: "days", value: 0 },
5357
});
54-
58+
// The experiments of the current project. We need this because experimentReport only contains the experiments that have been run
59+
const [projectExperiments, setProjectExperiments] = useState<Experiment[]>(
60+
[],
61+
);
62+
// The reports (if any) of the experiments
5563
const [experimentsReportData, setExperimentsReportData] = useState<
5664
ExperimentReport[]
5765
>([]);
@@ -70,14 +78,19 @@ export default function ProjectPage({
7078

7179
const [selectedExperimentId, setSelectedExperimentId] =
7280
useState<string>("");
73-
7481
const [selectedRunId, setSelectedRunId] = useState<string>("");
7582

83+
const refreshExperimentList = useCallback(async () => {
84+
// Logic to refresh experiments if needed
85+
const experiments: Experiment[] = await getExperiments(projectId);
86+
setProjectExperiments(experiments);
87+
}, []);
88+
89+
/** Use effect functions */
7690
useEffect(() => {
77-
// Replace with your actual API endpoint
7891
const fetchProjectDetails = async () => {
7992
try {
80-
const project = await getOneProject(projectId);
93+
const project: Project | null = await getOneProject(projectId);
8194
if (!project) {
8295
return;
8396
}
@@ -88,8 +101,9 @@ export default function ProjectPage({
88101
};
89102

90103
fetchProjectDetails();
91-
}, [projectId]);
92-
104+
refreshExperimentList();
105+
}, [projectId, refreshExperimentList]);
106+
// Fetch the experiment report of the current project
93107
useEffect(() => {
94108
async function fetchData() {
95109
setIsLoading(true);
@@ -167,18 +181,29 @@ export default function ProjectPage({
167181
}
168182
}, [projectId, date]);
169183

170-
const handleExperimentClick = useCallback((experimentId: string) => {
171-
setSelectedExperimentId(experimentId);
172-
setRunData((prevData) => ({
173-
...prevData,
174-
experimentId: experimentId,
175-
}));
176-
setSelectedRunId(""); // Réinitialiser le runId sélectionné
177-
}, []);
184+
const handleExperimentClick = useCallback(
185+
(experimentId: string) => {
186+
if (experimentId === selectedExperimentId) {
187+
setSelectedExperimentId("");
188+
setSelectedRunId("");
189+
return;
190+
}
191+
setSelectedExperimentId(experimentId);
192+
setSelectedRunId("");
193+
},
194+
[selectedExperimentId],
195+
);
178196

179-
const handleRunClick = useCallback((runId: string) => {
180-
setSelectedRunId(runId);
181-
}, []);
197+
const handleRunClick = useCallback(
198+
(runId: string) => {
199+
if (runId === selectedRunId) {
200+
setSelectedRunId("");
201+
return;
202+
}
203+
setSelectedRunId(runId);
204+
},
205+
[selectedRunId],
206+
);
182207

183208
return (
184209
<div className="h-full w-full overflow-auto">
@@ -213,6 +238,7 @@ export default function ProjectPage({
213238
runData={runData}
214239
selectedExperimentId={selectedExperimentId}
215240
selectedRunId={selectedRunId}
241+
projectExperiments={projectExperiments}
216242
onExperimentClick={handleExperimentClick}
217243
onRunClick={handleRunClick}
218244
onSettingsClick={handleSettingsClick}

webapp/src/app/public/projects/[projectId]/page.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import Loader from "@/components/loader";
1818
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
1919
import { AlertCircle } from "lucide-react";
2020
import { getDefaultDateRange } from "@/helpers/date-utils";
21+
import { Experiment } from "@/types/experiment";
22+
import { getExperiments } from "@/server-functions/experiments";
2123

2224
export default function PublicProjectPage({
2325
params,
@@ -35,6 +37,11 @@ export default function PublicProjectPage({
3537
// Dashboard state
3638
const default_date = getDefaultDateRange();
3739
const [date, setDate] = useState<DateRange>(default_date);
40+
// The experiments of the current project. We need this because experimentReport only contains the experiments that have been run
41+
const [projectExperiments, setProjectExperiments] = useState<Experiment[]>(
42+
[],
43+
);
44+
// The reports (if any) of the experiments
3845
const [experimentsReportData, setExperimentsReportData] = useState<
3946
ExperimentReport[]
4047
>([]);
@@ -106,6 +113,7 @@ export default function PublicProjectPage({
106113

107114
if (projectId && !project) {
108115
fetchProjectData();
116+
refreshExperimentList();
109117
}
110118
}, [projectId, project]);
111119

@@ -194,6 +202,12 @@ export default function PublicProjectPage({
194202
fetchData();
195203
}
196204
}, [projectId, project, date]);
205+
const refreshExperimentList = useCallback(async () => {
206+
if (!projectId) return;
207+
// Logic to refresh experiments if needed
208+
const experiments: Experiment[] = await getExperiments(projectId);
209+
setProjectExperiments(experiments);
210+
}, []);
197211

198212
const handleExperimentClick = useCallback((experimentId: string) => {
199213
setSelectedExperimentId(experimentId);
@@ -249,6 +263,7 @@ export default function PublicProjectPage({
249263
radialChartData={radialChartData}
250264
convertedValues={convertedValues}
251265
experimentsReportData={experimentsReportData}
266+
projectExperiments={projectExperiments}
252267
runData={runData}
253268
selectedExperimentId={selectedExperimentId}
254269
selectedRunId={selectedRunId}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import {
1818
} from "@/components/ui/chart";
1919
import { exportExperimentsToCsv } from "@/utils/export";
2020
import { Loader2 } from "lucide-react";
21-
import { useState } from "react";
21+
import { useEffect, useState } from "react";
2222
import { ExportCsvButton } from "./export-csv-button";
2323

2424
interface ExperimentsBarChartProps {
2525
isPublicView: boolean;
2626
experimentsReportData: ExperimentReport[];
2727
onExperimentClick: (experimentId: string) => void;
28+
selectedExperimentId: string;
2829
localLoading?: boolean;
2930
projectName: string;
3031
}
@@ -44,6 +45,7 @@ export default function ExperimentsBarChart({
4445
isPublicView,
4546
experimentsReportData,
4647
onExperimentClick,
48+
selectedExperimentId,
4749
localLoading = false,
4850
projectName,
4951
}: ExperimentsBarChartProps) {
@@ -52,8 +54,13 @@ export default function ExperimentsBarChart({
5254

5355
const handleBarClick = (data: ExperimentReport, index: number) => {
5456
onExperimentClick(data.experiment_id);
55-
setSelectedBar(index);
5657
};
58+
useEffect(() => {
59+
const highlightedBar = experimentsReportData.findIndex(
60+
(experiment) => experiment.experiment_id === selectedExperimentId,
61+
);
62+
setSelectedBar(highlightedBar);
63+
}, [selectedExperimentId, experimentsReportData]);
5764

5865
const CustomBar = (props: any) => {
5966
const { fill, x, y, width, height, index } = props;

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

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { Button } from "./ui/button";
1717
import { Card, CardContent } from "./ui/card";
1818
import { Skeleton } from "./ui/skeleton";
1919
import { useRouter } from "next/navigation";
20+
import { Table, TableBody, TableHeader } from "./ui/table";
21+
import { Experiment } from "@/types/experiment";
2022

2123
export interface ProjectDashboardBaseProps {
2224
isPublicView: boolean;
@@ -26,6 +28,7 @@ export interface ProjectDashboardBaseProps {
2628
radialChartData: RadialChartData;
2729
convertedValues: ConvertedValues;
2830
experimentsReportData: ExperimentReport[];
31+
projectExperiments: Experiment[];
2932
runData: {
3033
experimentId: string;
3134
startDate: string;
@@ -47,6 +50,7 @@ export default function ProjectDashboardBase({
4750
radialChartData,
4851
convertedValues,
4952
experimentsReportData,
53+
projectExperiments,
5054
runData,
5155
selectedExperimentId,
5256
selectedRunId,
@@ -91,18 +95,6 @@ export default function ProjectDashboardBase({
9195
}
9296
/>
9397
</div>
94-
<Button
95-
onClick={handleCreateExperimentClick}
96-
className="bg-primary text-primary-foreground"
97-
>
98-
+ Add Experiment
99-
</Button>
100-
<CreateExperimentModal
101-
projectId={project.id}
102-
isOpen={isExperimentModalOpen}
103-
onClose={() => setIsExperimentModalOpen(false)}
104-
onExperimentCreated={refreshExperimentList}
105-
/>
10698
</div>
10799
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
108100
<div className="grid grid-cols-1 gap-4">
@@ -210,6 +202,74 @@ export default function ProjectDashboardBase({
210202
</div>
211203

212204
<Separator className="h-[0.5px] bg-muted-foreground my-6" />
205+
<Card className="flex flex-col md:flex-row justify-start gap-4 px-4 py-4 w-full max-w-3/4">
206+
<div className="flex items-center justify-start px-2">
207+
<p className="text-sm font-medium pr-2 text-center">
208+
{projectExperiments.length === 0
209+
? isPublicView
210+
? "No experiment data in the selected date range" // This is because for public projects we show only the experiments that have runs, but for private projects we show in this list as well the projects created but without runs yet
211+
: "No experiments have been created yet."
212+
: "Set of experiments included in this project"}
213+
</p>
214+
{!isPublicView && (
215+
<div className="flex items-center justify-center px-2">
216+
<Button
217+
onClick={handleCreateExperimentClick}
218+
className="bg-primary text-primary-foreground"
219+
>
220+
+ Add Experiment
221+
</Button>
222+
<CreateExperimentModal
223+
projectId={project.id}
224+
isOpen={isExperimentModalOpen}
225+
onClose={() => setIsExperimentModalOpen(false)}
226+
onExperimentCreated={refreshExperimentList}
227+
/>
228+
</div>
229+
)}
230+
</div>
231+
{projectExperiments.length !== 0 && (
232+
<Card className="flex flex-col md:flex-row justify-between md:items-center gap-4 py-4 px-4 w-full max-w-3/4">
233+
<Table>
234+
<TableHeader>
235+
<tr>
236+
<th className="text-left">Experiment</th>
237+
<th className="text-left">Description</th>
238+
{!isPublicView && (
239+
<th className="text-left">
240+
Experiment id
241+
</th>
242+
)}
243+
</tr>
244+
</TableHeader>
245+
<TableBody>
246+
{projectExperiments.map((experiment) => (
247+
<tr
248+
key={experiment.id}
249+
className={`cursor-pointer hover:bg-muted/50 ${
250+
experiment.id ===
251+
selectedExperimentId
252+
? "bg-primary/10"
253+
: ""
254+
}`}
255+
onClick={() =>
256+
onExperimentClick(
257+
experiment.id || "",
258+
)
259+
}
260+
>
261+
<td>{experiment.name}</td>
262+
<td>{experiment.description}</td>
263+
{!isPublicView && (
264+
<td>{experiment.id}</td>
265+
)}
266+
</tr>
267+
))}
268+
</TableBody>
269+
</Table>
270+
</Card>
271+
)}
272+
</Card>
213273
<div className="grid gap-8 md:grid-cols-2">
214274
{isLoading ? (
215275
<>
@@ -223,6 +283,7 @@ export default function ProjectDashboardBase({
223283
experimentsReportData={experimentsReportData}
224284
onExperimentClick={onExperimentClick}
225285
projectName={project.name}
286+
selectedExperimentId={selectedExperimentId}
226287
/>
227288
<RunsScatterChart
228289
isPublicView={isPublicView}
@@ -237,7 +298,7 @@ export default function ProjectDashboardBase({
237298
</>
238299
)}
239300
</div>
240-
{selectedRunId && (
301+
{selectedRunId && selectedRunId != "" && (
241302
<>
242303
<Separator className="h-[0.5px] bg-muted-foreground my-6" />
243304
<div className="w-full">

webapp/src/components/project-dashboard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default function ProjectDashboard({
2626
radialChartData,
2727
convertedValues,
2828
experimentsReportData,
29+
projectExperiments,
2930
runData,
3031
selectedExperimentId,
3132
selectedRunId,
@@ -185,6 +186,7 @@ export default function ProjectDashboard({
185186
selectedRunId={selectedRunId}
186187
onExperimentClick={onExperimentClick}
187188
onRunClick={onRunClick}
189+
projectExperiments={projectExperiments}
188190
headerContent={headerContent}
189191
isLoading={isLoading}
190192
/>

0 commit comments

Comments
 (0)