Chore/refactor download results button to use papaparse (#34693)

* refactor LogTable and PreviewFilterPanel to use DownloadResultsButton

* Remove CSVButton file and remove react-csv dependency

* Update

* Change the download button to use file-saver.

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Joshen LimandIvan Vasilov authored and GitHub committed 2025-04-03 13:44:16 +02:00
1 parent 98c7cbff4d
commit fa8cf46d50
6 files changed
+73 -227

No files matched your search

@@ -1,32 +1,20 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { isEqual } from 'lodash'
import { ChevronDown, Clipboard, Download, Eye, EyeOff, Play } from 'lucide-react'
import Papa from 'papaparse'
import { Key, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Clipboard, Eye, EyeOff, Play } from 'lucide-react'
import { Key, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { Item, Menu, useContextMenu } from 'react-contexify'
import DataGrid, { Column, RenderRowProps, Row } from 'react-data-grid'
import { createPortal } from 'react-dom'
import { toast } from 'sonner'
import { IS_PLATFORM } from 'common'
import { IS_PLATFORM, useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import CSVButton from 'components/ui/CSVButton'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { copyToClipboard } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { ResponseError } from 'types'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
cn,
} from 'ui'
import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui'
import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
@@ -93,11 +81,11 @@ const LogTable = ({
selectedLogError,
onSelectedLogChange,
}: Props) => {
const { ref } = useParams()
const { profile } = useProfile()
const [selectedLogId] = useSelectedLog()
const { show: showContextMenu } = useContextMenu()
const downloadCsvRef = useRef<HTMLDivElement>(null)
const [cellPosition, setCellPosition] = useState<any>()
const [selectionOpen, setSelectionOpen] = useState(false)
const [selectedRow, setSelectedRow] = useState<LogData | null>(null)
@@ -188,7 +176,6 @@ const LogTable = ({
}
}
const stringData = useMemo(() => JSON.stringify(data), [data])
const [dedupedData, logMap] = useMemo<[LogData[], LogMap]>(() => {
const deduped = [...new Set(data)] as LogData[]
@@ -245,54 +232,12 @@ const LogTable = ({
)}
>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" iconRight={<ChevronDown size={14} />}>
Results {data && data.length ? `(${data.length})` : ''}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => {
downloadCsvRef.current?.click()
}}
className="space-x-2"
>
<Download size={14} />
<div>Download CSV</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const csvData = Papa.unparse(data)
copyToClipboard(csvData, () => {
toast.success('Results copied to clipboard')
})
}}
className="space-x-2"
>
<Clipboard size={14} />
<div>Copy as CSV</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
copyToClipboard(stringData, () => {
toast.success('Results copied to clipboard')
})
}}
className="space-x-2"
>
<Clipboard size={14} />
<div>Copy as JSON</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Using .hidden with a ref so I don't have to duplicate the code to download the CSV - Jordi */}
<div className="hidden">
<CSVButton buttonType={'text'} data={data}>
<div ref={downloadCsvRef}>Download CSV</div>
</CSVButton>
<DownloadResultsButton
type="text"
text={`Results ${data && data.length ? `(${data.length})` : ''}`}
results={data}
fileName={`supabase-logs-${ref}.csv`}
/>
</div>
{showHistogramToggle && (
@@ -5,8 +5,8 @@ import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import CSVButton from 'components/ui/CSVButton'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
import { IS_PLATFORM } from 'lib/constants'
import { Button, Input, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui'
@@ -70,6 +70,8 @@ const PreviewFilterPanel = ({
const { ref } = useParams()
const [search, setSearch] = useState('')
const logName = router.pathname.split('/').pop()
const { data: loadBalancers } = useLoadBalancersQuery({ projectRef: ref })
// [Joshen] These are the routes tested that can show replica logs
@@ -233,7 +235,15 @@ const PreviewFilterPanel = ({
Chart
</Button>
</div>
<CSVButton data={csvData} disabled={!Boolean(csvData)} title="Download data" />
{Boolean(csvData) && (
<DownloadResultsButton
iconOnly
type="default"
align="center"
results={csvData ?? []}
fileName={`supabase-${logName}-${ref}.csv`}
/>
)}
</div>
{showDatabaseSelector ? (
-78
View File
@@ -1,78 +0,0 @@
import { Download } from 'lucide-react'
import React, { PropsWithChildren, useMemo, useRef } from 'react'
import { CSVLink } from 'react-csv'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { ButtonProps } from 'ui/src/components/Button/Button'
interface CSVButtonProps {
buttonType?: ButtonProps['type']
onClick?: ButtonProps['onClick']
disabled?: ButtonProps['disabled']
icon?: React.ReactNode
data?: unknown[]
title?: string
}
const CSVButton = ({
onClick,
buttonType = 'default',
icon,
children,
disabled,
data,
title,
}: PropsWithChildren<CSVButtonProps>) => {
const csvRef = useRef(null)
const handleDownload = () => {
;(csvRef.current as any)?.link.click()
}
const formattedData = useMemo(() => {
const first = data?.[0]
if (!first || !data) return
const keys = Object.keys(first as any)
return data.map((datum: any) => {
return keys.reduce((acc: any, key) => {
if (typeof datum[key] === 'object') {
acc[key] = JSON.stringify(datum[key]) as string
} else {
acc[key] = String(datum[key])
}
return acc
}, {})
})
// retrieve dot notation of all keys
}, [JSON.stringify(data)])
return (
<>
<CSVLink
ref={csvRef}
className="hidden"
data={formattedData || ([] as any)}
filename={`supabase_logs.csv`}
title={title}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type={buttonType}
icon={icon || <Download />}
disabled={disabled}
className="px-1.5"
onClick={(e) => {
if (onClick) onClick(e)
handleDownload()
}}
>
{children}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
Download logs as CSV
</TooltipContent>
</Tooltip>
</>
)
}
export default CSVButton
@@ -1,7 +1,8 @@
import saveAs from 'file-saver'
import { ChevronDown, Clipboard, Download } from 'lucide-react'
import { markdownTable } from 'markdown-table'
import { useMemo, useRef } from 'react'
import { CSVLink } from 'react-csv'
import Papa from 'papaparse'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { copyToClipboard } from 'lib/helpers'
@@ -14,7 +15,9 @@ import {
} from 'ui'
interface DownloadResultsButtonProps {
iconOnly?: boolean
type?: 'text' | 'default'
text?: string
align?: 'start' | 'center' | 'end'
results: any[]
fileName: string
@@ -24,7 +27,9 @@ interface DownloadResultsButtonProps {
}
export const DownloadResultsButton = ({
iconOnly = false,
type = 'default',
text = 'Export',
align = 'start',
results,
fileName,
@@ -32,8 +37,6 @@ export const DownloadResultsButton = ({
onCopyAsMarkdown,
onCopyAsJSON,
}: DownloadResultsButtonProps) => {
const csvRef = useRef<CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }>(null)
// [Joshen] Ensure JSON values are stringified for CSV and Markdown
const formattedResults = results.map((row) => {
const r = { ...row }
@@ -51,6 +54,14 @@ export const DownloadResultsButton = ({
return undefined
}, [results])
const downloadAsCSV = () => {
const csv = Papa.unparse(formattedResults, { columns: headers })
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
saveAs(blob, `${fileName}.csv`)
toast.success('Downloading results as CSV')
onDownloadAsCSV?.()
}
const copyAsMarkdown = () => {
if (navigator) {
if (formattedResults.length == 0) toast('Results are empty')
@@ -82,42 +93,32 @@ export const DownloadResultsButton = ({
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type={type} iconRight={<ChevronDown />} disabled={results.length === 0}>
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="w-44">
<DropdownMenuItem
className="gap-x-2"
onClick={() => {
csvRef.current?.link.click()
toast.success('Downloading results as CSV')
onDownloadAsCSV?.()
}}
>
<Download size={14} />
<p>Download CSV</p>
</DropdownMenuItem>
<DropdownMenuItem onClick={copyAsMarkdown} className="gap-x-2">
<Clipboard size={14} />
<p>Copy as markdown</p>
</DropdownMenuItem>
<DropdownMenuItem onClick={copyAsJSON} className="gap-x-2">
<Clipboard size={14} />
<p>Copy as JSON</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<CSVLink
ref={csvRef}
className="hidden"
headers={headers}
data={formattedResults}
filename={`${fileName}.csv`}
/>
</>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type={type}
icon={iconOnly ? <Download /> : undefined}
iconRight={iconOnly ? undefined : <ChevronDown />}
disabled={results.length === 0}
className={iconOnly ? 'w-7' : ''}
>
{!iconOnly && text}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="w-44">
<DropdownMenuItem className="gap-x-2" onClick={() => downloadAsCSV()}>
<Download size={14} />
<p>Download CSV</p>
</DropdownMenuItem>
<DropdownMenuItem onClick={copyAsMarkdown} className="gap-x-2">
<Clipboard size={14} />
<p>Copy as markdown</p>
</DropdownMenuItem>
<DropdownMenuItem onClick={copyAsJSON} className="gap-x-2">
<Clipboard size={14} />
<p>Copy as JSON</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
-2
View File
@@ -95,7 +95,6 @@
"react-beautiful-dnd": "^13.1.0",
"react-contexify": "^5.0.0",
"react-copy-to-clipboard": "^5.1.0",
"react-csv": "^2.0.3",
"react-data-grid": "7.0.0-beta.41",
"react-datepicker": "^4.18.0",
"react-dnd": "^16.0.1",
@@ -150,7 +149,6 @@
"@types/react": "^18.2.24",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-csv": "^1.1.2",
"@types/react-datepicker": "^4.3.4",
"@types/react-dom": "^18.2.8",
"@types/react-grid-layout": "^1.3.0",
+7 -37
View File
@@ -745,9 +745,6 @@ importers:
react-copy-to-clipboard:
specifier: ^5.1.0
version: 5.1.0(react@18.2.0)
react-csv:
specifier: ^2.0.3
version: 2.2.2
react-data-grid:
specifier: 7.0.0-beta.41
version: 7.0.0-beta.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -905,9 +902,6 @@ importers:
'@types/react-copy-to-clipboard':
specifier: ^5.0.4
version: 5.0.5
'@types/react-csv':
specifier: ^1.1.2
version: 1.1.6
'@types/react-datepicker':
specifier: ^4.3.4
version: 4.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -6504,9 +6498,6 @@ packages:
'@types/react-copy-to-clipboard@5.0.5':
resolution: {integrity: sha512-en3JGqPA4RX4aUlo6q6uUbnqLp31Dhm2E/thiMvFTIvU+dUDG249jBG2MJ0rPMXE/MbKVrpmi/1r1G4QLhIHKQ==}
'@types/react-csv@1.1.6':
resolution: {integrity: sha512-D8MmuwIrqVr1ilgCEhB5U7uyMeojPC+Ffr/Zg/galVEFMDy/dbmegj38Bs5hWwgC3VVjCyo9GR+YHrC1fnFm1w==}
'@types/react-datepicker@4.15.1':
resolution: {integrity: sha512-6/LthK0pTDBKjjVJqA2ygY3jJsHH7uZXIk8WPQcGHUiFOQLOKIv3krOxFZ2mt3BB3guMB6jVTc6ansQAd0r7xQ==}
@@ -9004,9 +8995,6 @@ packages:
resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==}
engines: {node: '>=12.0.0'}
flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
@@ -9901,9 +9889,6 @@ packages:
is-reference@1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
is-reference@3.0.2:
resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==}
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
@@ -12394,9 +12379,6 @@ packages:
react: '>= 15'
react-dom: '>= 15'
react-csv@2.2.2:
resolution: {integrity: sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==}
react-data-grid@7.0.0-beta.41:
resolution: {integrity: sha512-WmTP/PV+vtVjIaGVLgyG6WAhqvuPBM8I54bsR7oJZl6w43+mIasZM9rEBWjQ52XHJEy41/tjcMBIMNiWqoEbrQ==}
peerDependencies:
@@ -20914,10 +20896,6 @@ snapshots:
dependencies:
'@types/react': 18.3.3
'@types/react-csv@1.1.6':
dependencies:
'@types/react': 18.3.3
'@types/react-datepicker@4.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@popperjs/core': 2.11.8
@@ -23556,8 +23534,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.11.3
acorn-jsx: 5.3.2(acorn@8.11.3)
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 3.4.3
esprima@4.0.1: {}
@@ -23904,12 +23882,10 @@ snapshots:
flat-cache@3.1.0:
dependencies:
flatted: 3.3.1
flatted: 3.3.2
keyv: 4.5.3
rimraf: 3.0.2
flatted@3.3.1: {}
flatted@3.3.2: {}
follow-redirects@1.15.9: {}
@@ -25011,10 +24987,6 @@ snapshots:
dependencies:
'@types/estree': 1.0.5
is-reference@3.0.2:
dependencies:
'@types/estree': 1.0.5
is-reference@3.0.3:
dependencies:
'@types/estree': 1.0.7
@@ -28084,9 +28056,9 @@ snapshots:
periscopic@3.1.0:
dependencies:
'@types/estree': 1.0.5
'@types/estree': 1.0.7
estree-walker: 3.0.3
is-reference: 3.0.2
is-reference: 3.0.3
pg-cloudflare@1.1.1:
optional: true
@@ -28588,8 +28560,6 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-csv@2.2.2: {}
react-data-grid@7.0.0-beta.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
clsx: 2.1.1
@@ -29491,9 +29461,9 @@ snapshots:
sass@1.72.0:
dependencies:
chokidar: 3.5.3
chokidar: 3.6.0
immutable: 4.3.4
source-map-js: 1.2.0
source-map-js: 1.2.1
satori@0.10.9:
dependencies: