From c8511f318e8643406a526fa7f3922053feb953f4 Mon Sep 17 00:00:00 2001 From: Nidhi Singh <120259299+nidhi-wa@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:09:55 +0530 Subject: [PATCH] Add ability to delete traces (#314) * Add ability to delete individual traces * improve delete traces and span functionality * update route handlers --- .../api/projects/[projectId]/spans/route.ts | 32 ++++++++++++ .../api/projects/[projectId]/traces/route.ts | 33 ++++++++++++ frontend/components/dataset/dataset.tsx | 48 +++-------------- frontend/components/datasets/datasets.tsx | 46 +++------------- .../components/evaluations/evaluations.tsx | 47 +++-------------- frontend/components/traces/spans-table.tsx | 50 ++++++++++++++++++ frontend/components/traces/trace-view.tsx | 2 +- frontend/components/traces/traces-table.tsx | 49 +++++++++++++++++ frontend/components/ui/DeleteSelectedRows.tsx | 52 +++++++++++++++++++ 9 files changed, 235 insertions(+), 124 deletions(-) create mode 100644 frontend/components/ui/DeleteSelectedRows.tsx diff --git a/frontend/app/api/projects/[projectId]/spans/route.ts b/frontend/app/api/projects/[projectId]/spans/route.ts index 9d39cec0..178dd837 100644 --- a/frontend/app/api/projects/[projectId]/spans/route.ts +++ b/frontend/app/api/projects/[projectId]/spans/route.ts @@ -128,3 +128,35 @@ export async function GET(req: NextRequest, props: { params: Promise<{ projectId return new Response(JSON.stringify(spanData), { status: 200 }); } + + +export async function DELETE( + req: NextRequest, + props: { params: Promise<{ projectId: string; spanId: string }> } +): Promise { + const params = await props.params; + const projectId = params.projectId; + + const { searchParams } = new URL(req.url); + const spanId = searchParams.get('spanId')?.split(','); + + if (!spanId) { + return new Response('At least one Span ID is required', { status: 400 }); + } + + try { + await db.delete(spans) + .where( + and( + inArray(spans.spanId, spanId), + eq(spans.projectId, projectId) + ) + ); + + return new Response('Spans deleted successfully', { status: 200 }); + } catch (error) { + return new Response('Error deleting spans', { status: 500 }); + } +} + + diff --git a/frontend/app/api/projects/[projectId]/traces/route.ts b/frontend/app/api/projects/[projectId]/traces/route.ts index 62412bc2..ae058d48 100644 --- a/frontend/app/api/projects/[projectId]/traces/route.ts +++ b/frontend/app/api/projects/[projectId]/traces/route.ts @@ -1,7 +1,10 @@ +import { and, eq, inArray} from 'drizzle-orm'; import { NextRequest } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; +import { db } from '@/lib/db/drizzle'; +import { traces } from '@/lib/db/migrations/schema'; import { fetcher } from '@/lib/utils'; export async function GET(req: NextRequest, props: { params: Promise<{ projectId: string }> }): Promise { @@ -21,3 +24,33 @@ export async function GET(req: NextRequest, props: { params: Promise<{ projectId } ); } + + +export async function DELETE( + req: NextRequest, + props: { params: Promise<{ projectId: string; traceId: string }> } +): Promise { + const params = await props.params; + const projectId = params.projectId; + + const { searchParams } = new URL(req.url); + const traceId = searchParams.get('traceId')?.split(','); + + if (!traceId) { + return new Response('At least one Trace ID is required', { status: 400 }); + } + + try { + await db.delete(traces) + .where( + and( + inArray(traces.id, traceId), + eq(traces.projectId, projectId) + ) + ); + + return new Response('Traces deleted successfully', { status: 200 }); + } catch (error) { + return new Response('Error deleting traces', { status: 500 }); + } +} diff --git a/frontend/components/dataset/dataset.tsx b/frontend/components/dataset/dataset.tsx index 6452f996..929f7871 100644 --- a/frontend/components/dataset/dataset.tsx +++ b/frontend/components/dataset/dataset.tsx @@ -1,14 +1,13 @@ 'use client'; import { ColumnDef } from '@tanstack/react-table'; -import { Loader2, Trash2 } from 'lucide-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { Resizable } from 're-resizable'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; -import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/ui/datatable'; +import DeleteSelectedRows from '@/components/ui/DeleteSelectedRows'; import { useProjectContext } from '@/contexts/project-context'; import { Datapoint, Dataset as DatasetType } from '@/lib/dataset/types'; import { useToast } from '@/lib/hooks/use-toast'; @@ -16,15 +15,6 @@ import { PaginatedResponse } from '@/lib/types'; import { swrFetcher } from '@/lib/utils'; import ClientTimestampFormatter from '../client-timestamp-formatter'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger -} from '../ui/dialog'; import DownloadButton from '../ui/download-button'; import Header from '../ui/header'; import MonoWithCopy from '../ui/mono-with-copy'; @@ -44,8 +34,6 @@ export default function Dataset({ dataset }: DatasetProps) { const { projectId } = useProjectContext(); const { toast } = useToast(); const [datapoints, setDatapoints] = useState(undefined); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); // Get datapointId from URL params const datapointId = searchParams.get('datapointId'); @@ -115,7 +103,6 @@ export default function Dataset({ dataset }: DatasetProps) { ]; const handleDeleteDatapoints = async (datapointIds: string[]) => { - setIsDeleting(true); const response = await fetch( `/api/projects/${projectId}/datasets/${dataset.id}/datapoints` + `?datapointIds=${datapointIds.join(',')}` + @@ -143,9 +130,6 @@ export default function Dataset({ dataset }: DatasetProps) { if (selectedDatapoint && datapointIds.includes(selectedDatapoint.id)) { handleDatapointSelect(null); } - - setIsDeleting(false); - setIsDeleteDialogOpen(false); }; // Update URL when datapoint is selected @@ -218,31 +202,11 @@ export default function Dataset({ dataset }: DatasetProps) { enableRowSelection selectionPanel={(selectedRowIds) => (
- - - - - - - Delete Datapoints - - Are you sure you want to delete - {selectedRowIds.length} datapoint(s)? This action cannot be undone. - - - - - - - - +
)} /> diff --git a/frontend/components/datasets/datasets.tsx b/frontend/components/datasets/datasets.tsx index d6c87241..cd67b092 100644 --- a/frontend/components/datasets/datasets.tsx +++ b/frontend/components/datasets/datasets.tsx @@ -1,20 +1,10 @@ 'use client'; import { ColumnDef } from '@tanstack/react-table'; -import { Loader2, Trash2 } from 'lucide-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger -} from '@/components/ui/dialog'; +import DeleteSelectedRows from '@/components/ui/DeleteSelectedRows'; import { useProjectContext } from '@/contexts/project-context'; import { DatasetInfo } from '@/lib/dataset/types'; import { useToast } from '@/lib/hooks/use-toast'; @@ -63,12 +53,9 @@ export default function Datasets() { const pageCount = Math.ceil(totalCount / pageSize); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); const { toast } = useToast(); const handleDeleteDatasets = async (datasetIds: string[]) => { - setIsDeleting(true); try { const res = await fetch( `/api/projects/${projectId}/datasets?datasetIds=${datasetIds.join(',')}`, @@ -93,8 +80,6 @@ export default function Datasets() { variant: 'destructive', }); } - setIsDeleting(false); - setIsDeleteDialogOpen(false); }; const columns: ColumnDef[] = [ @@ -153,30 +138,11 @@ export default function Datasets() { totalItemsCount={totalCount} selectionPanel={(selectedRowIds) => (
- - - - - - - Delete Datasets - - Are you sure you want to delete {selectedRowIds.length} dataset(s)? This action cannot be undone. - - - - - - - - +
)} emptyRow={ diff --git a/frontend/components/evaluations/evaluations.tsx b/frontend/components/evaluations/evaluations.tsx index c46a10e4..16780d47 100644 --- a/frontend/components/evaluations/evaluations.tsx +++ b/frontend/components/evaluations/evaluations.tsx @@ -1,12 +1,12 @@ 'use client'; import { ColumnDef } from '@tanstack/react-table'; -import { Loader2, Trash2 } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; import { usePostHog } from 'posthog-js/react'; import { useState } from 'react'; import useSWR from 'swr'; +import DeleteSelectedRows from '@/components/ui/DeleteSelectedRows'; import { useProjectContext } from '@/contexts/project-context'; import { useUserContext } from '@/contexts/user-context'; import { AggregationFunction } from '@/lib/clickhouse/utils'; @@ -17,17 +17,7 @@ import { PaginatedResponse } from '@/lib/types'; import { swrFetcher } from '@/lib/utils'; import ClientTimestampFormatter from '../client-timestamp-formatter'; -import { Button } from '../ui/button'; import { DataTable } from '../ui/datatable'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '../ui/dialog'; import Header from '../ui/header'; import Mono from '../ui/mono'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable'; @@ -82,12 +72,9 @@ export default function Evaluations() { } ]; - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); const { toast } = useToast(); const handleDeleteEvaluations = async (selectedRowIds: string[]) => { - setIsDeleting(true); try { const response = await fetch( `/api/projects/${projectId}/evaluations?evaluationIds=${selectedRowIds.join(',')}`, @@ -116,8 +103,6 @@ export default function Evaluations() { variant: 'destructive', }); } - setIsDeleting(false); - setIsDeleteDialogOpen(false); }; return ( @@ -166,31 +151,11 @@ export default function Evaluations() { manualPagination selectionPanel={(selectedRowIds) => (
- - - - - - - Delete Evaluations - - Are you sure you want to delete {selectedRowIds.length} evaluation(s)? - This action cannot be undone. - - - - - - - - +
)} /> diff --git a/frontend/components/traces/spans-table.tsx b/frontend/components/traces/spans-table.tsx index 67066dc7..5dbefe03 100644 --- a/frontend/components/traces/spans-table.tsx +++ b/frontend/components/traces/spans-table.tsx @@ -3,10 +3,14 @@ import { ColumnDef } from '@tanstack/react-table'; import { ArrowRight, RefreshCcw } from 'lucide-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; +import useSWR from 'swr'; +import DeleteSelectedRows from '@/components/ui/DeleteSelectedRows'; import { useProjectContext } from '@/contexts/project-context'; +import { useToast } from '@/lib/hooks/use-toast'; import { Span } from '@/lib/traces/types'; import { PaginatedResponse } from '@/lib/types'; +import { swrFetcher } from '@/lib/utils'; import ClientTimestampFormatter from '../client-timestamp-formatter'; import { Button } from '../ui/button'; @@ -39,6 +43,7 @@ export default function SpansTable({ onRowClick }: SpansTableProps) { const searchParams = new URLSearchParams(useSearchParams().toString()); const pathName = usePathname(); const router = useRouter(); + const { toast } = useToast(); const pageNumber = searchParams.get('pageNumber') ? parseInt(searchParams.get('pageNumber')!) : 0; @@ -104,6 +109,19 @@ export default function SpansTable({ onRowClick }: SpansTableProps) { setSpans(data.items); setTotalCount(data.totalCount); }; + + const { data, mutate } = useSWR>( + `/api/projects/${projectId}/spans?pageNumber=${pageNumber}&pageSize=${pageSize}`, + swrFetcher + ); + + useEffect(() => { + if (data) { + setSpans(data.items); + setTotalCount(data.totalCount); + } + }, [data]); + useEffect(() => { getSpans(); }, [ @@ -117,6 +135,29 @@ export default function SpansTable({ onRowClick }: SpansTableProps) { textSearchFilter ]); + const handleDeleteSpans = async (spanId: string[]) => { + const response = await fetch( + `/api/projects/${projectId}/spans?spanId=${spanId.join(',')}`, + { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + } + ); + + if (!response.ok) { + toast({ + title: 'Failed to delete Span', + variant: 'destructive' + }); + } else { + toast({ + title: 'Span deleted', + description: `Successfully deleted ${spanId.length} Span(s).` + }); + mutate(); + } + }; + const handleRowClick = (row: Span) => { searchParams.set('traceId', row.traceId!); searchParams.set('spanId', row.spanId); @@ -371,6 +412,15 @@ export default function SpansTable({ onRowClick }: SpansTableProps) { }} totalItemsCount={totalCount} enableRowSelection + selectionPanel={(selectedRowIds) => ( +
+ +
+ )} > void; } -export default function TraceView({ traceId, onClose }: TraceViewProps) { +export default function TraceView({ traceId, onClose}: TraceViewProps) { const searchParams = new URLSearchParams(useSearchParams().toString()); const router = useRouter(); const pathName = usePathname(); diff --git a/frontend/components/traces/traces-table.tsx b/frontend/components/traces/traces-table.tsx index eb37b07b..f3f6288f 100644 --- a/frontend/components/traces/traces-table.tsx +++ b/frontend/components/traces/traces-table.tsx @@ -3,13 +3,17 @@ import { ColumnDef } from '@tanstack/react-table'; import { ArrowRight, RefreshCcw } from 'lucide-react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; +import useSWR from 'swr'; +import DeleteSelectedRows from '@/components/ui/DeleteSelectedRows'; import { useProjectContext } from '@/contexts/project-context'; import { useUserContext } from '@/contexts/user-context'; import { SUPABASE_ANON_KEY, SUPABASE_URL } from '@/lib/const'; import { Feature, isFeatureEnabled } from '@/lib/features/features'; +import { useToast } from '@/lib/hooks/use-toast'; import { Trace } from '@/lib/traces/types'; import { PaginatedGetResponseWithProjectPresenceFlag } from '@/lib/types'; +import { swrFetcher } from '@/lib/utils'; import ClientTimestampFormatter from '../client-timestamp-formatter'; import { Button } from '../ui/button'; @@ -42,6 +46,7 @@ export default function TracesTable({ onRowClick }: TracesTableProps) { const searchParams = new URLSearchParams(useSearchParams().toString()); const pathName = usePathname(); const router = useRouter(); + const { toast } = useToast(); const pageNumber = searchParams.get('pageNumber') ? parseInt(searchParams.get('pageNumber')!) : 0; @@ -116,6 +121,18 @@ export default function TracesTable({ onRowClick }: TracesTableProps) { setAnyInProject(data.anyInProject); }; + const { data, mutate } = useSWR< PaginatedGetResponseWithProjectPresenceFlag>( + `/api/projects/${projectId}/traces?pageNumber=${pageNumber}&pageSize=${pageSize}`, + swrFetcher + ); + + useEffect(() => { + if (data) { + setTraces(data.items); + setTotalCount(data.totalCount); + } + }, [data]); + useEffect(() => { getTraces(); }, [ @@ -129,6 +146,29 @@ export default function TracesTable({ onRowClick }: TracesTableProps) { textSearchFilter ]); + const handleDeleteTraces = async (traceId: string[]) => { + const response = await fetch( + `/api/projects/${projectId}/traces?traceId=${traceId.join(',')}`, + { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + } + ); + + if (!response.ok) { + toast({ + title: 'Failed to delete traces', + variant: 'destructive' + }); + } else { + toast({ + title: 'Traces deleted', + description: `Successfully deleted ${traceId.length} trace(s).` + }); + mutate(); + } + }; + const handleRowClick = (row: Trace) => { searchParams.set('traceId', row.id!); searchParams.delete('spanId'); @@ -438,6 +478,15 @@ export default function TracesTable({ onRowClick }: TracesTableProps) { }} totalItemsCount={totalCount} enableRowSelection + selectionPanel={(selectedRowIds) => ( +
+ +
+ )} > Promise; + entityName?: string; +} + +export default function DeleteSelectedRows({ selectedRowIds, onDelete, entityName = 'datapoints', }: DeleteSelectedRowsProps) { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + setIsDeleting(true); + await onDelete(selectedRowIds); + setIsDeleting(false); + setIsDeleteDialogOpen(false); + }; + + return ( + + + + + + + Delete {entityName} + + Are you sure you want to delete {selectedRowIds.length} {entityName}? This action cannot be undone. + + + + + + + + + ); +}