Skip to content

Commit 572979b

Browse files
authored
[OPIK-3796] [FE] Dashboard Defaults - Ability to Auto-Select Latest Experiment(s) (#4786)
* [OPIK-3796] Dashboard Defaults - Ability to Auto-Select Latest Experiment(s) * [OPIK-3796] [FE] Fix defensive copying for experimentFilters array * [OPIK-3796] [FE] Update UI text: remove $ symbol and improve filters description * [OPIK-3796] [FE] Extract getFormDefaults helper to eliminate duplication * - fix comment * [OPIK-3796] [FE] Add isValidIntegerInRange utility and simplify validation schemas * [OPIK-3796] [FE] Set experimentDataSource in runtime config for compare page
1 parent 55eac02 commit 572979b

File tree

22 files changed

+1274
-383
lines changed

22 files changed

+1274
-383
lines changed

apps/opik-frontend/src/components/pages-shared/dashboards/AddEditCloneDashboardDialog/AddEditCloneDashboardDialog.tsx

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { useCallback, useState } from "react";
1+
import React, { useCallback, useState, useEffect } from "react";
22
import { flushSync } from "react-dom";
33
import { AxiosError, HttpStatusCode } from "axios";
44
import { useForm } from "react-hook-form";
55
import { zodResolver } from "@hookform/resolvers/zod";
66
import { z } from "zod";
77
import get from "lodash/get";
8+
import isEmpty from "lodash/isEmpty";
89
import { Loader2, ChevronLeft } from "lucide-react";
910
import { Button } from "@/components/ui/button";
1011
import {
@@ -18,17 +19,28 @@ import {
1819
} from "@/components/ui/dialog";
1920
import { Form } from "@/components/ui/form";
2021
import useDashboardCreateMutation from "@/api/dashboards/useDashboardCreateMutation";
21-
import { Dashboard, TEMPLATE_TYPE } from "@/types/dashboard";
22+
import {
23+
Dashboard,
24+
TEMPLATE_TYPE,
25+
EXPERIMENT_DATA_SOURCE,
26+
} from "@/types/dashboard";
2227
import useDashboardUpdateMutation from "@/api/dashboards/useDashboardUpdateMutation";
28+
import { Filters } from "@/types/filters";
29+
import { FiltersArraySchema } from "@/components/shared/FiltersAccordionSection/schema";
2330
import { useNavigate } from "@tanstack/react-router";
2431
import useAppStore from "@/store/AppStore";
2532
import {
2633
generateEmptyDashboard,
2734
regenerateAllIds,
35+
MIN_MAX_EXPERIMENTS,
36+
MAX_MAX_EXPERIMENTS,
37+
DEFAULT_MAX_EXPERIMENTS,
38+
isValidIntegerInRange,
2839
} from "@/lib/dashboard/utils";
2940
import { useToast } from "@/components/ui/use-toast";
3041
import { DASHBOARD_TEMPLATES } from "@/lib/dashboard/templates";
3142
import { DialogAutoScrollBody } from "@/components/ui/dialog";
43+
import useProjectsList from "@/api/projects/useProjectsList";
3244
import DashboardDialogSelectStep from "./DashboardDialogSelectStep";
3345
import DashboardDialogDetailsStep from "./DashboardDialogDetailsStep";
3446

@@ -37,21 +49,43 @@ enum DialogStep {
3749
DETAILS = "details",
3850
}
3951

40-
const DashboardFormSchema = z.object({
41-
name: z
42-
.string()
43-
.min(1, "Name is required")
44-
.max(100, "Name must be less than 100 characters")
45-
.trim(),
46-
description: z
47-
.string()
48-
.max(255, "Description must be less than 255 characters")
49-
.optional()
50-
.or(z.literal("")),
51-
projectId: z.string().optional(),
52-
experimentIds: z.array(z.string()).optional(),
53-
templateType: z.string().optional(),
54-
});
52+
const DashboardFormSchema = z
53+
.object({
54+
name: z
55+
.string()
56+
.min(1, "Name is required")
57+
.max(100, "Name must be less than 100 characters")
58+
.trim(),
59+
description: z
60+
.string()
61+
.max(255, "Description must be less than 255 characters")
62+
.optional()
63+
.or(z.literal("")),
64+
projectId: z.string().optional(),
65+
experimentIds: z.array(z.string()).optional(),
66+
templateType: z.string().optional(),
67+
experimentDataSource: z.nativeEnum(EXPERIMENT_DATA_SOURCE).optional(),
68+
experimentFilters: FiltersArraySchema.optional(),
69+
maxExperimentsCount: z.string().optional(),
70+
})
71+
.refine(
72+
(data) => {
73+
if (
74+
data.experimentDataSource === EXPERIMENT_DATA_SOURCE.FILTER_AND_GROUP
75+
) {
76+
return isValidIntegerInRange(
77+
data.maxExperimentsCount || "",
78+
MIN_MAX_EXPERIMENTS,
79+
MAX_MAX_EXPERIMENTS,
80+
);
81+
}
82+
return true;
83+
},
84+
{
85+
message: `Max experiments to load is required and must be between ${MIN_MAX_EXPERIMENTS} and ${MAX_MAX_EXPERIMENTS}`,
86+
path: ["maxExperimentsCount"],
87+
},
88+
);
5589

5690
type DashboardFormData = z.infer<typeof DashboardFormSchema>;
5791

@@ -119,9 +153,44 @@ const AddEditCloneDashboardDialog: React.FC<
119153
projectId: defaultProjectId,
120154
experimentIds: defaultExperimentIds || [],
121155
templateType: undefined,
156+
experimentDataSource: EXPERIMENT_DATA_SOURCE.FILTER_AND_GROUP,
157+
experimentFilters: [],
158+
maxExperimentsCount: String(DEFAULT_MAX_EXPERIMENTS),
122159
},
123160
});
124161

