Skip to content

Commit a06c8cd

Browse files
authored
feat: Add download csv functionality to search tables (#939)
Adds a download icon that allows users to download a csv of results. Note: In v1, this was a gear icon that brought up a modal with a few different search options, including adding additional columns. Since that functionality doesn't exist in v2 yet, I thought it was best to just have a direct icon for now. ![image](https://github.com/user-attachments/assets/f6c71d0e-951e-4cc8-a2af-489c03a53598) Fixes: HDX-1590
1 parent b75d7c0 commit a06c8cd

File tree

8 files changed

+626
-85
lines changed

8 files changed

+626
-85
lines changed

.changeset/tall-actors-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
feat: Add download csv functionality to search tables

packages/app/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
"react-bootstrap": "^2.4.0",
7474
"react-bootstrap-range-slider": "^3.0.8",
7575
"react-copy-to-clipboard": "^5.1.0",
76-
"react-csv": "^2.2.2",
7776
"react-dom": "18.3.1",
7877
"react-error-boundary": "^3.1.4",
7978
"react-grid-layout": "^1.3.4",
@@ -83,6 +82,7 @@
8382
"react-json-tree": "^0.17.0",
8483
"react-markdown": "^8.0.4",
8584
"react-modern-drawer": "^1.2.0",
85+
"react-papaparse": "^4.4.0",
8686
"react-query": "^3.39.3",
8787
"react-select": "^5.7.0",
8888
"react-sortable-hoc": "^2.0.0",
@@ -125,7 +125,6 @@
125125
"@types/pluralize": "^0.0.29",
126126
"@types/react": "18.3.1",
127127
"@types/react-copy-to-clipboard": "^5.0.2",
128-
"@types/react-csv": "^1.1.3",
129128
"@types/react-dom": "18.3.1",
130129
"@types/react-grid-layout": "^1.3.2",
131130
"@types/react-syntax-highlighter": "^13.5.2",

packages/app/src/HDXMultiSeriesTableChart.tsx

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { memo, useCallback, useMemo, useRef } from 'react';
22
import Link from 'next/link';
3-
import { CSVLink } from 'react-csv';
43
import { Flex, Text } from '@mantine/core';
54
import {
65
flexRender,
@@ -13,29 +12,12 @@ import {
1312
import { ColumnDef } from '@tanstack/react-table';
1413
import { useVirtualizer } from '@tanstack/react-virtual';
1514

15+
import { CsvExportButton } from './components/CsvExportButton';
16+
import { useCsvExport } from './hooks/useCsvExport';
1617
import { UNDEFINED_WIDTH } from './tableUtils';
1718
import type { NumberFormat } from './types';
1819
import { formatNumber } from './utils';
1920

20-
export const generateCsvData = (
21-
data: any[],
22-
columns: {
23-
dataKey: string;
24-
displayName: string;
25-
sortOrder?: 'asc' | 'desc';
26-
numberFormat?: NumberFormat;
27-
columnWidthPercent?: number;
28-
}[],
29-
groupColumnName?: string,
30-
) => {
31-
return data.map(row => ({
32-
...(groupColumnName != null ? { [groupColumnName]: row.group } : {}),
33-
...Object.fromEntries(
34-
columns.map(({ displayName, dataKey }) => [displayName, row[dataKey]]),
35-
),
36-
}));
37-
};
38-
3921
export const Table = ({
4022
data,
4123
groupColumnName,
@@ -177,9 +159,14 @@ export const Table = ({
177159
[items, rowVirtualizer.options.scrollMargin, totalSize],
178160
);
179161

180-
const csvData = useMemo(() => {
181-
return generateCsvData(data, columns, groupColumnName);
182-
}, [data, columns, groupColumnName]);
162+
const { csvData } = useCsvExport(
163+
data,
164+
columns.map(col => ({
165+
dataKey: col.dataKey,
166+
displayName: col.displayName,
167+
})),
168+
{ groupColumnName },
169+
);
183170

184171
return (
185172
<div
@@ -242,33 +229,30 @@ export const Table = ({
242229
)}
243230
</div>
244231
)}
245-
{header.column.getCanResize() && (
246-
<div
247-
onMouseDown={header.getResizeHandler()}
248-
onTouchStart={header.getResizeHandler()}
249-
className={`resizer text-gray-600 cursor-grab ${
250-
header.column.getIsResizing()
251-
? 'isResizing'
252-
: ''
253-
}`}
254-
>
255-
<i className="bi bi-three-dots-vertical" />
256-
</div>
257-
)}
232+
{header.column.getCanResize() &&
233+
headerIndex !== headerGroup.headers.length - 1 && (
234+
<div
235+
onMouseDown={header.getResizeHandler()}
236+
onTouchStart={header.getResizeHandler()}
237+
className={`resizer text-gray-600 cursor-grab ${
238+
header.column.getIsResizing()
239+
? 'isResizing'
240+
: ''
241+
}`}
242+
>
243+
<i className="bi bi-three-dots-vertical" />
244+
</div>
245+
)}
258246
{headerIndex === headerGroup.headers.length - 1 && (
259247
<div className="d-flex align-items-center">
260-
<CSVLink
248+
<CsvExportButton
261249
data={csvData}
262-
filename={`HyperDX_table_results`}
250+
filename="HyperDX_table_results"
251+
className="fs-8 text-muted-hover ms-2"
252+
title="Download table as CSV"
263253
>
264-
<div
265-
className="fs-8 text-muted-hover ms-2"
266-
role="button"
267-
title="Download table as CSV"
268-
>
269-
<i className="bi bi-download" />
270-
</div>
271-
</CSVLink>
254+
<i className="bi bi-download" />
255+
</CsvExportButton>
272256
</div>
273257
)}
274258
</Flex>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from 'react';
2+
import { useCSVDownloader } from 'react-papaparse';
3+
4+
interface CsvExportButtonProps {
5+
data: Record<string, any>[];
6+
filename: string;
7+
children: React.ReactNode;
8+
className?: string;
9+
title?: string;
10+
disabled?: boolean;
11+
onExportStart?: () => void;
12+
onExportComplete?: () => void;
13+
onExportError?: (error: Error) => void;
14+
}
15+
16+
export const CsvExportButton: React.FC<CsvExportButtonProps> = ({
17+
data,
18+
filename,
19+
children,
20+
className,
21+
title,
22+
disabled = false,
23+
onExportStart,
24+
onExportComplete,
25+
onExportError,
26+
...props
27+
}) => {
28+
const { CSVDownloader } = useCSVDownloader();
29+
30+
const handleClick = () => {
31+
try {
32+
if (data.length === 0) {
33+
onExportError?.(new Error('No data to export'));
34+
return;
35+
}
36+
37+
onExportStart?.();
38+
onExportComplete?.();
39+
} catch (error) {
40+
onExportError?.(
41+
error instanceof Error ? error : new Error('Export failed'),
42+
);
43+
}
44+
};
45+
46+
if (disabled || data.length === 0) {
47+
return (
48+
<div
49+
className={className}
50+
title={disabled ? 'Export disabled' : 'No data to export'}
51+
style={{ opacity: 0.5, cursor: 'not-allowed' }}
52+
{...props}
53+
>
54+
{children}
55+
</div>
56+
);
57+
}
58+
59+
return (
60+
<div
61+
className={className}
62+
role="button"
63+
title={title}
64+
onClick={handleClick}
65+
{...props}
66+
>
67+
<CSVDownloader
68+
data={data}
69+
filename={filename}
70+
config={{
71+
quotes: true,
72+
quoteChar: '"',
73+
escapeChar: '"',
74+
delimiter: ',',
75+
header: true,
76+
}}
77+
style={{
78+
color: 'inherit',
79+
textDecoration: 'none',
80+
background: 'none',
81+
border: 'none',
82+
padding: 0,
83+
cursor: 'pointer',
84+
display: 'block',
85+
width: '100%',
86+
height: '100%',
87+
}}
88+
>
89+
{children}
90+
</CSVDownloader>
91+
</div>
92+
);
93+
};

packages/app/src/components/DBRowTable.tsx

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import cx from 'classnames';
33
import { isString } from 'lodash';
44
import curry from 'lodash/curry';
55
import { Button, Modal } from 'react-bootstrap';
6-
import { CSVLink } from 'react-csv';
76
import { useHotkeys } from 'react-hotkeys-hook';
87
import {
98
Bar,
@@ -43,6 +42,7 @@ import {
4342
} from '@tanstack/react-table';
4443
import { useVirtualizer } from '@tanstack/react-virtual';
4544

45+
import { useCsvExport } from '@/hooks/useCsvExport';
4646
import { useTableMetadata } from '@/hooks/useMetadata';
4747
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
4848
import { useGroupedPatterns } from '@/hooks/usePatterns';
@@ -60,9 +60,11 @@ import {
6060
} from '@/utils';
6161

6262
import { SQLPreview } from './ChartSQLPreview';
63+
import { CsvExportButton } from './CsvExportButton';
6364
import LogLevel from './LogLevel';
6465

6566
import styles from '../../styles/LogTable.module.scss';
67+
6668
type Row = Record<string, any> & { duration: number };
6769
type AccessorFn = (row: Row, column: string) => any;
6870

@@ -315,6 +317,14 @@ export const RawLogTable = memo(
315317
return inferLogLevelColumn(dedupedRows);
316318
}, [dedupedRows]);
317319

320+
const { csvData, maxRows, isLimited } = useCsvExport(
321+
dedupedRows,
322+
displayedColumns.map(col => ({
323+
dataKey: col,
324+
displayName: columnNameMap?.[col] ?? col,
325+
})),
326+
);
327+
318328
const columns = useMemo<ColumnDef<any>[]>(
319329
() => [
320330
{
@@ -632,24 +642,25 @@ export const RawLogTable = memo(
632642
)}
633643
</div>
634644
)}
635-
{header.column.getCanResize() && (
636-
<div
637-
onMouseDown={header.getResizeHandler()}
638-
onTouchStart={header.getResizeHandler()}
639-
className={`resizer text-gray-600 cursor-col-resize ${
640-
header.column.getIsResizing() ? 'isResizing' : ''
641-
}`}
642-
style={{
643-
position: 'absolute',
644-
right: 4,
645-
top: 0,
646-
bottom: 0,
647-
width: 12,
648-
}}
649-
>
650-
<i className="bi bi-three-dots-vertical" />
651-
</div>
652-
)}
645+
{header.column.getCanResize() &&
646+
headerIndex !== headerGroup.headers.length - 1 && (
647+
<div
648+
onMouseDown={header.getResizeHandler()}
649+
onTouchStart={header.getResizeHandler()}
650+
className={`resizer text-gray-600 cursor-col-resize ${
651+
header.column.getIsResizing() ? 'isResizing' : ''
652+
}`}
653+
style={{
654+
position: 'absolute',
655+
right: 4,
656+
top: 0,
657+
bottom: 0,
658+
width: 12,
659+
}}
660+
>
661+
<i className="bi bi-three-dots-vertical" />
662+
</div>
663+
)}
653664
{headerIndex === headerGroup.headers.length - 1 && (
654665
<div
655666
className="d-flex align-items-center"
@@ -671,6 +682,14 @@ export const RawLogTable = memo(
671682
<i className="bi bi-arrow-clockwise" />
672683
</div>
673684
)}
685+
<CsvExportButton
686+
data={csvData}
687+
filename={`hyperdx_search_results_${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`}
688+
className="fs-6 text-muted-hover ms-2"
689+
title={`Download table as CSV (max ${maxRows.toLocaleString()} rows)${isLimited ? ' - data truncated' : ''}`}
690+
>
691+
<i className="bi bi-download" />
692+
</CsvExportButton>
674693
{onSettingsClick != null && (
675694
<div
676695
className="fs-8 text-muted-hover ms-2"

0 commit comments

Comments
 (0)