1
- import { Box } from '@mantine/core' ;
2
- import { useCallback , useMemo } from 'react' ;
3
- import type { ReactNode } from 'react' ;
1
+ import { Box , Menu } from '@mantine/core' ;
2
+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
3
+ import type { Dispatch , ReactNode , SetStateAction } from 'react' ;
4
4
import EmptyBox from '@/components/Empty' ;
5
5
import FilterPills from '../../components/FilterPills' ;
6
6
import tableStyles from '../../styles/Logs.module.css' ;
@@ -21,8 +21,11 @@ import { Log } from '@/@types/parseable/api/query';
21
21
import { CopyIcon } from './JSONView' ;
22
22
import { FieldTypeMap , useStreamStore } from '../../providers/StreamProvider' ;
23
23
import timeRangeUtils from '@/utils/timeRangeUtils' ;
24
+ import { IconDotsVertical } from '@tabler/icons-react' ;
25
+ import { copyTextToClipboard } from '@/utils' ;
26
+ import { notifySuccess } from '@/utils/notification' ;
24
27
25
- const { setSelectedLog } = logsStoreReducers ;
28
+ const { setSelectedLog, setRowNumber } = logsStoreReducers ;
26
29
const TableContainer = ( props : { children : ReactNode } ) => {
27
30
return < Box className = { tableStyles . container } > { props . children } </ Box > ;
28
31
} ;
@@ -54,10 +57,23 @@ const getSanitizedValue = (value: CellType, isTimestamp: boolean) => {
54
57
return String ( value ) ;
55
58
} ;
56
59
57
- const makeHeaderOpts = ( headers : string [ ] , isSecureHTTPContext : boolean , fieldTypeMap : FieldTypeMap ) => {
60
+ type ContextMenuState = {
61
+ visible : boolean ;
62
+ x : number ;
63
+ y : number ;
64
+ row : Log | null ;
65
+ } ;
66
+
67
+ const makeHeaderOpts = (
68
+ headers : string [ ] ,
69
+ isSecureHTTPContext : boolean ,
70
+ fieldTypeMap : FieldTypeMap ,
71
+ rowNumber : string ,
72
+ setContextMenu : Dispatch < SetStateAction < ContextMenuState > > ,
73
+ ) => {
58
74
return _ . reduce (
59
75
headers ,
60
- ( acc : { accessorKey : string ; header : string ; grow : boolean } [ ] , header ) => {
76
+ ( acc : { accessorKey : string ; header : string ; grow : boolean } [ ] , header , index ) => {
61
77
const isTimestamp = _ . get ( fieldTypeMap , header , null ) === 'timestamp' ;
62
78
63
79
return [
@@ -76,8 +92,38 @@ const makeHeaderOpts = (headers: string[], isSecureHTTPContext: boolean, fieldTy
76
92
} )
77
93
. value ( ) ;
78
94
const sanitizedValue = getSanitizedValue ( value , isTimestamp ) ;
95
+ let isFirstSelectedRow = false ;
96
+ if ( rowNumber ) {
97
+ const [ start ] = rowNumber . split ( ':' ) . map ( Number ) ;
98
+ isFirstSelectedRow = cell . row . index === start ;
99
+ }
100
+ const isFirstColumn = index === 0 ;
79
101
return (
80
- < div className = { tableStyles . customCellContainer } style = { { overflow : 'hidden' , textOverflow : 'ellipsis' } } >
102
+ < div
103
+ className = { tableStyles . customCellContainer }
104
+ style = { {
105
+ marginLeft : isFirstSelectedRow && isFirstColumn ? '4px' : '' ,
106
+ overflow : 'hidden' ,
107
+ textOverflow : 'ellipsis' ,
108
+ } } >
109
+ < div
110
+ className = { tableStyles . actionIconContainer }
111
+ onClick = { ( event ) => {
112
+ event . stopPropagation ( ) ;
113
+ setContextMenu ( {
114
+ visible : true ,
115
+ x : event . pageX ,
116
+ y : event . pageY ,
117
+ row : cell . row . original ,
118
+ } ) ;
119
+ } }
120
+ style = { {
121
+ display : isFirstSelectedRow && isFirstColumn ? 'flex' : '' ,
122
+ } } >
123
+ { isSecureHTTPContext
124
+ ? sanitizedValue && < IconDotsVertical stroke = { 1.2 } size = { '0.8rem' } color = "#545beb" />
125
+ : null }
126
+ </ div >
81
127
{ sanitizedValue }
82
128
< div className = { tableStyles . copyIconContainer } >
83
129
{ isSecureHTTPContext ? sanitizedValue && < CopyIcon value = { sanitizedValue } /> : null }
@@ -91,19 +137,31 @@ const makeHeaderOpts = (headers: string[], isSecureHTTPContext: boolean, fieldTy
91
137
[ ] ,
92
138
) ;
93
139
} ;
94
-
95
140
const makeColumnVisiblityOpts = ( columns : string [ ] ) => {
96
141
return _ . reduce ( columns , ( acc , column ) => ( { ...acc , [ column ] : false } ) , { } ) ;
97
142
} ;
98
143
99
144
const Table = ( props : { primaryHeaderHeight : number } ) => {
100
- const [ { orderedHeaders, disabledColumns, pinnedColumns, pageData, wrapDisabledColumns } , setLogsStore ] =
101
- useLogsStore ( ( store ) => store . tableOpts ) ;
145
+ const [ contextMenu , setContextMenu ] = useState < ContextMenuState > ( {
146
+ visible : false ,
147
+ x : 0 ,
148
+ y : 0 ,
149
+ row : null ,
150
+ } ) ;
151
+
152
+ const contextMenuRef = useRef < HTMLDivElement > ( null ) ;
153
+ const [ { orderedHeaders, disabledColumns, pageData, wrapDisabledColumns, rowNumber } , setLogsStore ] = useLogsStore (
154
+ ( store ) => store . tableOpts ,
155
+ ) ;
102
156
const [ isSecureHTTPContext ] = useAppStore ( ( store ) => store . isSecureHTTPContext ) ;
103
157
const [ fieldTypeMap ] = useStreamStore ( ( store ) => store . fieldTypeMap ) ;
104
- const columns = useMemo ( ( ) => makeHeaderOpts ( orderedHeaders , isSecureHTTPContext , fieldTypeMap ) , [ orderedHeaders ] ) ;
158
+ const columns = useMemo (
159
+ ( ) => makeHeaderOpts ( orderedHeaders , isSecureHTTPContext , fieldTypeMap , rowNumber , setContextMenu ) ,
160
+ [ orderedHeaders , rowNumber ] ,
161
+ ) ;
105
162
const columnVisibility = useMemo ( ( ) => makeColumnVisiblityOpts ( disabledColumns ) , [ disabledColumns , orderedHeaders ] ) ;
106
- const selectLog = useCallback ( ( log : Log ) => {
163
+ const selectLog = useCallback ( ( log : Log | null ) => {
164
+ if ( ! log ) return ;
107
165
const selectedText = window . getSelection ( ) ?. toString ( ) ;
108
166
if ( selectedText !== undefined && selectedText ?. length > 0 ) return ;
109
167
@@ -126,67 +184,179 @@ const Table = (props: { primaryHeaderHeight: number }) => {
126
184
} ,
127
185
[ wrapDisabledColumns ] ,
128
186
) ;
187
+ useEffect ( ( ) => {
188
+ const handleClickOutside = ( event : MouseEvent ) => {
189
+ if ( contextMenuRef . current && ! contextMenuRef . current . contains ( event . target as Node ) ) {
190
+ closeContextMenu ( ) ;
191
+ }
192
+ } ;
193
+
194
+ if ( contextMenu . visible ) {
195
+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
196
+ }
197
+
198
+ return ( ) => {
199
+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
200
+ } ;
201
+ } , [ contextMenu . visible ] ) ;
202
+
203
+ const closeContextMenu = ( ) => setContextMenu ( { visible : false , x : 0 , y : 0 , row : null } ) ;
204
+
205
+ const copyUrl = useCallback ( ( ) => {
206
+ copyTextToClipboard ( window . location . href ) ;
207
+ notifySuccess ( { message : 'Link Copied!' } ) ;
208
+ } , [ window . location . href ] ) ;
209
+
210
+ const copyJSON = useCallback ( ( ) => {
211
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
212
+
213
+ const rowsToCopy = pageData . slice ( start , end + 1 ) ;
214
+
215
+ copyTextToClipboard ( rowsToCopy ) ;
216
+ notifySuccess ( { message : 'JSON Copied!' } ) ;
217
+ } , [ rowNumber ] ) ;
218
+
219
+ const handleRowClick = ( index : number , event : React . MouseEvent ) => {
220
+ let newRange = `${ index } :${ index } ` ;
221
+
222
+ if ( ( event . ctrlKey || event . metaKey ) && rowNumber ) {
223
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
224
+ const lastIndex = Math . max ( start , end ) ;
225
+
226
+ const startIndex = Math . min ( lastIndex , index ) ;
227
+ const endIndex = Math . max ( lastIndex , index ) ;
228
+ newRange = `${ startIndex } :${ endIndex } ` ;
229
+ setLogsStore ( ( store ) => setRowNumber ( store , newRange ) ) ;
230
+ } else {
231
+ if ( rowNumber ) {
232
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
233
+ if ( index >= start && index <= end ) {
234
+ setLogsStore ( ( store ) => setRowNumber ( store , '' ) ) ;
235
+ return ;
236
+ }
237
+ }
238
+
239
+ setLogsStore ( ( store ) => setRowNumber ( store , newRange ) ) ;
240
+ }
241
+ } ;
129
242
130
243
return (
131
- < MantineReactTable
132
- enableBottomToolbar = { false }
133
- enableTopToolbar = { false }
134
- enableColumnResizing = { true }
135
- mantineTableBodyCellProps = { ( { column : { id } } ) => makeCellCustomStyles ( id ) }
136
- mantineTableHeadRowProps = { { style : { border : 'none' } } }
137
- mantineTableHeadCellProps = { {
138
- style : {
139
- fontWeight : 600 ,
140
- fontSize : '0.65rem' ,
141
- border : 'none' ,
142
- padding : '0.5rem 1rem' ,
143
- } ,
144
- } }
145
- mantineTableBodyRowProps = { ( { row } ) => {
146
- return {
147
- onClick : ( ) => {
148
- selectLog ( row . original ) ;
244
+ < >
245
+ < MantineReactTable
246
+ enableBottomToolbar = { false }
247
+ enableTopToolbar = { false }
248
+ enableColumnResizing
249
+ mantineTableBodyCellProps = { ( { column : { id } } ) => makeCellCustomStyles ( id ) }
250
+ mantineTableHeadRowProps = { { style : { border : 'none' } } }
251
+ mantineTableHeadCellProps = { {
252
+ style : {
253
+ fontWeight : 600 ,
254
+ fontSize : '0.65rem' ,
255
+ border : 'none' ,
256
+ padding : '0.5rem 1rem' ,
149
257
} ,
258
+ } }
259
+ mantineTableBodyRowProps = { ( { row } ) => {
260
+ return {
261
+ onClick : ( event ) => {
262
+ event . preventDefault ( ) ;
263
+ handleRowClick ( row . index , event ) ;
264
+ } ,
265
+ style : {
266
+ border : 'none' ,
267
+ background : row . index % 2 === 0 ? '#f8f9fa' : 'white' ,
268
+ backgroundColor :
269
+ rowNumber &&
270
+ ( ( ) => {
271
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
272
+ return row . index >= start && row . index <= end ;
273
+ } ) ( )
274
+ ? '#E8EDFE'
275
+ : '' ,
276
+ } ,
277
+ } ;
278
+ } }
279
+ mantineTableProps = { { highlightOnHover : false } }
280
+ mantineTableHeadProps = { {
150
281
style : {
151
282
border : 'none' ,
152
- background : row . index % 2 === 0 ? '#f8f9fa' : 'white' ,
153
283
} ,
154
- } ;
155
- } }
156
- mantineTableHeadProps = { {
157
- style : {
158
- border : 'none' ,
159
- } ,
160
- } }
161
- columns = { columns }
162
- data = { pageData }
163
- mantinePaperProps = { { style : { border : 'none' } } }
164
- enablePagination = { false }
165
- enableColumnPinning = { true }
166
- initialState = { {
167
- columnPinning : {
168
- left : pinnedColumns ,
169
- } ,
170
- } }
171
- enableStickyHeader = { true }
172
- defaultColumn = { { minSize : 100 } }
173
- layoutMode = "grid"
174
- state = { {
175
- columnPinning : {
176
- left : pinnedColumns ,
177
- } ,
178
- columnVisibility,
179
- columnOrder : orderedHeaders ,
180
- } }
181
- mantineTableContainerProps = { {
182
- style : {
183
- height : `calc(100vh - ${ props . primaryHeaderHeight + LOGS_FOOTER_HEIGHT } px )` ,
184
- } ,
185
- } }
186
- renderColumnActionsMenuItems = { ( { column } ) => {
187
- return < Column columnName = { column . id } /> ;
188
- } }
189
- />
284
+ } }
285
+ columns = { columns }
286
+ data = { pageData }
287
+ mantinePaperProps = { { style : { border : 'none' } } }
288
+ enablePagination = { false }
289
+ enableColumnPinning
290
+ initialState = { {
291
+ columnPinning : {
292
+ left : [ 'rowNumber' ] ,
293
+ } ,
294
+ } }
295
+ enableStickyHeader
296
+ defaultColumn = { { minSize : 100 } }
297
+ layoutMode = "grid"
298
+ state = { {
299
+ columnPinning : {
300
+ left : [ 'rowNumber' ] ,
301
+ } ,
302
+ columnVisibility,
303
+ columnOrder : orderedHeaders ,
304
+ } }
305
+ mantineTableContainerProps = { {
306
+ style : {
307
+ height : `calc(100vh - ${ props . primaryHeaderHeight + LOGS_FOOTER_HEIGHT } px )` ,
308
+ } ,
309
+ } }
310
+ renderColumnActionsMenuItems = { ( { column } ) => {
311
+ return < Column columnName = { column . id } /> ;
312
+ } }
313
+ />
314
+ { contextMenu . visible && (
315
+ < div
316
+ ref = { contextMenuRef }
317
+ style = { {
318
+ top : contextMenu . y ,
319
+ left : contextMenu . x ,
320
+ } }
321
+ className = { tableStyles . contextMenuContainer }
322
+ onClick = { closeContextMenu } >
323
+ < Menu opened = { contextMenu . visible } onClose = { closeContextMenu } >
324
+ { ( ( ) => {
325
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
326
+ const rowCount = end - start + 1 ;
327
+
328
+ if ( rowCount === 1 ) {
329
+ return (
330
+ < Menu . Item
331
+ onClick = { ( ) => {
332
+ selectLog ( contextMenu . row ) ;
333
+ closeContextMenu ( ) ;
334
+ } } >
335
+ View JSON
336
+ </ Menu . Item >
337
+ ) ;
338
+ }
339
+
340
+ return null ;
341
+ } ) ( ) }
342
+ < Menu . Item
343
+ onClick = { ( ) => {
344
+ copyJSON ( ) ;
345
+ closeContextMenu ( ) ;
346
+ } } >
347
+ Copy JSON
348
+ </ Menu . Item >
349
+ < Menu . Item
350
+ onClick = { ( ) => {
351
+ copyUrl ( ) ;
352
+ closeContextMenu ( ) ;
353
+ } } >
354
+ Copy permalink
355
+ </ Menu . Item >
356
+ </ Menu >
357
+ </ div >
358
+ ) }
359
+ </ >
190
360
) ;
191
361
} ;
192
362
0 commit comments