forked from jaegertracing/jaeger-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
484 lines (453 loc) · 15.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
// @flow
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import * as React from 'react';
import Positions from './Positions';
/**
* @typedef
*/
type ListViewProps = {
/**
* Number of elements in the list.
*/
dataLength: number,
/**
* Convert item index (number) to the key (string). ListView uses both indexes
* and keys to handle the addtion of new rows.
*/
getIndexFromKey: string => number,
/**
* Convert item key (string) to the index (number). ListView uses both indexes
* and keys to handle the addtion of new rows.
*/
getKeyFromIndex: number => string,
/**
* Number of items to draw and add to the DOM, initially.
*/
initialDraw: number,
/**
* The parent provides fallback height measurements when there is not a
* rendered element to measure.
*/
itemHeightGetter: (number, string) => number,
/**
* Function that renders an item; rendered items are added directly to the
* DOM, they are not wrapped in list item wrapper HTMLElement.
*/
itemRenderer: (string, {}, number, {}) => React.Node,
/**
* `className` for the HTMLElement that holds the items.
*/
itemsWrapperClassName?: string,
/**
* When adding new items to the DOM, this is the number of items to add above
* and below the current view. E.g. if list is 100 items and is srcolled
* halfway down (so items [46, 55] are in view), then when a new range of
* items is rendered, it will render items `46 - viewBuffer` to
* `55 + viewBuffer`.
*/
viewBuffer: number,
/**
* The minimum number of items offscreen in either direction; e.g. at least
* `viewBuffer` number of items must be off screen above and below the
* current view, or more items will be rendered.
*/
viewBufferMin: number,
/**
* When `true`, expect `_wrapperElm` to have `overflow: visible` and to,
* essentially, be tall to the point the entire page will will end up
* scrolling as a result of the ListView. Similar to react-virtualized
* window scroller.
*
* - Ref: https://bvaughn.github.io/react-virtualized/#/components/WindowScroller
* - Ref:https://github.com/bvaughn/react-virtualized/blob/497e2a1942529560681d65a9ef9f5e9c9c9a49ba/docs/WindowScroller.md
*/
windowScroller?: boolean,
};
/**
* Virtualized list view component, for the most part, only renders the window
* of items that are in-view with some buffer before and after. Listens for
* scroll events and updates which items are rendered. See react-virtualized
* for a suite of components with similar, but generalized, functinality.
* https://github.com/bvaughn/react-virtualized
*
* Note: Presently, ListView cannot be a PureComponent. This is because ListView
* is sensitive to the underlying state that drives the list items, but it
* doesn't actually receive that state. So, a render may still be required even
* if ListView's props are unchanged.
*
* @export
* @class ListView
*/
export default class ListView extends React.Component<ListViewProps> {
props: ListViewProps;
/**
* Keeps track of the height and y-value of items, by item index, in the
* ListView.
*/
_yPositions: Positions;
/**
* Keep track of the known / measured heights of the rendered items; populated
* with values through observation and keyed on the item key, not the item
* index.
*/
_knownHeights: Map<string, number>;
/**
* The start index of the items currently drawn.
*/
_startIndexDrawn: number;
/**
* The end index of the items currently drawn.
*/
_endIndexDrawn: number;
/**
* The start index of the items currently in view.
*/
_startIndex: number;
/**
* The end index of the items currently in view.
*/
_endIndex: number;
/**
* Height of the visual window, e.g. height of the scroller element.
*/
_viewHeight: number;
/**
* `scrollTop` of the current scroll position.
*/
_scrollTop: number;
/**
* Used to keep track of whether or not a re-calculation of what should be
* drawn / viewable has been scheduled.
*/
_isScrolledOrResized: boolean;
/**
* If `windowScroller` is true, this notes how far down the page the scroller
* is located. (Note: repositioning and below-the-fold views are untested)
*/
_htmlTopOffset: number;
_windowScrollListenerAdded: boolean;
_htmlElm: HTMLElement;
/**
* HTMLElement holding the scroller.
*/
_wrapperElm: ?HTMLElement;
/**
* HTMLElement holding the rendered items.
*/
_itemHolderElm: ?HTMLElement;
static defaultProps = {
/**
* E.g.`str => Number(str)`
*/
getIndexFromKey: Number,
/**
* E.g.`num => String(num)`
*/
getKeyFromIndex: String,
initialDraw: 300,
itemsWrapperClassName: '',
viewBuffer: 90,
viewBufferMin: 30,
windowScroller: false,
};
constructor(props: ListViewProps) {
super(props);
this._yPositions = new Positions(200);
// _knownHeights is (item-key -> observed height) of list items
this._knownHeights = new Map();
this._startIndexDrawn = 2 ** 20;
this._endIndexDrawn = -(2 ** 20);
this._startIndex = 0;
this._endIndex = 0;
this._viewHeight = -1;
this._scrollTop = -1;
this._isScrolledOrResized = false;
this._htmlTopOffset = -1;
this._windowScrollListenerAdded = false;
// _htmlElm is only relevant if props.windowScroller is true
this._htmlElm = (document.documentElement: any);
this._wrapperElm = undefined;
this._itemHolderElm = undefined;
}
componentDidMount() {
if (this.props.windowScroller) {
if (this._wrapperElm) {
const { top } = this._wrapperElm.getBoundingClientRect();
this._htmlTopOffset = top + this._htmlElm.scrollTop;
}
window.addEventListener('scroll', this._onScroll);
this._windowScrollListenerAdded = true;
}
}
componentDidUpdate() {
if (this._itemHolderElm) {
this._scanItemHeights();
}
}
componentWillUnmount() {
if (this._windowScrollListenerAdded) {
window.removeEventListener('scroll', this._onScroll);
}
}
getViewHeight = () => this._viewHeight;
/**
* Get the index of the item at the bottom of the current view.
*/
getBottomVisibleIndex = (): number => {
const bottomY = this._scrollTop + this._viewHeight;
return this._yPositions.findFloorIndex(bottomY, this._getHeight);
};
/**
* Get the index of the item at the top of the current view.
*/
getTopVisibleIndex = (): number => this._yPositions.findFloorIndex(this._scrollTop, this._getHeight);
getRowPosition = (index: number): { height: number, y: number } =>
this._yPositions.getRowPosition(index, this._getHeight);
/**
* Scroll event listener that schedules a remeasuring of which items should be
* rendered.
*/
_onScroll = () => {
if (!this._isScrolledOrResized) {
this._isScrolledOrResized = true;
window.requestAnimationFrame(this._positionList);
}
};
/**
* Returns true is the view height (scroll window) or scroll position have
* changed.
*/
_isViewChanged() {
if (!this._wrapperElm) {
return false;
}
const useRoot = this.props.windowScroller;
const clientHeight = useRoot ? this._htmlElm.clientHeight : this._wrapperElm.clientHeight;
const scrollTop = useRoot ? this._htmlElm.scrollTop : this._wrapperElm.scrollTop;
return clientHeight !== this._viewHeight || scrollTop !== this._scrollTop;
}
/**
* Recalculate _startIndex and _endIndex, e.g. which items are in view.
*/
_calcViewIndexes() {
const useRoot = this.props.windowScroller;
// funky if statement is to satisfy flow
if (!useRoot) {
if (!this._wrapperElm) {
this._viewHeight = -1;
this._startIndex = 0;
this._endIndex = 0;
return;
}
this._viewHeight = this._wrapperElm.clientHeight;
this._scrollTop = this._wrapperElm.scrollTop;
} else {
this._viewHeight = window.innerHeight - this._htmlTopOffset;
this._scrollTop = window.scrollY;
}
const yStart = this._scrollTop;
const yEnd = this._scrollTop + this._viewHeight;
this._startIndex = this._yPositions.findFloorIndex(yStart, this._getHeight);
this._endIndex = this._yPositions.findFloorIndex(yEnd, this._getHeight);
}
/**
* Checked to see if the currently rendered items are sufficient, if not,
* force an update to trigger more items to be rendered.
*/
_positionList = () => {
this._isScrolledOrResized = false;
if (!this._wrapperElm) {
return;
}
this._calcViewIndexes();
// indexes drawn should be padded by at least props.viewBufferMin
const maxStart =
this.props.viewBufferMin > this._startIndex ? 0 : this._startIndex - this.props.viewBufferMin;
const minEnd =
this.props.viewBufferMin < this.props.dataLength - this._endIndex
? this._endIndex + this.props.viewBufferMin
: this.props.dataLength - 1;
if (maxStart < this._startIndexDrawn || minEnd > this._endIndexDrawn) {
// console.time('force update');
// setTimeout(() => console.timeEnd('force update'), 0);
this.forceUpdate();
}
};
_initWrapper = (elm: HTMLElement) => {
this._wrapperElm = elm;
if (!this.props.windowScroller) {
this._viewHeight = elm && elm.clientHeight;
}
};
_initItemHolder = (elm: HTMLElement) => {
this._itemHolderElm = elm;
this._scanItemHeights();
};
/**
* Go through all items that are rendered and save their height based on their
* item-key (which is on a data-* attribute). If any new or adjusted heights
* are found, re-measure the current known y-positions (via .yPositions).
*/
_scanItemHeights = () => {
const getIndexFromKey = this.props.getIndexFromKey;
if (!this._itemHolderElm) {
return;
}
// note the keys for the first and last altered heights, the `yPositions`
// needs to be updated
let lowDirtyKey = null;
let highDirtyKey = null;
let isDirty = false;
// iterating childNodes is faster than children
// https://jsperf.com/large-htmlcollection-vs-large-nodelist
const nodes = this._itemHolderElm.childNodes;
const max = nodes.length;
for (let i = 0; i < max; i++) {
const node: HTMLElement = (nodes[i]: any);
// use `.getAttribute(...)` instead of `.dataset` for jest / JSDOM
const itemKey = node.getAttribute('data-item-key');
if (!itemKey) {
// eslint-disable-next-line no-console
console.warn('itemKey not found');
continue;
}
// measure the first child, if it's available, otherwise the node itself
// (likely not transferable to other contexts, and instead is specific to
// how we have the items rendered)
const measureSrc: Element = node.firstElementChild || node;
const observed = measureSrc.clientHeight;
const known = this._knownHeights.get(itemKey);
if (observed !== known) {
this._knownHeights.set(itemKey, observed);
if (!isDirty) {
isDirty = true;
// eslint-disable-next-line no-multi-assign
lowDirtyKey = highDirtyKey = itemKey;
} else {
highDirtyKey = itemKey;
}
}
}
if (lowDirtyKey != null && highDirtyKey != null) {
// update yPositions, then redraw
const imin = getIndexFromKey(lowDirtyKey);
const imax = highDirtyKey === lowDirtyKey ? imin : getIndexFromKey(highDirtyKey);
this._yPositions.calcHeights(imax, this._getHeight, imin);
this.forceUpdate();
}
};
/**
* Get the height of the element at index `i`; first check the known heigths,
* fallbck to `.props.itemHeightGetter(...)`.
*/
_getHeight = (i: number) => {
const key = this.props.getKeyFromIndex(i);
const known = this._knownHeights.get(key);
// known !== known iff known is NaN
// eslint-disable-next-line no-self-compare
if (known != null && known === known) {
return known;
}
return this.props.itemHeightGetter(i, key);
};
render() {
const { dataLength, getKeyFromIndex, initialDraw, itemRenderer, viewBuffer, viewBufferMin } = this.props;
const heightGetter = this._getHeight;
const items = [];
let start;
let end;
if (!this._wrapperElm) {
start = 0;
end = (initialDraw < dataLength ? initialDraw : dataLength) - 1;
} else {
if (this._isViewChanged()) {
this._calcViewIndexes();
}
const maxStart = viewBufferMin > this._startIndex ? 0 : this._startIndex - viewBufferMin;
const minEnd =
viewBufferMin < dataLength - this._endIndex ? this._endIndex + viewBufferMin : dataLength - 1;
if (maxStart < this._startIndexDrawn || minEnd > this._endIndexDrawn) {
start = viewBuffer > this._startIndex ? 0 : this._startIndex - viewBuffer;
end = this._endIndex + viewBuffer;
if (end >= dataLength) {
end = dataLength - 1;
}
} else {
start = this._startIndexDrawn;
end = this._endIndexDrawn > dataLength - 1 ? dataLength - 1 : this._endIndexDrawn;
}
}
this._yPositions.profileData(dataLength);
this._yPositions.calcHeights(end, heightGetter, start || -1);
this._startIndexDrawn = start;
this._endIndexDrawn = end;
items.length = end - start + 1;
for (let i = start; i <= end; i++) {
const { y: top, height } = this._yPositions.getRowPosition(i, heightGetter);
const style = {
height,
top,
position: 'absolute',
};
const itemKey = getKeyFromIndex(i);
const attrs = { 'data-item-key': itemKey };
items.push(itemRenderer(itemKey, style, i, attrs));
}
type wrapperPropsT = {
style: { [string]: string },
ref: Function,
onScroll?: Function,
};
const wrapperProps: wrapperPropsT = {
style: {
overflowY: 'auto',
position: 'relative',
height: '100%',
},
ref: this._initWrapper,
};
if (!this.props.windowScroller) {
wrapperProps.onScroll = this._onScroll;
}
const scrollerStyle = {
position: 'relative',
height: this._yPositions.getEstimatedHeight(),
};
return (
<div {...wrapperProps}>
<div style={scrollerStyle}>
<div
style={{
position: 'absolute',
top: 0,
margin: 0,
padding: 0,
}}
className={this.props.itemsWrapperClassName}
ref={(this._initItemHolder: Function)}
>
{items}
</div>
</div>
</div>
);
}
}