1
- import { Box , Button , Loader , Stack , Text , TextInput } from '@mantine/core' ;
2
- import { ReactNode , useCallback , useRef , useState } from 'react' ;
1
+ import { Box , Button , Loader , Menu , Stack , Text , TextInput } from '@mantine/core' ;
2
+ import { ReactNode , useCallback , useEffect , useRef , useState } from 'react' ;
3
3
import classes from '../../styles/JSONView.module.css' ;
4
4
import EmptyBox from '@/components/Empty' ;
5
5
import { ErrorView , LoadingView } from './LoadingViews' ;
@@ -14,14 +14,23 @@ import { useLogsStore, logsStoreReducers, isJqSearch, formatLogTs } from '../../
14
14
import { Log } from '@/@types/parseable/api/query' ;
15
15
import _ from 'lodash' ;
16
16
import jqSearch from '@/utils/jqSearch' ;
17
- import { IconCheck , IconCopy , IconSearch } from '@tabler/icons-react' ;
17
+ import { IconCheck , IconCopy , IconDotsVertical , IconSearch } from '@tabler/icons-react' ;
18
18
import { copyTextToClipboard } from '@/utils' ;
19
19
import { useStreamStore } from '../../providers/StreamProvider' ;
20
20
import timeRangeUtils from '@/utils/timeRangeUtils' ;
21
21
import { AxiosError } from 'axios' ;
22
22
import { useHotkeys } from '@mantine/hooks' ;
23
+ import { notifySuccess } from '@/utils/notification' ;
24
+ import { isFirstRowInRange , isRowHighlighted } from '../../utils' ;
23
25
24
- const { setInstantSearchValue, applyInstantSearch, applyJqSearch } = logsStoreReducers ;
26
+ type ContextMenuState = {
27
+ visible : boolean ;
28
+ x : number ;
29
+ y : number ;
30
+ row : Log | null ;
31
+ } ;
32
+
33
+ const { setInstantSearchValue, applyInstantSearch, applyJqSearch, setRowNumber, setSelectedLog } = logsStoreReducers ;
25
34
26
35
const Item = ( props : { header : string | null ; value : string ; highlight : boolean } ) => {
27
36
return (
@@ -75,14 +84,35 @@ const Row = (props: {
75
84
log : Log ;
76
85
searchValue : string ;
77
86
disableHighlight : boolean ;
87
+ isRowHighlighted : boolean ;
88
+ showEllipses : boolean ;
89
+ setContextMenu : any ;
78
90
shouldHighlight : ( header : string | null , val : number | string | Date | null ) => boolean ;
79
91
} ) => {
80
92
const [ isSecureHTTPContext ] = useAppStore ( ( store ) => store . isSecureHTTPContext ) ;
81
93
const [ fieldTypeMap ] = useStreamStore ( ( store ) => store . fieldTypeMap ) ;
82
- const { log, disableHighlight, shouldHighlight } = props ;
94
+ const { log, disableHighlight, shouldHighlight, isRowHighlighted , showEllipses , setContextMenu } = props ;
83
95
84
96
return (
85
- < Stack style = { { flexDirection : 'row' } } className = { classes . rowContainer } gap = { 0 } >
97
+ < Stack
98
+ style = { { flexDirection : 'row' , background : isRowHighlighted ? '#E8EDFE' : 'white' } }
99
+ className = { classes . rowContainer }
100
+ gap = { 0 } >
101
+ { showEllipses && (
102
+ < div
103
+ className = { classes . actionIconContainer }
104
+ onClick = { ( event ) => {
105
+ event . stopPropagation ( ) ;
106
+ setContextMenu ( {
107
+ visible : true ,
108
+ x : event . pageX ,
109
+ y : event . pageY ,
110
+ row : log ,
111
+ } ) ;
112
+ } } >
113
+ < IconDotsVertical stroke = { 1.2 } size = { '0.8rem' } color = "#545beb" />
114
+ </ div >
115
+ ) }
86
116
< span >
87
117
{ _ . isObject ( log ) ? (
88
118
_ . map ( log , ( value , key ) => {
@@ -112,8 +142,8 @@ const Row = (props: {
112
142
) ;
113
143
} ;
114
144
115
- const JsonRows = ( props : { isSearching : boolean } ) => {
116
- const [ { pageData, instantSearchValue } ] = useLogsStore ( ( store ) => store . tableOpts ) ;
145
+ const JsonRows = ( props : { isSearching : boolean ; setContextMenu : any } ) => {
146
+ const [ { pageData, instantSearchValue, rowNumber } , setLogsStore ] = useLogsStore ( ( store ) => store . tableOpts ) ;
117
147
const disableHighlight = props . isSearching || _ . isEmpty ( instantSearchValue ) || isJqSearch ( instantSearchValue ) ;
118
148
const regExp = disableHighlight ? null : new RegExp ( instantSearchValue , 'i' ) ;
119
149
@@ -124,16 +154,50 @@ const JsonRows = (props: { isSearching: boolean }) => {
124
154
[ regExp ] ,
125
155
) ;
126
156
157
+ const handleRowClick = ( index : number , event : React . MouseEvent ) => {
158
+ let newRange = `${ index } :${ index } ` ;
159
+
160
+ if ( ( event . ctrlKey || event . metaKey ) && rowNumber ) {
161
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
162
+ const lastIndex = Math . max ( start , end ) ;
163
+
164
+ const startIndex = Math . min ( lastIndex , index ) ;
165
+ const endIndex = Math . max ( lastIndex , index ) ;
166
+ newRange = `${ startIndex } :${ endIndex } ` ;
167
+ setLogsStore ( ( store ) => setRowNumber ( store , newRange ) ) ;
168
+ } else {
169
+ if ( rowNumber ) {
170
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
171
+ if ( index >= start && index <= end ) {
172
+ setLogsStore ( ( store ) => setRowNumber ( store , '' ) ) ;
173
+ return ;
174
+ }
175
+ }
176
+
177
+ setLogsStore ( ( store ) => setRowNumber ( store , newRange ) ) ;
178
+ }
179
+ } ;
180
+
127
181
return (
128
182
< Stack gap = { 0 } style = { { flex : 1 } } >
129
183
{ _ . map ( pageData , ( d , index ) => (
130
- < Row
131
- log = { d }
184
+ < div
132
185
key = { index }
133
- searchValue = { instantSearchValue }
134
- disableHighlight = { disableHighlight }
135
- shouldHighlight = { shouldHighlight }
136
- />
186
+ onClick = { ( event ) => {
187
+ event . preventDefault ( ) ;
188
+ handleRowClick ( index , event ) ;
189
+ } } >
190
+ < Row
191
+ log = { d }
192
+ key = { index }
193
+ searchValue = { instantSearchValue }
194
+ disableHighlight = { disableHighlight }
195
+ shouldHighlight = { shouldHighlight }
196
+ isRowHighlighted = { isRowHighlighted ( index , rowNumber ) }
197
+ showEllipses = { isFirstRowInRange ( index , rowNumber ) }
198
+ setContextMenu = { props . setContextMenu }
199
+ />
200
+ </ div >
137
201
) ) }
138
202
</ Stack >
139
203
) ;
@@ -250,13 +314,63 @@ const JsonView = (props: {
250
314
isFetchingCount : boolean ;
251
315
} ) => {
252
316
const [ maximized ] = useAppStore ( ( store ) => store . maximized ) ;
317
+ const [ contextMenu , setContextMenu ] = useState < ContextMenuState > ( {
318
+ visible : false ,
319
+ x : 0 ,
320
+ y : 0 ,
321
+ row : null ,
322
+ } ) ;
253
323
324
+ const contextMenuRef = useRef < HTMLDivElement > ( null ) ;
254
325
const { errorMessage, hasNoData, showTable, isFetchingCount } = props ;
255
326
const [ isSearching , setSearching ] = useState ( false ) ;
327
+ const [ rowNumber , setLogsStore ] = useLogsStore ( ( store ) => store . tableOpts . rowNumber ) ;
328
+ const [ pageData ] = useLogsStore ( ( store ) => store . tableOpts . pageData ) ;
329
+ const [ isSecureHTTPContext ] = useAppStore ( ( store ) => store . isSecureHTTPContext ) ;
256
330
const primaryHeaderHeight = ! maximized
257
331
? PRIMARY_HEADER_HEIGHT + STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT + STREAM_SECONDARY_TOOLBAR_HRIGHT
258
332
: 0 ;
259
333
334
+ useEffect ( ( ) => {
335
+ const handleClickOutside = ( event : MouseEvent ) => {
336
+ if ( contextMenuRef . current && ! contextMenuRef . current . contains ( event . target as Node ) ) {
337
+ closeContextMenu ( ) ;
338
+ }
339
+ } ;
340
+
341
+ if ( contextMenu . visible ) {
342
+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
343
+ }
344
+
345
+ return ( ) => {
346
+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
347
+ } ;
348
+ } , [ contextMenu . visible ] ) ;
349
+
350
+ const closeContextMenu = ( ) => setContextMenu ( { visible : false , x : 0 , y : 0 , row : null } ) ;
351
+
352
+ const selectLog = useCallback ( ( log : Log | null ) => {
353
+ if ( ! log ) return ;
354
+ const selectedText = window . getSelection ( ) ?. toString ( ) ;
355
+ if ( selectedText !== undefined && selectedText ?. length > 0 ) return ;
356
+
357
+ setLogsStore ( ( store ) => setSelectedLog ( store , log ) ) ;
358
+ } , [ ] ) ;
359
+
360
+ const copyUrl = useCallback ( ( ) => {
361
+ copyTextToClipboard ( window . location . href ) ;
362
+ notifySuccess ( { message : 'Link Copied!' } ) ;
363
+ } , [ window . location . href ] ) ;
364
+
365
+ const copyJSON = useCallback ( ( ) => {
366
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
367
+
368
+ const rowsToCopy = pageData . slice ( start , end + 1 ) ;
369
+
370
+ copyTextToClipboard ( rowsToCopy ) ;
371
+ notifySuccess ( { message : 'JSON Copied!' } ) ;
372
+ } , [ rowNumber ] ) ;
373
+
260
374
return (
261
375
< TableContainer >
262
376
< Toolbar isSearching = { isSearching } setSearching = { setSearching } />
@@ -268,10 +382,59 @@ const JsonView = (props: {
268
382
style = { { display : 'flex' , flexDirection : 'row' , maxHeight : `calc(100vh - ${ primaryHeaderHeight } px )` } } >
269
383
< Stack gap = { 0 } >
270
384
< Stack style = { { overflowY : 'scroll' } } >
271
- < JsonRows isSearching = { isSearching } />
385
+ < JsonRows isSearching = { isSearching } setContextMenu = { setContextMenu } />
272
386
</ Stack >
273
387
</ Stack >
274
388
</ Box >
389
+ { contextMenu . visible && (
390
+ < div
391
+ ref = { contextMenuRef }
392
+ style = { {
393
+ top : contextMenu . y ,
394
+ left : contextMenu . x ,
395
+ } }
396
+ className = { classes . contextMenuContainer }
397
+ onClick = { closeContextMenu } >
398
+ < Menu opened = { contextMenu . visible } onClose = { closeContextMenu } >
399
+ { ( ( ) => {
400
+ const [ start , end ] = rowNumber . split ( ':' ) . map ( Number ) ;
401
+ const rowCount = end - start + 1 ;
402
+
403
+ if ( rowCount === 1 ) {
404
+ return (
405
+ < Menu . Item
406
+ onClick = { ( ) => {
407
+ selectLog ( contextMenu . row ) ;
408
+ closeContextMenu ( ) ;
409
+ } } >
410
+ View JSON
411
+ </ Menu . Item >
412
+ ) ;
413
+ }
414
+
415
+ return null ;
416
+ } ) ( ) }
417
+ { isSecureHTTPContext && (
418
+ < >
419
+ < Menu . Item
420
+ onClick = { ( ) => {
421
+ copyJSON ( ) ;
422
+ closeContextMenu ( ) ;
423
+ } } >
424
+ Copy JSON
425
+ </ Menu . Item >
426
+ < Menu . Item
427
+ onClick = { ( ) => {
428
+ copyUrl ( ) ;
429
+ closeContextMenu ( ) ;
430
+ } } >
431
+ Copy permalink
432
+ </ Menu . Item >
433
+ </ >
434
+ ) }
435
+ </ Menu >
436
+ </ div >
437
+ ) }
275
438
</ Box >
276
439
) : hasNoData ? (
277
440
< >
0 commit comments