DownloadResultsButton.tsx180 lines · main
1import { IS_PLATFORM, useParams } from 'common'
2import saveAs from 'file-saver'
3import { ChevronDown, Copy, Download, Settings } from 'lucide-react'
4import Link from 'next/link'
5import { usePathname } from 'next/navigation'
6import { useMemo } from 'react'
7import { toast } from 'sonner'
8import {
9 Button,
10 copyToClipboard,
11 DropdownMenu,
12 DropdownMenuContent,
13 DropdownMenuItem,
14 DropdownMenuTrigger,
15 KeyboardShortcut,
16} from 'ui'
17
18import {
19 convertResultsToCSV,
20 convertResultsToJSON,
21 convertResultsToMarkdown,
22} from '@/components/interfaces/SQLEditor/UtilityPanel/Results.utils'
23import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
24import { useShortcut } from '@/state/shortcuts/useShortcut'
25
26interface DownloadResultsButtonProps {
27 iconOnly?: boolean
28 type?: 'text' | 'default'
29 text?: string
30 align?: 'start' | 'center' | 'end'
31 results: any[]
32 fileName: string
33 enableCopyShortcuts?: boolean
34 onDownloadAsCSV?: () => void
35 onCopyAsMarkdown?: () => void
36 onCopyAsJSON?: () => void
37 onCopyAsCSV?: () => void
38}
39
40export const DownloadResultsButton = ({
41 iconOnly = false,
42 type = 'default',
43 text = 'Export',
44 align = 'start',
45 results,
46 fileName,
47 enableCopyShortcuts = true,
48 onDownloadAsCSV,
49 onCopyAsMarkdown,
50 onCopyAsJSON,
51 onCopyAsCSV,
52}: DownloadResultsButtonProps) => {
53 const { ref } = useParams()
54 const pathname = usePathname()
55 const isLogs = pathname?.includes?.('/logs') ?? false
56 const isEmpty = useMemo(() => results.length === 0, [results])
57
58 const downloadAsCSV = () => {
59 const csv = convertResultsToCSV(results)
60 if (!csv) {
61 toast('Results are empty')
62 return
63 }
64
65 const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
66 saveAs(blob, `${fileName}.csv`)
67 toast.success('Downloading results as CSV')
68 onDownloadAsCSV?.()
69 }
70
71 const copyAsMarkdown = () => {
72 const markdownData = convertResultsToMarkdown(results)
73 if (!markdownData) {
74 toast('Results are empty')
75 return
76 }
77 copyToClipboard(markdownData, () => {
78 toast.success('Copied Markdown to clipboard')
79 onCopyAsMarkdown?.()
80 })
81 }
82
83 const copyAsJSON = () => {
84 const jsonData = convertResultsToJSON(results)
85 if (!jsonData) {
86 toast('Results are empty')
87 return
88 }
89 copyToClipboard(jsonData, () => {
90 toast.success('Copied JSON to clipboard')
91 onCopyAsJSON?.()
92 })
93 }
94
95 const copyAsCSV = () => {
96 const csv = convertResultsToCSV(results)
97 if (!csv) {
98 toast('Results are empty')
99 return
100 }
101 copyToClipboard(csv, () => {
102 toast.success('Copied CSV to clipboard')
103 onCopyAsCSV?.()
104 })
105 }
106
107 useShortcut(SHORTCUT_IDS.RESULTS_COPY_MARKDOWN, copyAsMarkdown, {
108 enabled: !isEmpty && enableCopyShortcuts,
109 conflictBehavior: 'allow',
110 registerInCommandMenu: true,
111 })
112 useShortcut(SHORTCUT_IDS.RESULTS_COPY_JSON, copyAsJSON, {
113 enabled: !isEmpty && enableCopyShortcuts,
114 conflictBehavior: 'allow',
115 registerInCommandMenu: true,
116 })
117 useShortcut(SHORTCUT_IDS.RESULTS_COPY_CSV, copyAsCSV, {
118 enabled: !isEmpty && enableCopyShortcuts,
119 conflictBehavior: 'allow',
120 registerInCommandMenu: true,
121 })
122 useShortcut(SHORTCUT_IDS.RESULTS_DOWNLOAD_CSV, downloadAsCSV, {
123 enabled: !isEmpty,
124 registerInCommandMenu: true,
125 })
126
127 return (
128 <DropdownMenu>
129 <DropdownMenuTrigger asChild>
130 <Button
131 type={type}
132 icon={iconOnly ? <Download /> : undefined}
133 iconRight={iconOnly ? undefined : <ChevronDown />}
134 disabled={results.length === 0}
135 className={iconOnly ? 'w-7' : ''}
136 >
137 {!iconOnly && text}
138 </Button>
139 </DropdownMenuTrigger>
140 <DropdownMenuContent align={align} className="w-60">
141 {isLogs && IS_PLATFORM && (
142 <DropdownMenuItem asChild className="gap-x-2">
143 <Link href={`/project/${ref}/settings/log-drains`}>
144 <Settings size={14} />
145 <p>Add a Log Drain</p>
146 </Link>
147 </DropdownMenuItem>
148 )}
149 <DropdownMenuItem onClick={copyAsMarkdown} className="gap-x-2">
150 <Copy size={14} />
151 <p>Copy as Markdown</p>
152 <span className="ml-auto">
153 <KeyboardShortcut keys={['Shift', 'Meta', 'm']} />
154 </span>
155 </DropdownMenuItem>
156 <DropdownMenuItem onClick={copyAsJSON} className="gap-x-2">
157 <Copy size={14} />
158 <p>Copy as JSON</p>
159 <span className="ml-auto">
160 <KeyboardShortcut keys={['Shift', 'Meta', 'j']} />
161 </span>
162 </DropdownMenuItem>
163 <DropdownMenuItem onClick={copyAsCSV} className="gap-x-2">
164 <Copy size={14} />
165 <p>Copy as CSV</p>
166 <span className="ml-auto">
167 <KeyboardShortcut keys={['Shift', 'Meta', 'c']} />
168 </span>
169 </DropdownMenuItem>
170 <DropdownMenuItem className="gap-x-2" onClick={() => downloadAsCSV()}>
171 <Download size={14} />
172 <p>Download CSV</p>
173 <span className="ml-auto">
174 <KeyboardShortcut keys={['Shift', 'Meta', 'd']} />
175 </span>
176 </DropdownMenuItem>
177 </DropdownMenuContent>
178 </DropdownMenu>
179 )
180}