import { DataTable } from "@/src/components/table/data-table"; import TableLink from "@/src/components/table/table-link"; import { type LangfuseColumnDef } from "@/src/components/table/types"; import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context"; import { api } from "@/src/utils/api"; import { formatIntervalSeconds } from "@/src/utils/dates"; import { useQueryParams, withDefault, NumberParam } from "use-query-params"; import { type RouterOutput } from "@/src/utils/types"; import { useEffect, useMemo, useState } from "react"; import { usdFormatter } from "../../../utils/numbers"; import { DataTableToolbar } from "@/src/components/table/data-table-toolbar"; import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility"; import { type Prisma, datasetRunsTableColsWithOptions } from "@langfuse/shared"; import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState"; import { useDebounce } from "@/src/hooks/useDebounce"; import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch"; import { IOTableCell } from "@/src/components/ui/IOTableCell"; import { type ScoreAggregate } from "@langfuse/shared"; import { ChevronDown, Columns3, MoreVertical, Trash } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; import { Button } from "@/src/components/ui/button"; import { DeleteDatasetRunButton } from "@/src/features/datasets/components/DeleteDatasetRunButton"; import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder"; import { Checkbox } from "@/src/components/ui/checkbox"; import { type RowSelectionState } from "@tanstack/react-table"; import Link from "next/link"; import { joinTableCoreAndMetrics } from "@/src/components/table/utils/joinTableCoreAndMetrics"; import { Skeleton } from "@/src/components/ui/skeleton"; import { RESOURCE_METRICS, transformAggregatedRunMetricsToChartData, } from "@/src/features/dashboard/lib/score-analytics-utils"; import { TimeseriesChart } from "@/src/features/scores/components/TimeseriesChart"; import { CompareViewAdapter } from "@/src/features/scores/adapters"; import { isNumericDataType } from "@/src/features/scores/lib/helpers"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { LocalIsoDate } from "@/src/components/LocalIsoDate"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/src/components/ui/dialog"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/src/components/ui/resizable"; import useSessionStorage from "@/src/components/useSessionStorage"; import { useScoreColumns } from "@/src/features/scores/hooks/useScoreColumns"; import { scoreFilters, addPrefixToScoreKeys, convertScoreColumnsToAnalyticsData, } from "@/src/features/scores/lib/scoreColumns"; import { getScoreLabelFromKey } from "@/src/features/scores/lib/aggregateScores"; import { NoDataOrLoading } from "@/src/components/NoDataOrLoading"; export type DatasetRunRowData = { id: string; name: string; createdAt: Date; countRunItems: string; avgLatency: number | undefined; avgTotalCost: string | undefined; totalCost: string | undefined; // scores holds grouped column with individual scores runItemScores?: ScoreAggregate | undefined; runScores?: ScoreAggregate | undefined; description: string; metadata: Prisma.JsonValue; }; const DatasetRunTableMultiSelectAction = ({ selectedRunIds, projectId, datasetId, setRowSelection, }: { selectedRunIds: string[]; projectId: string; datasetId: string; setRowSelection: (value: Record) => void; }) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const capture = usePostHogClientCapture(); const utils = api.useUtils(); const mutDelete = api.datasets.deleteDatasetRuns.useMutation({ onSuccess: () => { utils.datasets.invalidate(); setRowSelection({}); }, }); return ( <> Compare setIsDeleteDialogOpen(true)} > Delete { if (!mutDelete.isPending) { setIsDeleteDialogOpen(isOpen); } }} > Please confirm This action cannot be undone and removes all the data associated with {selectedRunIds.length} dataset run {selectedRunIds.length > 1 ? "s" : ""}. ); }; export function DatasetRunsTable(props: { projectId: string; datasetId: string; selectedMetrics: string[]; setScoreOptions: (options: { key: string; value: string }[]) => void; }) { const [paginationState, setPaginationState] = useQueryParams({ pageIndex: withDefault(NumberParam, 0), pageSize: withDefault(NumberParam, 50), }); const [selectedRows, setSelectedRows] = useState({}); const [userFilterState, setUserFilterState] = useQueryFilterState( [], "dataset_runs", props.projectId, ); const [rowHeight, setRowHeight] = useRowHeightLocalStorage( "datasetRuns", "s", ); // Add panel size state with default size of 30% const [chartsPanelSize, setChartsPanelSize] = useSessionStorage( "dataset-runs-charts-panel-size", 30, ); const { setScoreOptions } = props; // Filter options for the table const datasetRunsFilterOptionsResponse = api.datasets.runFilterOptions.useQuery( { projectId: props.projectId, datasetId: props.datasetId }, { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const datasetRunsFilterOptions = datasetRunsFilterOptionsResponse.data; const transformedFilterOptions = useMemo(() => { return datasetRunsTableColsWithOptions(datasetRunsFilterOptions); }, [datasetRunsFilterOptions]); const setFilterState = useDebounce(setUserFilterState); const runs = api.datasets.runsByDatasetId.useQuery({ projectId: props.projectId, datasetId: props.datasetId, page: paginationState.pageIndex, limit: paginationState.pageSize, filter: userFilterState, }); const runsMetrics = api.datasets.runsByDatasetIdMetrics.useQuery( { projectId: props.projectId, datasetId: props.datasetId, runIds: runs.data?.runs.map((r) => r.id) ?? [], filter: userFilterState, }, { enabled: runs.isSuccess, }, ); type DatasetsCoreOutput = RouterOutput["datasets"]["runsByDatasetId"]["runs"][number]; type DatasetsMetricOutput = RouterOutput["datasets"]["runsByDatasetIdMetrics"]["runs"][number]; const runsWithMetrics = joinTableCoreAndMetrics< DatasetsCoreOutput, DatasetsMetricOutput >(runs.data?.runs, runsMetrics.data?.runs); const { setDetailPageList } = useDetailPageLists(); useEffect(() => { if (runs.isSuccess) { setDetailPageList( "datasetRuns", runs.data.runs.map((t) => ({ id: t.id })), ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [runs.isSuccess, runs.data]); const { scoreColumns, isLoading: isColumnLoading } = useScoreColumns({ displayFormat: "aggregate", scoreColumnKey: "runItemScores", projectId: props.projectId, filter: runs.data?.runs?.length ? scoreFilters.forDatasetRunItems({ datasetRunIds: runs.data.runs.map((r) => r.id), datasetId: props.datasetId, }) : [], isFilterDataPending: runs.isPending, }); const { scoreColumns: runScoreColumns, isLoading: isRunScoreColumnLoading } = useScoreColumns({ scoreColumnKey: "runScores", projectId: props.projectId, filter: runs.data?.runs?.length ? scoreFilters.forDatasetRuns({ datasetRunIds: runs.data?.runs.map((r) => r.id), }) : [], prefix: "Run-level", isFilterDataPending: runs.isPending, }); const scoreKeysAndProps = api.scores.getScoreColumns.useQuery( { projectId: props.projectId, filter: runs.data?.runs?.length ? scoreFilters.forDatasetRunItems({ datasetRunIds: runs.data?.runs.map((r) => r.id), datasetId: props.datasetId, }) : [], }, { enabled: runs.isSuccess, }, ); const scoreIdToName = useMemo(() => { return new Map( scoreKeysAndProps.data?.scoreColumns.map((obj) => [obj.key, obj.name]) ?? [], ); }, [scoreKeysAndProps.data?.scoreColumns]); const runAggregatedMetrics = useMemo(() => { return transformAggregatedRunMetricsToChartData( runsMetrics.data?.runs ?? [], scoreIdToName, ); }, [runsMetrics.data, scoreIdToName]); const { scoreAnalyticsOptions, scoreKeyToData } = useMemo( () => convertScoreColumnsToAnalyticsData(scoreKeysAndProps.data?.scoreColumns), [scoreKeysAndProps.data?.scoreColumns], ); useEffect(() => { setScoreOptions(scoreAnalyticsOptions); }, [scoreAnalyticsOptions, setScoreOptions]); const columns: LangfuseColumnDef[] = [ { id: "select", accessorKey: "select", size: 30, isFixedPosition: true, isPinnedLeft: true, header: ({ table }) => { return (
{ table.toggleAllPageRowsSelected(!!value); if (!value) { setSelectedRows({}); } }} aria-label="Select all" className="opacity-60" />
); }, cell: ({ row }) => { return ( row.toggleSelected(!!value)} aria-label="Select row" className="opacity-60" /> ); }, }, { accessorKey: "name", header: "Name", id: "name", size: 150, isFixedPosition: true, isPinnedLeft: true, cell: ({ row }) => { const name: DatasetRunRowData["name"] = row.getValue("name"); const id: DatasetRunRowData["id"] = row.getValue("id"); return ( ); }, }, { accessorKey: "id", header: "Id", id: "id", size: 150, enableHiding: true, defaultHidden: true, cell: ({ row }) => { const id: DatasetRunRowData["id"] = row.getValue("id"); return ( ); }, }, { accessorKey: "description", header: "Description", id: "description", size: 300, enableHiding: true, cell: ({ row }) => { const description: DatasetRunRowData["description"] = row.getValue("description"); return description; }, }, { accessorKey: "countRunItems", header: "Run Items", id: "countRunItems", size: 90, enableHiding: true, cell: ({ row }) => { const countRunItems: DatasetRunRowData["countRunItems"] = row.getValue("countRunItems"); if (countRunItems === undefined || runsMetrics.isPending) return ; return <>{countRunItems}; }, }, { accessorKey: "avgLatency", header: "Latency (avg)", id: "avgLatency", size: 120, enableHiding: true, cell: ({ row }) => { const avgLatency: DatasetRunRowData["avgLatency"] = row.getValue("avgLatency"); if (avgLatency === undefined || runsMetrics.isPending) return ; return <>{formatIntervalSeconds(avgLatency)}; }, }, { accessorKey: "avgTotalCost", header: "Trace Cost (avg)", id: "avgTotalCost", size: 130, enableHiding: true, cell: ({ row }) => { const avgTotalCost: DatasetRunRowData["avgTotalCost"] = row.getValue("avgTotalCost"); if (!avgTotalCost || runsMetrics.isPending) return ; return <>{avgTotalCost}; }, }, { accessorKey: "totalCost", header: "Trace Cost (sum)", id: "totalCost", size: 130, enableHiding: true, cell: ({ row }) => { const totalCost: DatasetRunRowData["totalCost"] = row.getValue("totalCost"); if (!totalCost || runsMetrics.isPending) return ; return <>{totalCost}; }, }, { accessorKey: "runScores", header: "Run-Level Scores", id: "runScores", enableHiding: true, defaultHidden: true, cell: () => { return isRunScoreColumnLoading ? ( ) : null; }, columns: runScoreColumns, }, { accessorKey: "runItemScores", header: "Run Item Scores", id: "runItemScores", enableHiding: true, defaultHidden: true, cell: () => { return isColumnLoading ? : null; }, columns: scoreColumns, }, { accessorKey: "createdAt", header: "Created", id: "createdAt", size: 150, enableHiding: true, cell: ({ row }) => { const value: DatasetRunRowData["createdAt"] = row.getValue("createdAt"); return ; }, }, { accessorKey: "metadata", header: "Metadata", id: "metadata", size: 200, enableHiding: true, cell: ({ row }) => { const metadata: DatasetRunRowData["metadata"] = row.getValue("metadata"); return !!metadata ? ( ) : null; }, }, { id: "actions", accessorKey: "actions", header: "Actions", size: 70, cell: ({ row }) => { const id: DatasetRunRowData["id"] = row.getValue("id"); return ( Actions ); }, }, ]; const convertToTableRow = ( item: DatasetsCoreOutput & Partial, ): DatasetRunRowData => { return { id: item.id, name: item.name, createdAt: item.createdAt, countRunItems: item.countRunItems?.toString() ?? "0", avgLatency: item.avgLatency ?? 0, avgTotalCost: item.avgTotalCost ? usdFormatter(item.avgTotalCost.toNumber()) : usdFormatter(0), totalCost: item.totalCost ? usdFormatter(item.totalCost.toNumber()) : usdFormatter(0), runItemScores: item.scores, runScores: item.runScores ? addPrefixToScoreKeys(item.runScores, "Run-level") : {}, description: item.description ?? "", metadata: item.metadata, }; }; const [columnVisibility, setColumnVisibility] = useColumnVisibility( `datasetRunColumnVisibility-${props.projectId}`, columns, ); const [columnOrder, setColumnOrder] = useColumnOrder( `datasetRunColumnOrder-${props.projectId}`, columns, ); // Check if we have charts to display const hasCharts = Boolean(props.selectedMetrics.length); return ( <> {hasCharts ? ( { setChartsPanelSize(sizes[0]); }} >
{props.selectedMetrics.map((key) => { const title = RESOURCE_METRICS.find((metric) => metric.key === key) ?.label ?? getScoreLabelFromKey(key); if (!Boolean(runAggregatedMetrics?.size)) { return (
{title}
); } const adapter = new CompareViewAdapter( runAggregatedMetrics, key, ); const { chartData, chartLabels } = adapter.toChartData(); const scoreData = scoreKeyToData.get(key); if (!scoreData) return (
metric.key === key, )?.maxFractionDigits } />
); return (
); })}
runs.data?.runs.map((run) => run.id).includes(runId), ).length > 0 ? ( runs.data?.runs.map((run) => run.id).includes(runId), )} projectId={props.projectId} datasetId={props.datasetId} setRowSelection={setSelectedRows} /> ) : null, ]} /> convertToTableRow(t), ), } } pagination={{ totalCount: runs.data?.totalRuns ?? null, onChange: setPaginationState, state: paginationState, }} columnVisibility={columnVisibility} onColumnVisibilityChange={setColumnVisibility} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} rowHeight={rowHeight} rowSelection={selectedRows} setRowSelection={setSelectedRows} />
) : ( <> runs.data?.runs.map((run) => run.id).includes(runId), ).length > 0 ? ( runs.data?.runs.map((run) => run.id).includes(runId), )} projectId={props.projectId} datasetId={props.datasetId} setRowSelection={setSelectedRows} /> ) : null, ]} /> convertToTableRow(t), ), } } pagination={{ totalCount: runs.data?.totalRuns ?? null, onChange: setPaginationState, state: paginationState, }} columnVisibility={columnVisibility} onColumnVisibilityChange={setColumnVisibility} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} rowHeight={rowHeight} rowSelection={selectedRows} setRowSelection={setSelectedRows} /> )} ); }