Results.tsx 4.8 KB

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