1
- import React , { useCallback , useEffect , useRef , useState } from 'react '
1
+ import { EuiIcon , EuiProgress , EuiResizeObserver , EuiText , } from '@elastic/eui '
2
2
import cx from 'classnames'
3
- import { InfiniteLoader ,
4
- Table ,
5
- Column ,
6
- IndexRange ,
3
+ import { findIndex , isNumber , sumBy , xor } from 'lodash'
4
+ import React , { useCallback , useEffect , useRef , useState } from 'react'
5
+ import {
7
6
CellMeasurer ,
8
- TableCellProps ,
9
7
CellMeasurerCache ,
8
+ Column ,
9
+ IndexRange ,
10
+ InfiniteLoader ,
10
11
RowMouseEventHandlerParams ,
12
+ Table ,
13
+ TableCellProps ,
11
14
} from 'react-virtualized'
12
- import { findIndex , isNumber , xor } from 'lodash'
13
- import {
14
- EuiText ,
15
- EuiProgress ,
16
- EuiResizeObserver ,
17
- EuiIcon ,
18
- } from '@elastic/eui'
19
-
20
- import { isEqualBuffers , Maybe , Nullable } from 'uiSrc/utils'
15
+ import TableColumnSearchTrigger from 'uiSrc/components/table-column-search-trigger/TableColumnSearchTrigger'
16
+ import TableColumnSearch from 'uiSrc/components/table-column-search/TableColumnSearch'
21
17
import { SortOrder } from 'uiSrc/constants'
22
18
import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'
23
- import TableColumnSearch from 'uiSrc/components/table-column-search/TableColumnSearch'
24
- import TableColumnSearchTrigger from 'uiSrc/components/table-column-search-trigger/TableColumnSearchTrigger '
25
- import { IColumnSearchState , IProps , IResizeEvent , ITableColumn } from './interfaces'
19
+
20
+ import { isEqualBuffers , Maybe , Nullable } from 'uiSrc/utils '
21
+ import { ColumnWidthSizes , IColumnSearchState , IProps , IResizeEvent , ITableColumn , ResizableState } from './interfaces'
26
22
import KeysSummary from '../keys-summary'
27
23
28
24
import styles from './styles.module.scss'
29
25
26
+ // this is needed to align content when scrollbar appears
27
+ const TABLE_OUTSIDE_WIDTH = 24
28
+
30
29
const VirtualTable = ( props : IProps ) => {
31
30
const {
32
31
selectable = false ,
@@ -64,17 +63,25 @@ const VirtualTable = (props: IProps) => {
64
63
fixedWidth : true ,
65
64
minHeight : rowHeight ,
66
65
} ) ,
66
+ onColResizeEnd
67
67
} = props
68
68
let selectTimer : number = 0
69
69
const selectTimerDelay = 300
70
70
let preventSelect = false
71
71
72
72
const scrollTopRef = useRef < number > ( 0 )
73
+ const resizeColRef = useRef < ResizableState > ( { column : null , active : false , x : 0 } )
74
+
73
75
const [ selectedRowIndex , setSelectedRowIndex ] = useState < Nullable < number > > ( null )
74
76
const [ search , setSearch ] = useState < IColumnSearchState [ ] > ( [ ] )
75
77
const [ width , setWidth ] = useState < number > ( 100 )
76
78
const [ height , setHeight ] = useState < number > ( 100 )
77
79
const [ forceScrollTop , setForceScrollTop ] = useState < Maybe < number > > ( scrollTopProp )
80
+ const [ columnWidthSizes , setColumnWidthSizes ] = useState < Nullable < ColumnWidthSizes > > ( null )
81
+ const [ isColResizing , setIsColResizing ] = useState ( false )
82
+ const [ , forceUpdate ] = useState ( { } )
83
+
84
+ const [ minWidthAllCols , setMinWidthAllCols ] = useState < number > ( 0 )
78
85
79
86
useEffect ( ( ) => {
80
87
const searchableFields : ITableColumn [ ] = columns . filter (
@@ -102,6 +109,10 @@ const VirtualTable = (props: IProps) => {
102
109
}
103
110
} , [ ] )
104
111
112
+ useEffect ( ( ) => {
113
+ setMinWidthAllCols ( sumBy ( columns , ( ( col ) => col ?. minWidth || 0 ) ) )
114
+ } , [ columns ] )
115
+
105
116
useEffect ( ( ) => {
106
117
if ( forceScrollTop !== undefined ) {
107
118
setForceScrollTop ( undefined )
@@ -118,6 +129,34 @@ const VirtualTable = (props: IProps) => {
118
129
cellCache ?. clearAll ( )
119
130
} , [ totalItemsCount ] )
120
131
132
+ const clearSelectTimeout = ( timer : number = 0 ) => {
133
+ clearTimeout ( timer || selectTimer )
134
+ preventSelect = true
135
+ }
136
+
137
+ const clearCache = ( ) => setTimeout ( ( ) => {
138
+ cellCache . clearAll ( )
139
+ forceUpdate ( { } )
140
+ } , 0 )
141
+
142
+ const getWidthOfColumn = ( colId : string , colWidth : number , width : number , isRelative = false ) => {
143
+ let newColWidth = isRelative ? ( colWidth / 100 ) * width : colWidth
144
+
145
+ const currentColumn = columns . find ( ( col ) => col . id === colId )
146
+ const maxWidthFromTable = width - ( minWidthAllCols - ( currentColumn ?. minWidth || 0 ) ) - TABLE_OUTSIDE_WIDTH
147
+ const maxWidth = currentColumn ?. maxWidth || maxWidthFromTable
148
+ const minWidth = ( currentColumn ?. minWidth || 60 )
149
+
150
+ if ( newColWidth > maxWidth ) newColWidth = maxWidth
151
+ if ( newColWidth < minWidth ) newColWidth = minWidth
152
+
153
+ const newAbsWidth = Math . floor ( newColWidth )
154
+ return {
155
+ abs : newAbsWidth ,
156
+ relative : ( newAbsWidth / width ) * 100
157
+ }
158
+ }
159
+
121
160
const onRowSelect = ( data : RowMouseEventHandlerParams ) => {
122
161
const isRowSelectable = checkIfRowSelectable ( data . rowData )
123
162
@@ -132,6 +171,9 @@ const VirtualTable = (props: IProps) => {
132
171
if ( ! preventSelect && ! textSelected ) {
133
172
setExpandedRows ( xor ( expandedRows , [ data . index ] ) )
134
173
onRowToggleViewClick ?.( expandedRows . indexOf ( data . index ) === - 1 , data . index )
174
+
175
+ clearCache ( )
176
+ setTimeout ( ( ) => { clearCache ( ) } , 0 )
135
177
}
136
178
preventSelect = false
137
179
} , selectTimerDelay , cellCache )
@@ -144,25 +186,96 @@ const VirtualTable = (props: IProps) => {
144
186
cellCache . clearAll ( )
145
187
}
146
188
}
147
- const clearSelectTimeout = ( timer : number = 0 ) => {
148
- clearTimeout ( timer || selectTimer )
149
- preventSelect = true
150
- }
151
189
152
- const onScroll = useCallback (
153
- ( { scrollTop } ) => {
154
- scrollTopRef . current = scrollTop
155
- } ,
156
- [ scrollTopRef ] ,
157
- )
190
+ const onScroll = useCallback ( ( { scrollTop } : { scrollTop : number } ) => {
191
+ scrollTopRef . current = scrollTop
192
+ } , [ scrollTopRef ] )
158
193
159
194
const onResize = ( { height, width } : IResizeEvent ) : void => {
160
195
setHeight ( height )
161
- setWidth ( width )
196
+ setWidth ( Math . floor ( width ) )
162
197
onChangeWidth ?.( width )
198
+
199
+ if ( ! columnWidthSizes ) {
200
+ // init width sizes
201
+ setColumnWidthSizes (
202
+ columns
203
+ . filter ( ( col ) => col . isResizable )
204
+ . reduce ( ( prev , next ) => {
205
+ const propAbsWidth = next . absoluteWidth && isNumber ( next . absoluteWidth ) ? next . absoluteWidth : 0
206
+ return {
207
+ ...prev ,
208
+ [ next . id ] : next . relativeWidth
209
+ ? getWidthOfColumn ( next . id , next . relativeWidth , width , true )
210
+ : getWidthOfColumn ( next . id , propAbsWidth , width )
211
+ }
212
+ } , { } )
213
+ )
214
+ }
215
+
216
+ if ( columnWidthSizes ) {
217
+ setColumnWidthSizes ( ( colWidthSizesPrev ) => {
218
+ const newSizes : ColumnWidthSizes = { }
219
+ // eslint-disable-next-line guard-for-in,no-restricted-syntax
220
+ for ( const col in colWidthSizesPrev ) {
221
+ newSizes [ col ] = getWidthOfColumn ( col , colWidthSizesPrev [ col ] . relative , width , true )
222
+ }
223
+ return newSizes
224
+ } )
225
+ }
226
+
163
227
cellCache ?. clearAll ( )
164
228
}
165
229
230
+ const onDragColumn = ( e : React . MouseEvent ) => {
231
+ const { column, x, active } = resizeColRef . current
232
+ if ( active && column ) {
233
+ const diffX = x - e . clientX
234
+ setColumnWidthSizes ( ( prev ) => {
235
+ if ( ! prev ) return null
236
+
237
+ resizeColRef . current . x = e . clientX
238
+ return ( {
239
+ ...prev ,
240
+ [ column ] : getWidthOfColumn ( column , prev [ column ] . abs - diffX , width )
241
+ } )
242
+ } )
243
+
244
+ cellCache ?. clearAll ( )
245
+ }
246
+ }
247
+
248
+ const onDragColumnStart = ( e : React . MouseEvent , column : ITableColumn ) => {
249
+ resizeColRef . current = {
250
+ column : column . id ,
251
+ active : true ,
252
+ x : e . clientX
253
+ }
254
+ setIsColResizing ( true )
255
+ }
256
+
257
+ const onDragColumnEnd = ( ) => {
258
+ if ( resizeColRef . current . active ) {
259
+ resizeColRef . current = {
260
+ active : false ,
261
+ column : null ,
262
+ x : 0
263
+ }
264
+ setIsColResizing ( false )
265
+ cellCache ?. clearAll ( )
266
+
267
+ if ( columnWidthSizes ) {
268
+ onColResizeEnd ?.(
269
+ Object . keys ( columnWidthSizes )
270
+ . reduce ( ( prev , next ) => ( {
271
+ ...prev ,
272
+ [ next ] : columnWidthSizes [ next ] . relative
273
+ } ) , { } )
274
+ )
275
+ }
276
+ }
277
+ }
278
+
166
279
const checkIfRowSelectable = ( rowData : any ) => ! ! rowData
167
280
168
281
const cellRenderer = ( { cellData, columnIndex, rowData, rowIndex, parent, dataKey } : TableCellProps ) => {
@@ -221,7 +334,10 @@ const VirtualTable = (props: IProps) => {
221
334
const isColumnSorted = sortedColumn && sortedColumn . column === column . id
222
335
223
336
return (
224
- < div className = "flex-row fluid" style = { { justifyContent : column . alignment } } >
337
+ < div
338
+ className = "flex-row fluid"
339
+ style = { { justifyContent : column . alignment , position : 'relative' } }
340
+ >
225
341
{ column . isSortable && ! searching && (
226
342
< div className = { styles . headerCell } style = { { justifyContent : column . alignment } } >
227
343
< button
@@ -271,6 +387,14 @@ const VirtualTable = (props: IProps) => {
271
387
</ button >
272
388
</ div >
273
389
) }
390
+ { column . isResizable && (
391
+ < div
392
+ className = { styles . resizeTrigger }
393
+ onMouseDown = { ( e ) => onDragColumnStart ( e , column ) }
394
+ data-testid = { `resize-trigger-${ column . id } ` }
395
+ role = "presentation"
396
+ />
397
+ ) }
274
398
</ div >
275
399
)
276
400
}
@@ -363,8 +487,12 @@ const VirtualTable = (props: IProps) => {
363
487
{ ( resizeRef ) => (
364
488
< div
365
489
ref = { resizeRef }
366
- className = { styles . container }
490
+ className = { cx ( styles . container , { [ styles . isResizing ] : isColResizing } ) }
367
491
onWheel = { onWheel }
492
+ onMouseMove = { onDragColumn }
493
+ onMouseUp = { onDragColumnEnd }
494
+ onMouseLeave = { onDragColumnEnd }
495
+ role = "presentation"
368
496
data-testid = "virtual-table-container"
369
497
>
370
498
{ loading && ! hideProgress && (
@@ -381,7 +509,7 @@ const VirtualTable = (props: IProps) => {
381
509
minimumBatchSize = { SCAN_COUNT_DEFAULT }
382
510
threshold = { threshold }
383
511
loadMoreRows = { loadMoreRows }
384
- rowCount = { totalItemsCount }
512
+ rowCount = { totalItemsCount || undefined }
385
513
>
386
514
{ ( { onRowsRendered, registerChild } ) => (
387
515
< Table
@@ -423,11 +551,9 @@ const VirtualTable = (props: IProps) => {
423
551
maxWidth = { column . maxWidth }
424
552
label = { column . label }
425
553
dataKey = { column . id }
426
- width = {
427
- column . absoluteWidth || column . relativeWidth
428
- ? column . relativeWidth ?? 0
429
- : 20
430
- }
554
+ width = { columnWidthSizes ?. [ column . id ] ?. abs || (
555
+ column . absoluteWidth || column . relativeWidth ? column . relativeWidth ?? 0 : 20
556
+ ) }
431
557
flexGrow = { ! column . absoluteWidth && ! column . relativeWidth ? 1 : 0 }
432
558
headerRenderer = { ( headerProps ) =>
433
559
headerRenderer ( {
@@ -448,7 +574,7 @@ const VirtualTable = (props: IProps) => {
448
574
< div className = { cx ( styles . tableFooter ) } >
449
575
< KeysSummary
450
576
scanned = { scanned }
451
- totalItemsCount = { totalItemsCount }
577
+ totalItemsCount = { totalItemsCount || undefined }
452
578
loading = { loading }
453
579
loadMoreItems = { loadMoreItems }
454
580
items = { items }
0 commit comments