@@ -38,6 +38,10 @@ export interface DefaultDiscoverTableProps {
38
38
scrollToTop ?: ( ) => void ;
39
39
}
40
40
41
+ // ToDo: These would need to be read from an upcoming config panel
42
+ const PAGINATED_PAGE_SIZE = 50 ;
43
+ const INFINITE_SCROLLED_PAGE_SIZE = 10 ;
44
+
41
45
const DefaultDiscoverTableUI = ( {
42
46
columns,
43
47
hits,
@@ -70,52 +74,75 @@ const DefaultDiscoverTableUI = ({
70
74
isShortDots
71
75
) ;
72
76
const displayedColumnNames = displayedColumns . map ( ( column ) => column . name ) ;
73
- const pageSize = 10 ;
74
- const [ renderedRowCount , setRenderedRowCount ] = useState ( pageSize ) ; // Start with 10 rows
75
- const [ displayedRows , setDisplayedRows ] = useState ( rows . slice ( 0 , pageSize ) ) ;
77
+
78
+ /* INFINITE_SCROLLED_PAGE_SIZE:
79
+ * Infinitely scrolling, a page of 10 rows is shown and then 4 pages are lazy-loaded for a total of 5 pages.
80
+ * * The lazy-loading is mindful of the performance by monitoring the fps of the browser.
81
+ * *`renderedRowCount` and `desiredRowCount` are only used in this method.
82
+ *
83
+ * PAGINATED_PAGE_SIZE
84
+ * Paginated, the view is broken into pages of 50 rows.
85
+ * * `displayedRows` and `currentRowCounts` are only used in this method.
86
+ */
87
+ const [ renderedRowCount , setRenderedRowCount ] = useState ( INFINITE_SCROLLED_PAGE_SIZE ) ;
88
+ const [ desiredRowCount , setDesiredRowCount ] = useState (
89
+ Math . min ( rows . length , 5 * INFINITE_SCROLLED_PAGE_SIZE )
90
+ ) ;
91
+ const [ displayedRows , setDisplayedRows ] = useState ( rows . slice ( 0 , PAGINATED_PAGE_SIZE ) ) ;
76
92
const [ currentRowCounts , setCurrentRowCounts ] = useState ( {
77
93
startRow : 0 ,
78
- endRow : rows . length < pageSize ? rows . length : pageSize ,
94
+ endRow : rows . length < PAGINATED_PAGE_SIZE ? rows . length : PAGINATED_PAGE_SIZE ,
79
95
} ) ;
96
+
80
97
const observerRef = useRef < IntersectionObserver | null > ( null ) ;
81
- const [ sentinelEle , setSentinelEle ] = useState < HTMLDivElement > ( ) ;
82
- // Need a callback ref since the element isn't set on the first render.
98
+ // `sentinelElement` is attached to the bottom of the table to observe when the table is scrolled all the way.
99
+ const [ sentinelElement , setSentinelElement ] = useState < HTMLDivElement > ( ) ;
100
+ // `tableElement` is used for first auto-sizing and then fixing column widths
101
+ const [ tableElement , setTableElement ] = useState < HTMLTableElement > ( ) ;
102
+ // Both need callback refs since the elements aren't set on the first render.
83
103
const sentinelRef = useCallback ( ( node : HTMLDivElement | null ) => {
84
104
if ( node !== null ) {
85
- setSentinelEle ( node ) ;
105
+ setSentinelElement ( node ) ;
106
+ }
107
+ } , [ ] ) ;
108
+ const tableRef = useCallback ( ( el : HTMLTableElement | null ) => {
109
+ if ( el !== null ) {
110
+ setTableElement ( el ) ;
86
111
}
87
112
} , [ ] ) ;
88
113
89
114
useEffect ( ( ) => {
90
- if ( sentinelEle ) {
115
+ if ( sentinelElement && ! showPagination ) {
91
116
observerRef . current = new IntersectionObserver (
92
117
( entries ) => {
93
118
if ( entries [ 0 ] . isIntersecting ) {
94
- setRenderedRowCount ( ( prevRowCount ) => prevRowCount + pageSize ) ; // Load 50 more rows
119
+ // Load another batch of rows, some immediately and some lazily
120
+ setRenderedRowCount ( ( prevRowCount ) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE ) ;
121
+ setDesiredRowCount ( ( prevRowCount ) => prevRowCount + 5 * INFINITE_SCROLLED_PAGE_SIZE ) ;
95
122
}
96
123
} ,
97
124
{ threshold : 1.0 }
98
125
) ;
99
126
100
- observerRef . current . observe ( sentinelEle ) ;
127
+ observerRef . current . observe ( sentinelElement ) ;
101
128
}
102
129
103
130
return ( ) => {
104
- if ( observerRef . current && sentinelEle ) {
105
- observerRef . current . unobserve ( sentinelEle ) ;
131
+ if ( observerRef . current && sentinelElement ) {
132
+ observerRef . current . unobserve ( sentinelElement ) ;
106
133
}
107
134
} ;
108
- } , [ sentinelEle ] ) ;
135
+ } , [ sentinelElement , showPagination ] ) ;
109
136
137
+ // Page management when using a paginated table
110
138
const [ activePage , setActivePage ] = useState ( 0 ) ;
111
- const pageCount = Math . ceil ( rows . length / pageSize ) ;
112
-
139
+ const pageCount = Math . ceil ( rows . length / PAGINATED_PAGE_SIZE ) ;
113
140
const goToPage = ( pageNumber : number ) => {
114
- const startRow = pageNumber * pageSize ;
141
+ const startRow = pageNumber * PAGINATED_PAGE_SIZE ;
115
142
const endRow =
116
- rows . length < pageNumber * pageSize + pageSize
143
+ rows . length < pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE
117
144
? rows . length
118
- : pageNumber * pageSize + pageSize ;
145
+ : pageNumber * PAGINATED_PAGE_SIZE + PAGINATED_PAGE_SIZE ;
119
146
setCurrentRowCounts ( {
120
147
startRow,
121
148
endRow,
@@ -124,6 +151,60 @@ const DefaultDiscoverTableUI = ({
124
151
setActivePage ( pageNumber ) ;
125
152
} ;
126
153
154
+ // Lazy-loader of rows
155
+ const lazyLoadRequestFrameRef = useRef < number > ( 0 ) ;
156
+ const lazyLoadLastTimeRef = useRef < number > ( 0 ) ;
157
+
158
+ React . useEffect ( ( ) => {
159
+ if ( ! showPagination ) {
160
+ const loadMoreRows = ( time : number ) => {
161
+ if ( renderedRowCount < desiredRowCount ) {
162
+ // Load more rows only if fps > 30, when calls are less than 33ms apart
163
+ if ( time - lazyLoadLastTimeRef . current < 33 ) {
164
+ setRenderedRowCount ( ( prevRowCount ) => prevRowCount + INFINITE_SCROLLED_PAGE_SIZE ) ;
165
+ }
166
+ lazyLoadLastTimeRef . current = time ;
167
+ lazyLoadRequestFrameRef . current = requestAnimationFrame ( loadMoreRows ) ;
168
+ }
169
+ } ;
170
+ lazyLoadRequestFrameRef . current = requestAnimationFrame ( loadMoreRows ) ;
171
+ }
172
+
173
+ return ( ) => cancelAnimationFrame ( lazyLoadRequestFrameRef . current ) ;
174
+ } , [ showPagination , renderedRowCount , desiredRowCount ] ) ;
175
+
176
+ // Allow auto column-sizing using the initially rendered rows and then convert to fixed
177
+ const tableLayoutRequestFrameRef = useRef < number > ( 0 ) ;
178
+
179
+ useEffect ( ( ) => {
180
+ if ( tableElement ) {
181
+ // Load the first batch of rows and adjust the columns to the contents
182
+ tableElement . style . tableLayout = 'auto' ;
183
+
184
+ tableLayoutRequestFrameRef . current = requestAnimationFrame ( ( ) => {
185
+ if ( tableElement ) {
186
+ /* Get the widths for each header cell which is the column's width automatically adjusted to the content of
187
+ * the column. Apply the width as a style and change the layout to fixed. This is to
188
+ * 1) prevent columns from changing size when more rows are added, and
189
+ * 2) speed of rendering time of subsequently added rows.
190
+ *
191
+ * First cell is skipped because it has a dimention set already, and the last cell is skipped to allow it to
192
+ * grow as much as the table needs.
193
+ */
194
+ tableElement
195
+ . querySelectorAll ( 'thead > tr > th:not(:first-child):not(:last-child)' )
196
+ . forEach ( ( th ) => {
197
+ ( th as HTMLTableCellElement ) . style . width = th . getBoundingClientRect ( ) . width + 'px' ;
198
+ } ) ;
199
+
200
+ tableElement . style . tableLayout = 'fixed' ;
201
+ }
202
+ } ) ;
203
+ }
204
+
205
+ return ( ) => cancelAnimationFrame ( tableLayoutRequestFrameRef . current ) ;
206
+ } , [ columns , tableElement ] ) ;
207
+
127
208
return (
128
209
indexPattern && (
129
210
< >
@@ -138,7 +219,7 @@ const DefaultDiscoverTableUI = ({
138
219
sampleSize = { sampleSize }
139
220
/>
140
221
) : null }
141
- < table data-test-subj = "docTable" className = "osd-table table" >
222
+ < table data-test-subj = "docTable" className = "osd-table table" ref = { tableRef } >
142
223
< thead >
143
224
< TableHeader
144
225
displayedColumns = { displayedColumns }
@@ -155,7 +236,7 @@ const DefaultDiscoverTableUI = ({
155
236
( row : OpenSearchSearchHit , index : number ) => {
156
237
return (
157
238
< TableRow
158
- key = { index }
239
+ key = { row . _id }
159
240
row = { row }
160
241
columns = { displayedColumnNames }
161
242
indexPattern = { indexPattern }
0 commit comments