162+
const shouldFetchLastModifiedProject =
163+
!defaultProjectId && open && isCreateMode;
164+
165+
const { data: projectsData } = useProjectsList(
166+
{
167+
workspaceName,
168+
sorting: [
169+
{
170+
desc: true,
171+
id: "last_updated_trace_at",
172+
},
173+
],
174+
page: 1,
175+
size: 1,
176+
},
177+
{
178+
enabled: shouldFetchLastModifiedProject,
179+
},
180+
);
181+
182+
const lastModifiedProject = projectsData?.content?.[0];
183+
184+
useEffect(() => {
185+
if (
186+
shouldFetchLastModifiedProject &&
187+
lastModifiedProject?.id &&
188+
isEmpty(form.getValues("projectId"))
189+
) {
190+
form.setValue("projectId", lastModifiedProject.id);
191+
}
192+
}, [shouldFetchLastModifiedProject, lastModifiedProject?.id, form]);
193+
125194
const handleSelectOption = (templateType: string) => {
126195
setCurrentStep(DialogStep.DETAILS);
127196
form.setValue("templateType", templateType, {
@@ -249,6 +318,15 @@ const AddEditCloneDashboardDialog: React.FC<
249318
? [values.projectId]
250319
: [];
251320
dashboardConfig.config.experimentIds = values.experimentIds || [];
321+
dashboardConfig.config.experimentDataSource =
322+
values.experimentDataSource ||
323+
EXPERIMENT_DATA_SOURCE.FILTER_AND_GROUP;
324+
dashboardConfig.config.experimentFilters =
325+
(values.experimentFilters as Filters) || [];
326+
dashboardConfig.config.maxExperimentsCount =
327+
values.maxExperimentsCount
328+
? parseInt(values.maxExperimentsCount, 10)
329+
: DEFAULT_MAX_EXPERIMENTS;
252330
} else if (mode === "save_as" || mode === "clone") {
253331
dashboardConfig = regenerateAllIds(dashboard!.config);
254332
dashboardConfig.config.projectIds = values.projectId

apps/opik-frontend/src/components/pages-shared/dashboards/AddEditCloneDashboardDialog/DashboardDialogDetailsStep.tsx

Lines changed: 12 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
FormLabel,
88
FormMessage,
99
} from "@/components/ui/form";
10-
import { Description } from "@/components/ui/description";
1110
import { Input } from "@/components/ui/input";
1211
import { Textarea } from "@/components/ui/textarea";
1312
import {
@@ -16,8 +15,7 @@ import {
1615
AccordionItem,
1716
AccordionTrigger,
1817
} from "@/components/ui/accordion";
19-
import ProjectsSelectBox from "@/components/pages-shared/automations/ProjectsSelectBox";
20-
import ExperimentsSelectBox from "@/components/pages-shared/experiments/ExperimentsSelectBox/ExperimentsSelectBox";
18+
import DashboardDataSourceSection from "@/components/pages-shared/dashboards/DashboardDataSourceSection/DashboardDataSourceSection";
2119
import { Control } from "react-hook-form";
2220
import { cn } from "@/lib/utils";
2321
import { DASHBOARD_TEMPLATES } from "@/lib/dashboard/templates";
@@ -27,8 +25,6 @@ import DashboardTemplateCard from "./DashboardTemplateCard";
2725
interface DashboardFormFields {
2826
name: string;
2927
description?: string;
30-
projectId?: string;
31-
experimentIds?: string[];
3228
templateType?: string;
3329
}
3430

@@ -120,79 +116,18 @@ const DashboardDialogDetailsStep: React.FunctionComponent<
120116
/>
121117
</AccordionContent>
122118
</AccordionItem>
123-
</Accordion>
124-
125-
{showDataSourceSection && (
126-
<div className="flex flex-col gap-4">
127-
<div className="flex flex-col gap-1">
128-
<h4 className="comet-body-accented">Default data source</h4>
129-
<Description>
130-
Choose default project and experiments to preview data in this
131-
dashboard. Individual widgets can override these settings if
132-
needed.
133-
</Description>
134-
</div>
135-
136-
<FormField
137-
control={control}
138-
name="experimentIds"
139-
render={({ field, formState }) => {
140-
const validationErrors = get(formState.errors, ["experimentIds"]);
141-
142-
return (
143-
<FormItem>
144-
<FormLabel>Default experiments (optional)</FormLabel>
145-
<FormControl>
146-
<ExperimentsSelectBox
147-
value={field.value || []}
148-
onValueChange={field.onChange}
149-
multiselect
150-
showClearButton
151-
className={cn("flex-1", {
152-
"border-destructive": Boolean(
153-
validationErrors?.message,
154-
),
155-
})}
156-
/>
157-
</FormControl>
158-
<Description>
159-
Used by widgets that show experiment data.
160-
</Description>
161-
<FormMessage />
162-
</FormItem>
163-
);
164-
}}
165-
/>
166119

167-
<FormField
168-
control={control}
169-
name="projectId"
170-
render={({ field, formState }) => {
171-
const validationErrors = get(formState.errors, ["projectId"]);
172-
173-
return (
174-
<FormItem>
175-
<FormLabel>Default project (optional)</FormLabel>
176-
<FormControl>
177-
<ProjectsSelectBox
178-
value={field.value || ""}
179-
onValueChange={field.onChange}
180-
showClearButton
181-
className={cn("flex-1", {
182-
"border-destructive": Boolean(
183-
validationErrors?.message,
184-
),
185-
})}
186-
/>
187-
</FormControl>
188-
<Description>Used to preview data by default.</Description>
189-
<FormMessage />
190-
</FormItem>
191-
);
192-
}}
193-
/>
194-
</div>
195-
)}
120+
{showDataSourceSection && (
121+
<AccordionItem value="additional-settings">
122+
<AccordionTrigger className="h-11 py-1.5">
123+
Dashboard defaults
124+
</AccordionTrigger>
125+
<AccordionContent className="px-3 pb-3">
126+
<DashboardDataSourceSection />
127+
</AccordionContent>
128+
</AccordionItem>
129+
)}
130+
</Accordion>
196131
</div>
197132
);
198133
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { useState } from "react";
2+
import { Settings } from "lucide-react";
3+
import { Button } from "@/components/ui/button";
4+
import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper";
5+
import DashboardConfigDialog from "./DashboardConfigDialog";
6+
7+
interface DashboardConfigButtonProps {
8+
disableProjectSelector?: boolean;
9+
disableExperimentsSelector?: boolean;
10+
}
11+
12+
const DashboardConfigButton: React.FC<DashboardConfigButtonProps> = ({
13+
disableProjectSelector = false,
14+
disableExperimentsSelector = false,
15+
}) => {
16+
const [open, setOpen] = useState(false);
17+
18+
return (
19+
<>
20+
<TooltipWrapper content="Dashboard defaults">
21+
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
22+
<Settings className="mr-1.5 size-3.5" />
23+
Defaults
24+
</Button>
25+
</TooltipWrapper>
26+
<DashboardConfigDialog
27+
open={open}
28+
onOpenChange={setOpen}
29+
disableProjectSelector={disableProjectSelector}
30+
disableExperimentsSelector={disableExperimentsSelector}
31+
/>
32+
</>
33+
);
34+
};
35+
36+
export default DashboardConfigButton;

0 commit comments

Comments
 (0)