Results.tsx146 lines · main
1import { Copy, Expand } from 'lucide-react'
2import { useCallback, useMemo, useRef, useState } from 'react'
3import DataGrid, { CalculatedColumn } from 'react-data-grid'
4import {
5 ContextMenu,
6 ContextMenuContent,
7 ContextMenuItem,
8 ContextMenuTrigger,
9 copyToClipboard,
10} from 'ui'
11
12import { CellDetailPanel } from './CellDetailPanel'
13import { ResultCell } from './ResultCell'
14import { formatClipboardValue } from './Results.utils'
15import { handleCellKeyDown } from '@/components/grid/BrivenGrid.utils'
16
17export const Results = ({ rows }: { rows: readonly any[] }) => {
18 const [expandedCell, setExpandedCell] = useState<{ column: string; value: any } | null>(null)
19 const contextMenuCellRef = useRef<{ column: string; value: any } | null>(null)
20 const triggerRef = useRef<HTMLDivElement>(null)
21
22 const handleContextMenu = useCallback((e: React.MouseEvent, column: string, value: any) => {
23 contextMenuCellRef.current = { column, value }
24
25 if (triggerRef.current) {
26 // Position the hidden trigger at the mouse cursor so the context menu opens there
27 triggerRef.current.style.position = 'fixed'
28 triggerRef.current.style.left = `${e.clientX}px`
29 triggerRef.current.style.top = `${e.clientY}px`
30
31 const contextMenuEvent = new MouseEvent('contextmenu', {
32 bubbles: true,
33 clientX: e.clientX,
34 clientY: e.clientY,
35 })
36 triggerRef.current.dispatchEvent(contextMenuEvent)
37 }
38 }, [])
39
40 const columnRender = (name: string) => {
41 return <div className="flex h-full items-center justify-center font-mono text-xs">{name}</div>
42 }
43
44 const EST_CHAR_WIDTH = 8.25
45 const MIN_COLUMN_WIDTH = 100
46 const MAX_COLUMN_WIDTH = 500
47
48 const columns: CalculatedColumn<any>[] = useMemo(
49 () =>
50 Object.keys(rows?.[0] ?? []).map((key, idx) => {
51 const maxColumnValueLength = rows
52 .map((row) => String(row[key]).length)
53 .reduce((a, b) => Math.max(a, b), 0)
54
55 const columnWidth = Math.max(
56 Math.min(maxColumnValueLength * EST_CHAR_WIDTH, MAX_COLUMN_WIDTH),
57 MIN_COLUMN_WIDTH
58 )
59
60 return {
61 idx,
62 key,
63 name: key,
64 resizable: true,
65 parent: undefined,
66 level: 0,
67 width: columnWidth,
68 minWidth: MIN_COLUMN_WIDTH,
69 maxWidth: undefined,
70 draggable: false,
71 frozen: false,
72 sortable: false,
73 isLastFrozenColumn: false,
74 renderCell: ({ row }: { row: any }) => (
75 <ResultCell
76 column={key}
77 value={row[key]}
78 onContextMenu={handleContextMenu}
79 onExpand={(column, value) => setExpandedCell({ column, value })}
80 />
81 ),
82 renderHeaderCell: () => columnRender(key),
83 }
84 }),
85 [rows, handleContextMenu]
86 )
87
88 return (
89 <>
90 {rows.length === 0 ? (
91 <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark">
92 <p className="m-0 border-0 px-4 py-3 font-mono text-sm text-foreground-light">
93 Success. No rows returned
94 </p>
95 </div>
96 ) : (
97 <>
98 <ContextMenu modal={false}>
99 <ContextMenuTrigger asChild>
100 <div ref={triggerRef} className="fixed pointer-events-none w-0 h-0" />
101 </ContextMenuTrigger>
102 <ContextMenuContent onCloseAutoFocus={(e) => e.stopPropagation()}>
103 <ContextMenuItem
104 className="gap-x-2"
105 onSelect={() => {
106 const value = formatClipboardValue(contextMenuCellRef.current?.value ?? '')
107 copyToClipboard(value)
108 }}
109 onFocusCapture={(e) => e.stopPropagation()}
110 >
111 <Copy size={12} />
112 Copy cell content
113 </ContextMenuItem>
114 <ContextMenuItem
115 className="gap-x-2"
116 onSelect={() => {
117 const cell = contextMenuCellRef.current
118 if (cell) setExpandedCell({ column: cell.column, value: cell.value })
119 }}
120 onFocusCapture={(e) => e.stopPropagation()}
121 >
122 <Expand size={12} />
123 View cell content
124 </ContextMenuItem>
125 </ContextMenuContent>
126 </ContextMenu>
127 <DataGrid
128 columns={columns}
129 rows={rows}
130 className="grow min-h-0 border-t-0"
131 rowClass={() => '[&>.rdg-cell]:items-center'}
132 onCellKeyDown={handleCellKeyDown}
133 />
134 <CellDetailPanel
135 column={expandedCell?.column ?? ''}
136 value={expandedCell?.value}
137 visible={expandedCell !== null}
138 onClose={() => setExpandedCell(null)}
139 />
140 </>
141 )}
142 </>
143 )
144}
145
146export default Results