Skip to content

Commit 63319ef

Browse files
[OPIK-2497] [FE] Add batch tag update UI for traces, spans and threads
1 parent 247a694 commit 63319ef

File tree

6 files changed

+271
-67
lines changed

6 files changed

+271
-67
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
import get from "lodash/get";
4+
5+
import api, { SPANS_KEY, SPANS_REST_ENDPOINT } from "@/api/api";
6+
import { Span } from "@/types/traces";
7+
import { useToast } from "@/components/ui/use-toast";
8+
9+
type UseSpanBatchUpdateMutationParams = {
10+
projectId: string;
11+
spanIds: string[];
12+
span: Partial<Span>;
13+
mergeTags?: boolean;
14+
};
15+
16+
const useSpanBatchUpdateMutation = () => {
17+
const queryClient = useQueryClient();
18+
const { toast } = useToast();
19+
20+
return useMutation({
21+
mutationFn: async ({
22+
spanIds,
23+
span,
24+
mergeTags,
25+
}: UseSpanBatchUpdateMutationParams) => {
26+
const { data } = await api.patch(SPANS_REST_ENDPOINT + "batch", {
27+
ids: spanIds,
28+
update: span,
29+
merge_tags: mergeTags,
30+
});
31+
32+
return data;
33+
},
34+
onError: (error: AxiosError) => {
35+
const message = get(
36+
error,
37+
["response", "data", "message"],
38+
error.message,
39+
);
40+
41+
toast({
42+
title: "Error",
43+
description: message,
44+
variant: "destructive",
45+
});
46+
},
47+
onSettled: (data, error, variables) => {
48+
queryClient.invalidateQueries({
49+
queryKey: [SPANS_KEY, { projectId: variables.projectId }],
50+
});
51+
},
52+
});
53+
};
54+
55+
export default useSpanBatchUpdateMutation;
56+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
import get from "lodash/get";
4+
5+
import api, { THREADS_KEY, TRACES_REST_ENDPOINT } from "@/api/api";
6+
import { useToast } from "@/components/ui/use-toast";
7+
8+
type UseThreadBatchUpdateMutationParams = {
9+
projectId: string;
10+
threadIds: string[];
11+
thread: {
12+
tags?: string[];
13+
};
14+
mergeTags?: boolean;
15+
};
16+
17+
const useThreadBatchUpdateMutation = () => {
18+
const queryClient = useQueryClient();
19+
const { toast } = useToast();
20+
21+
return useMutation({
22+
mutationFn: async ({
23+
threadIds,
24+
thread,
25+
mergeTags,
26+
}: UseThreadBatchUpdateMutationParams) => {
27+
const { data } = await api.patch(
28+
TRACES_REST_ENDPOINT + "threads/batch",
29+
{
30+
ids: threadIds,
31+
update: thread,
32+
merge_tags: mergeTags,
33+
},
34+
);
35+
36+
return data;
37+
},
38+
onError: (error: AxiosError) => {
39+
const message = get(
40+
error,
41+
["response", "data", "message"],
42+
error.message,
43+
);
44+
45+
toast({
46+
title: "Error",
47+
description: message,
48+
variant: "destructive",
49+
});
50+
},
51+
onSettled: (data, error, variables) => {
52+
queryClient.invalidateQueries({
53+
queryKey: [THREADS_KEY, { projectId: variables.projectId }],
54+
});
55+
},
56+
});
57+
};
58+
59+
export default useThreadBatchUpdateMutation;
60+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { AxiosError } from "axios";
3+
import get from "lodash/get";
4+
5+
import api, { TRACES_KEY, TRACES_REST_ENDPOINT } from "@/api/api";
6+
import { Trace } from "@/types/traces";
7+
import { useToast } from "@/components/ui/use-toast";
8+
9+
type UseTraceBatchUpdateMutationParams = {
10+
projectId: string;
11+
traceIds: string[];
12+
trace: Partial<Trace>;
13+
mergeTags?: boolean;
14+
};
15+
16+
const useTraceBatchUpdateMutation = () => {
17+
const queryClient = useQueryClient();
18+
const { toast } = useToast();
19+
20+
return useMutation({
21+
mutationFn: async ({
22+
traceIds,
23+
trace,
24+
mergeTags,
25+
}: UseTraceBatchUpdateMutationParams) => {
26+
const { data } = await api.patch(TRACES_REST_ENDPOINT + "batch", {
27+
ids: traceIds,
28+
update: trace,
29+
merge_tags: mergeTags,
30+
});
31+
32+
return data;
33+
},
34+
onError: (error: AxiosError) => {
35+
const message = get(
36+
error,
37+
["response", "data", "message"],
38+
error.message,
39+
);
40+
41+
toast({
42+
title: "Error",
43+
description: message,
44+
variant: "destructive",
45+
});
46+
},
47+
onSettled: (data, error, variables) => {
48+
queryClient.invalidateQueries({
49+
queryKey: [TRACES_KEY, { projectId: variables.projectId }],
50+
});
51+
},
52+
});
53+
};
54+
55+
export default useTraceBatchUpdateMutation;
56+

