diff --git a/apps/opik-frontend/package-lock.json b/apps/opik-frontend/package-lock.json index bd888499bc..b3aa4fc592 100644 --- a/apps/opik-frontend/package-lock.json +++ b/apps/opik-frontend/package-lock.json @@ -45,8 +45,10 @@ "date-fns": "^3.6.0", "dayjs": "^1.11.11", "diff": "^7.0.0", + "file-saver": "^2.0.5", "flattie": "^1.1.1", "js-yaml": "^4.1.0", + "json-2-csv": "^5.5.6", "lodash": "^4.17.21", "lucide-react": "^0.395.0", "md5": "^2.3.0", @@ -73,6 +75,7 @@ "@tanstack/router-vite-plugin": "^1.37.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^14.3.1", + "@types/file-saver": "^2.0.7", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.17.5", "@types/node": "^20.14.13", @@ -4482,6 +4485,12 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -6007,6 +6016,14 @@ } } }, + "node_modules/deeks": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz", + "integrity": "sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==", + "engines": { + "node": ">= 16" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -6151,6 +6168,14 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/doc-path": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz", + "integrity": "sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6896,6 +6921,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8035,6 +8065,18 @@ "node": ">=4" } }, + "node_modules/json-2-csv": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.6.tgz", + "integrity": "sha512-N673XbJgHwUq9JreKpk530jSywPF/rEAQ08dV99QQpkluP/4HTwshpoP9hmDz26iSFqu7eNAPgyJfu/77HvPGA==", + "dependencies": { + "deeks": "3.1.0", + "doc-path": "4.1.1" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", diff --git a/apps/opik-frontend/package.json b/apps/opik-frontend/package.json index a7450fbf73..96244ae803 100644 --- a/apps/opik-frontend/package.json +++ b/apps/opik-frontend/package.json @@ -62,8 +62,10 @@ "date-fns": "^3.6.0", "dayjs": "^1.11.11", "diff": "^7.0.0", + "file-saver": "^2.0.5", "flattie": "^1.1.1", "js-yaml": "^4.1.0", + "json-2-csv": "^5.5.6", "lodash": "^4.17.21", "lucide-react": "^0.395.0", "md5": "^2.3.0", @@ -90,6 +92,7 @@ "@tanstack/router-vite-plugin": "^1.37.0", "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^14.3.1", + "@types/file-saver": "^2.0.7", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.17.5", "@types/node": "^20.14.13", diff --git a/apps/opik-frontend/src/components/pages/TracesPage/TracesActionsButton.tsx b/apps/opik-frontend/src/components/pages/TracesPage/TracesActionsButton.tsx index 08f4948f5e..0a098d1775 100644 --- a/apps/opik-frontend/src/components/pages/TracesPage/TracesActionsButton.tsx +++ b/apps/opik-frontend/src/components/pages/TracesPage/TracesActionsButton.tsx @@ -1,5 +1,9 @@ import React, { useCallback, useRef, useState } from "react"; -import { Database, Trash } from "lucide-react"; +import last from "lodash/last"; +import get from "lodash/get"; +import { json2csv } from "json-2-csv"; +import FileSaver from "file-saver"; +import { ArrowDownToLine, Database, Trash } from "lucide-react"; import { DropdownMenu, @@ -17,18 +21,19 @@ import useTracesBatchDeleteMutation from "@/api/traces/useTraceBatchDeleteMutati type TracesActionsButtonProps = { type: TRACE_DATA_TYPE; rows: Array; + selectedColumns: string[]; projectId: string; }; const TracesActionsButton: React.FunctionComponent< TracesActionsButtonProps -> = ({ rows, type, projectId }) => { +> = ({ rows, type, selectedColumns, projectId }) => { const resetKeyRef = useRef(0); const [open, setOpen] = useState(false); const tracesBatchDeleteMutation = useTracesBatchDeleteMutation(); - const deleteTraces = useCallback(() => { + const deleteTracesHandler = useCallback(() => { tracesBatchDeleteMutation.mutate({ projectId, ids: rows.map((row) => row.id), @@ -36,6 +41,23 @@ const TracesActionsButton: React.FunctionComponent< // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId, rows]); + const exportCSVHandler = useCallback(() => { + const mappedRows = rows.map((row) => { + return selectedColumns.reduce>((acc, column) => { + // we need split by dot to parse usage into correct structure + const keys = column.split("."); + const key = last(keys) as string; + acc[key] = get(row, keys, ""); + return acc; + }, {}); + }); + + FileSaver.saveAs( + new Blob([json2csv(mappedRows)], { type: "text/csv;charset=utf-8" }), + type === TRACE_DATA_TYPE.traces ? "traces.csv" : "llm_calls.csv", + ); + }, [rows, selectedColumns, type]); + return ( <> )} + + + Export CSV + diff --git a/apps/opik-frontend/src/components/pages/TracesPage/TracesPage.tsx b/apps/opik-frontend/src/components/pages/TracesPage/TracesPage.tsx index c405dc8f1c..191564e496 100644 --- a/apps/opik-frontend/src/components/pages/TracesPage/TracesPage.tsx +++ b/apps/opik-frontend/src/components/pages/TracesPage/TracesPage.tsx @@ -268,6 +268,7 @@ const TracesPage = () => { )}