apps/opik-frontend/src/components/pages-shared/traces/AddTagDialog/AddTagDialog.tsx

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from "react";
2-
import { Trace, Span } from "@/types/traces";
2+
import { Trace, Span, Thread } from "@/types/traces";
33
import { TRACE_DATA_TYPE } from "@/hooks/useTracesOrSpansList";
44
import {
55
Dialog,
@@ -11,16 +11,23 @@ import {
1111
import { Button } from "@/components/ui/button";
1212
import { Input } from "@/components/ui/input";
1313
import { useToast } from "@/components/ui/use-toast";
14-
import useTraceUpdateMutation from "@/api/traces/useTraceUpdateMutation";
15-
import useSpanUpdateMutation from "@/api/traces/useSpanUpdateMutation";
14+
import useTraceBatchUpdateMutation from "@/api/traces/useTraceBatchUpdateMutation";
15+
import useSpanBatchUpdateMutation from "@/api/traces/useSpanBatchUpdateMutation";
16+
import useThreadBatchUpdateMutation from "@/api/traces/useThreadBatchUpdateMutation";
1617
import useAppStore from "@/store/AppStore";
1718

19+
export enum TAG_ENTITY_TYPE {
20+
traces = "traces",
21+
spans = "spans",
22+
threads = "threads",
23+
}
24+
1825
type AddTagDialogProps = {
19-
rows: Array<Trace | Span>;
26+
rows: Array<Trace | Span | Thread>;
2027
open: boolean | number;
2128
setOpen: (open: boolean | number) => void;
2229
projectId: string;
23-
type: TRACE_DATA_TYPE;
30+
type: TRACE_DATA_TYPE | TAG_ENTITY_TYPE.threads;
2431
onSuccess?: () => void;
2532
};
2633

@@ -35,9 +42,9 @@ const AddTagDialog: React.FunctionComponent<AddTagDialogProps> = ({
3542
const { toast } = useToast();
3643
const workspaceName = useAppStore((state) => state.activeWorkspaceName);
3744
const [newTag, setNewTag] = useState<string>("");
38-
const traceUpdateMutation = useTraceUpdateMutation();
39-
const spanUpdateMutation = useSpanUpdateMutation();
40-
const MAX_ENTITIES = 10;
45+
const traceBatchUpdateMutation = useTraceBatchUpdateMutation();
46+
const spanBatchUpdateMutation = useSpanBatchUpdateMutation();
47+
const threadBatchUpdateMutation = useThreadBatchUpdateMutation();
4148

4249
const handleClose = () => {
4350
setOpen(false);
@@ -47,54 +54,55 @@ const AddTagDialog: React.FunctionComponent<AddTagDialogProps> = ({
4754
const handleAddTag = () => {
4855
if (!newTag) return;
4956

50-
const promises: Promise<unknown>[] = [];
51-
52-
rows.forEach((row) => {
53-
const currentTags = row.tags || [];
54-
55-
if (currentTags.includes(newTag)) return;
56-
57-
const newTags = [...currentTags, newTag];
58-
59-
if (type === TRACE_DATA_TYPE.traces) {
60-
promises.push(
61-
traceUpdateMutation.mutateAsync({
62-
projectId,
63-
traceId: row.id,
64-
trace: {
65-
workspace_name: workspaceName,
66-
project_id: projectId,
67-
tags: newTags,
68-
},
69-
}),
70-
);
71-
} else {
72-
const span = row as Span;
73-
const parentId = span.parent_span_id;
57+
let mutationPromise;
58+
let entityName;
7459

75-
promises.push(
76-
spanUpdateMutation.mutateAsync({
77-
projectId,
78-
spanId: span.id,
79-
span: {
80-
workspace_name: workspaceName,
81-
project_id: projectId,
82-
...(parentId && { parent_span_id: parentId }),
83-
trace_id: span.trace_id,
84-
tags: newTags,
85-
},
86-
}),
87-
);
88-
}
89-
});
60+
if (type === TRACE_DATA_TYPE.traces) {
61+
const ids = rows.map((row) => row.id);
62+
mutationPromise = traceBatchUpdateMutation.mutateAsync({
63+
projectId,
64+
traceIds: ids,
65+
trace: {
66+
workspace_name: workspaceName,
67+
project_id: projectId,
68+
tags: [newTag],
69+
},
70+
mergeTags: true,
71+
});
72+
entityName = "traces";
73+
} else if (type === TRACE_DATA_TYPE.spans) {
74+
const ids = rows.map((row) => row.id);
75+
mutationPromise = spanBatchUpdateMutation.mutateAsync({
76+
projectId,
77+
spanIds: ids,
78+
span: {
79+
workspace_name: workspaceName,
80+
project_id: projectId,
81+
trace_id: (rows[0] as Span).trace_id,
82+
tags: [newTag],
83+
},
84+
mergeTags: true,
85+
});
86+
entityName = "spans";
87+
} else {
88+
// threads - use thread_model_id instead of id
89+
const threadModelIds = rows.map((row) => (row as Thread).thread_model_id);
90+
mutationPromise = threadBatchUpdateMutation.mutateAsync({
91+
projectId,
92+
threadIds: threadModelIds,
93+
thread: {
94+
tags: [newTag],
95+
},
96+
mergeTags: true,
97+
});
98+
entityName = "threads";
99+
}
90100

91-
Promise.all(promises)
101+
mutationPromise
92102
.then(() => {
93103
toast({
94104
title: "Success",
95-
description: `Tag "${newTag}" added to ${rows.length} selected ${
96-
type === TRACE_DATA_TYPE.traces ? "traces" : "spans"
97-
}`,
105+
description: `Tag "${newTag}" added to ${rows.length} selected ${entityName}`,
98106
});
99107

100108
if (onSuccess) {
@@ -104,7 +112,7 @@ const AddTagDialog: React.FunctionComponent<AddTagDialogProps> = ({
104112
handleClose();
105113
})
106114
.catch(() => {
107-
// Error handling is already done by the mutation hooks,this just ensures we don't close the dialog on error
115+
// Error handling is already done by the mutation hooks
108116
});
109117
};
110118

@@ -114,34 +122,28 @@ const AddTagDialog: React.FunctionComponent<AddTagDialogProps> = ({
114122
<DialogHeader>
115123
<DialogTitle>
116124
Add tag to {rows.length}{" "}
117-
{type === TRACE_DATA_TYPE.traces ? "traces" : "spans"}
125+
{type === TRACE_DATA_TYPE.traces
126+
? "traces"
127+
: type === TRACE_DATA_TYPE.spans
128+
? "spans"
129+
: "threads"}
118130
</DialogTitle>
119131
</DialogHeader>
120-
{rows.length > MAX_ENTITIES && (
121-
<div className="mb-2 text-sm text-destructive">
122-
You can only add tags to up to {MAX_ENTITIES} entities at a time.
123-
Please select fewer entities.
124-
</div>
125-
)}
126132
<div className="grid gap-4 py-4">
127133
<div className="flex items-center gap-4">
128134
<Input
129135
placeholder="New tag"
130136
value={newTag}
131137
onChange={(event) => setNewTag(event.target.value)}
132138
className="col-span-3"
133-
disabled={rows.length > MAX_ENTITIES}
134139
/>
135140
</div>
136141
</div>
137142
<DialogFooter>
138143
<Button variant="outline" onClick={handleClose}>
139144
Cancel
140145
</Button>
141-
<Button
142-
onClick={handleAddTag}
143-
disabled={!newTag || rows.length > MAX_ENTITIES}
144-
>
146+
<Button onClick={handleAddTag} disabled={!newTag}>
145147
Add tag
146148
</Button>
147149
</DialogFooter>

0 commit comments

Comments
 (0)