Trace View: Span list visual update (#75238)

* Show color of row as a border under the row. Hide service name for sequential spans

* Increase default span name column width. Smaller font for service and span names in span list

* New background color on spans. Fixed hover of indent markers

* Service name and span name style tweaks

* Collapse hidden levels

* Fixed test

* Small tweak to Buffer size to make sure tests pass

* Trigger runs

* Update betterer results

* Address comment

* Style tweaks

* Remove duplicated code

* Rollback change to join <span> since they are needed for the tests
This commit is contained in:
Andre Pereira 2023-10-04 14:00:40 +01:00 committed by GitHub
parent 65fa94b16b
commit 607a664ef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 76 deletions

View File

@ -4163,14 +4163,17 @@ exports[`better eslint`] = {
], ],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx:5381": [ "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"] [0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"]
], ],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx:5381": [ "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanTreeOffset.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"], [0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"], [0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"] [0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"]
], ],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx:5381": [ "public/app/features/explore/TraceView/components/TraceTimelineViewer/Ticks.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],

View File

@ -91,7 +91,7 @@ export function TraceView(props: Props) {
/** /**
* Keeps state of resizable name column width * Keeps state of resizable name column width
*/ */
const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25); const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.4);
const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({ const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({
refId: props.dataFrames[0]?.refId, refId: props.dataFrames[0]?.refId,

View File

@ -73,7 +73,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
`, `,
endpointName: css` endpointName: css`
label: endpointName; label: endpointName;
color: ${autoColor(theme, '#808080')}; color: ${autoColor(theme, '#484848')};
font-size: 0.9em;
`, `,
view: css` view: css`
label: view; label: view;
@ -91,6 +92,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
`, `,
row: css` row: css`
label: row; label: row;
font-size: 0.9em;
&:hover .${spanBarClassName} { &:hover .${spanBarClassName} {
opacity: 1; opacity: 1;
} }
@ -213,7 +215,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
outline: none; outline: none;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
margin-right: 8px;
padding-left: 4px; padding-left: 4px;
padding-right: 0.25em; padding-right: 0.25em;
position: relative; position: relative;
@ -222,24 +223,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
&::before {
content: ' ';
position: absolute;
top: 4px;
bottom: 4px;
left: 0;
border-left: 4px solid;
border-left-color: inherit;
}
&:focus { &:focus {
text-decoration: none; text-decoration: none;
} }
&:hover > small { &:hover > span {
color: ${autoColor(theme, '#000')}; color: ${autoColor(theme, '#000')};
} }
text-align: left; text-align: left;
background: transparent; background: transparent;
border: none; border: none;
border-bottom-width: 1px;
border-bottom-style: solid;
`, `,
nameDetailExpanded: css` nameDetailExpanded: css`
label: nameDetailExpanded; label: nameDetailExpanded;
@ -249,8 +243,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, showSpanFilterMatchesOnly
`, `,
svcName: css` svcName: css`
label: svcName; label: svcName;
padding: 0 0.25rem 0 0.5rem; font-size: 0.9em;
font-size: 1.05em; font-weight: bold;
margin-right: 0.25rem;
`, `,
svcNameChildrenCollapsed: css` svcNameChildrenCollapsed: css`
label: svcNameChildrenCollapsed; label: svcNameChildrenCollapsed;
@ -301,6 +296,7 @@ export type SpanBarRowProps = {
onDetailToggled: (spanID: string) => void; onDetailToggled: (spanID: string) => void;
onChildrenToggled: (spanID: string) => void; onChildrenToggled: (spanID: string) => void;
numTicks: number; numTicks: number;
showServiceName: boolean;
rpc?: rpc?:
| { | {
viewStart: number; viewStart: number;
@ -327,6 +323,7 @@ export type SpanBarRowProps = {
clippingRight?: boolean; clippingRight?: boolean;
createSpanLink?: SpanLinkFunc; createSpanLink?: SpanLinkFunc;
datasourceType: string; datasourceType: string;
visibleSpanIds: string[];
}; };
/** /**
@ -378,6 +375,8 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
theme, theme,
createSpanLink, createSpanLink,
datasourceType, datasourceType,
showServiceName,
visibleSpanIds,
} = this.props; } = this.props;
const { const {
duration, duration,
@ -432,6 +431,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
hoverIndentGuideIds={hoverIndentGuideIds} hoverIndentGuideIds={hoverIndentGuideIds}
addHoverIndentGuideId={addHoverIndentGuideId} addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId}
visibleSpanIds={visibleSpanIds}
/> />
<button <button
type="button" type="button"
@ -440,43 +440,45 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
title={labelDetail} title={labelDetail}
onClick={this._detailToggle} onClick={this._detailToggle}
role="switch" role="switch"
style={{ borderColor: color }} style={{ background: `${color}10`, borderBottomColor: `${color}CF` }}
tabIndex={0} tabIndex={0}
> >
<span {showErrorIcon && (
className={cx(styles.svcName, { <Icon
[styles.svcNameChildrenCollapsed]: isParent && !isChildrenExpanded, name={'exclamation-circle'}
})} style={{
> backgroundColor: span.errorIconColor
{showErrorIcon && ( ? autoColor(theme, span.errorIconColor)
<Icon : autoColor(theme, '#db2828'),
name={'exclamation-circle'} }}
style={{ className={styles.errorIcon}
backgroundColor: span.errorIconColor />
? autoColor(theme, span.errorIconColor) )}
: autoColor(theme, '#db2828'), {showServiceName && (
}} <span
className={styles.errorIcon} className={cx(styles.svcName, {
/> [styles.svcNameChildrenCollapsed]: isParent && !isChildrenExpanded,
)} })}
{serviceName}{' '} >
{rpc && ( {`${serviceName} `}
<span> </span>
<Icon name={'arrow-right'} />{' '} )}
<i className={styles.rpcColorMarker} style={{ background: rpc.color }} /> {rpc && (
{rpc.serviceName} <span>
</span> <Icon name={'arrow-right'} />{' '}
)} <i className={styles.rpcColorMarker} style={{ background: rpc.color }} />
{noInstrumentedServer && ( {rpc.serviceName}
<span> </span>
<Icon name={'arrow-right'} />{' '} )}
<i className={styles.rpcColorMarker} style={{ background: noInstrumentedServer.color }} /> {noInstrumentedServer && (
{noInstrumentedServer.serviceName} <span>
</span> <Icon name={'arrow-right'} />{' '}
)} <i className={styles.rpcColorMarker} style={{ background: noInstrumentedServer.color }} />
</span> {noInstrumentedServer.serviceName}
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small> </span>
<small className={styles.endpointName}> {this.getSpanBarLabel(span, spanBarOptions, label)}</small> )}
<span className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</span>
<span className={styles.endpointName}> {this.getSpanBarLabel(span, spanBarOptions, label)}</span>
</button> </button>
{createSpanLink && {createSpanLink &&
(() => { (() => {
@ -492,7 +494,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
href={links[0].href} href={links[0].href}
// Needs to have target otherwise preventDefault would not work due to angularRouter. // Needs to have target otherwise preventDefault would not work due to angularRouter.
target={'_blank'} target={'_blank'}
style={{ marginRight: '5px' }} style={{ background: `${color}10`, borderBottom: `1px solid ${color}CF`, paddingRight: '4px' }}
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={ onClick={
links[0].onClick links[0].onClick
@ -509,7 +511,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
</a> </a>
); );
} else if (links && count > 1) { } else if (links && count > 1) {
return <SpanLinksMenu links={links} datasourceType={datasourceType} />; return <SpanLinksMenu links={links} datasourceType={datasourceType} color={color} />;
} else { } else {
return null; return null;
} }

View File

@ -38,7 +38,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
position: absolute; position: absolute;
width: 100%; width: 100%;
&::before { &::before {
border-left: 4px solid; border-left: 1px solid;
pointer-events: none; pointer-events: none;
width: 1000px; width: 1000px;
} }
@ -97,6 +97,7 @@ export type SpanDetailRowProps = {
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRefType?: TopOfViewRefType; topOfViewRefType?: TopOfViewRefType;
datasourceType: string; datasourceType: string;
visibleSpanIds: string[];
}; };
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> { export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
@ -134,6 +135,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
createFocusSpanLink, createFocusSpanLink,
topOfViewRefType, topOfViewRefType,
datasourceType, datasourceType,
visibleSpanIds,
} = this.props; } = this.props;
const styles = getStyles(theme); const styles = getStyles(theme);
return ( return (
@ -145,6 +147,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
hoverIndentGuideIds={hoverIndentGuideIds} hoverIndentGuideIds={hoverIndentGuideIds}
addHoverIndentGuideId={addHoverIndentGuideId} addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId}
visibleSpanIds={visibleSpanIds}
/> />
<Button <Button
fill="text" fill="text"

View File

@ -9,6 +9,7 @@ import { SpanLinkDef } from '../types/links';
interface SpanLinksProps { interface SpanLinksProps {
links: SpanLinkDef[]; links: SpanLinkDef[];
datasourceType: string; datasourceType: string;
color: string;
} }
const renderMenuItems = ( const renderMenuItems = (
@ -46,15 +47,15 @@ const renderMenuItems = (
)); ));
}; };
export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => { export const SpanLinksMenu = ({ links, datasourceType, color }: SpanLinksProps) => {
const styles = useStyles2(getStyles); const styles = useStyles2(() => getStyles(color));
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const closeMenu = () => setIsMenuOpen(false); const closeMenu = () => setIsMenuOpen(false);
return ( return (
<div data-testid="SpanLinksMenu"> <div data-testid="SpanLinksMenu" className={styles.wrapper}>
<button <button
onClick={(e) => { onClick={(e) => {
setIsMenuOpen(true); setIsMenuOpen(true);
@ -65,7 +66,7 @@ export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => {
}} }}
className={styles.button} className={styles.button}
> >
<Icon name="link" className={styles.button} /> <Icon name="link" className={styles.icon} />
</button> </button>
{isMenuOpen ? ( {isMenuOpen ? (
@ -81,13 +82,23 @@ export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => {
); );
}; };
const getStyles = () => { const getStyles = (color: string) => {
return { return {
wrapper: css`
border: none;
background: ${color}10;
border-bottom: 1px solid ${color}CF;
padding-right: 4px;
`,
button: css` button: css`
background: transparent; background: transparent;
border: none; border: none;
padding: 0; padding: 0;
margin: 0 3px 0 0; `,
icon: css`
background: transparent;
border: none;
padding: 0;
`, `,
menuItem: css` menuItem: css`
max-width: 60ch; max-width: 60ch;

View File

@ -39,6 +39,7 @@ describe('SpanTreeOffset', () => {
addHoverIndentGuideId: jest.fn(), addHoverIndentGuideId: jest.fn(),
hoverIndentGuideIds: new Set(), hoverIndentGuideIds: new Set(),
removeHoverIndentGuideId: jest.fn(), removeHoverIndentGuideId: jest.fn(),
visibleSpanIds: [],
span: { span: {
hasChildren: false, hasChildren: false,
spanID: ownSpanID, spanID: ownSpanID,

View File

@ -40,10 +40,10 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => {
indentGuide: css` indentGuide: css`
label: indentGuide; label: indentGuide;
/* The size of the indentGuide is based off of the iconWrapper */ /* The size of the indentGuide is based off of the iconWrapper */
padding-right: calc(0.5rem + 12px); padding-right: 1rem;
height: 100%; height: 100%;
border-left: 3px solid transparent;
display: inline-flex; display: inline-flex;
transition: padding 300ms ease-out;
&::before { &::before {
content: ''; content: '';
padding-left: 1px; padding-left: 1px;
@ -52,15 +52,17 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`, `,
indentGuideActive: css` indentGuideActive: css`
label: indentGuideActive; label: indentGuideActive;
border-color: ${autoColor(theme, 'darkgrey')};
&::before { &::before {
background-color: transparent; background-color: ${autoColor(theme, '#777')};
} }
`, `,
indentGuideThin: css`
padding-right: 0.3rem;
`,
iconWrapper: css` iconWrapper: css`
label: iconWrapper; label: iconWrapper;
position: absolute; position: absolute;
right: 0.25rem; right: 0;
`, `,
}; };
}); });
@ -75,6 +77,7 @@ export type TProps = {
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: GrafanaTheme2; theme: GrafanaTheme2;
visibleSpanIds: string[];
}; };
export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> { export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
@ -133,25 +136,28 @@ export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
}; };
render() { render() {
const { childrenVisible, onClick, showChildrenIcon, span, theme } = this.props; const { childrenVisible, onClick, showChildrenIcon, span, theme, visibleSpanIds } = this.props;
const { hasChildren, spanID } = span; const { hasChildren, spanID } = span;
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null; const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
const icon = const icon =
showChildrenIcon && showChildrenIcon &&
hasChildren && hasChildren &&
(childrenVisible ? ( (childrenVisible ? (
<Icon name={'angle-down'} data-testid="icon-arrow-down" /> <Icon name={'angle-down'} data-testid="icon-arrow-down" size={'sm'} />
) : ( ) : (
<Icon name={'angle-right'} data-testid="icon-arrow-right" /> <Icon name={'angle-right'} data-testid="icon-arrow-right" size={'sm'} />
)); ));
const styles = getStyles(theme); const styles = getStyles(theme);
return ( return (
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}> <span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
{this.ancestorIds.map((ancestorId) => ( {this.ancestorIds.map((ancestorId, index) => (
<span <span
key={ancestorId} key={ancestorId}
className={cx(styles.indentGuide, { className={cx(styles.indentGuide, {
[styles.indentGuideActive]: this.props.hoverIndentGuideIds.has(ancestorId), [styles.indentGuideActive]: this.props.hoverIndentGuideIds.has(ancestorId),
[styles.indentGuideThin]:
index !== this.ancestorIds.length - 1 && ancestorId !== 'root' && !visibleSpanIds.includes(ancestorId),
})} })}
data-ancestor-id={ancestorId} data-ancestor-id={ancestorId}
data-testid="SpanTreeOffset--indentGuide" data-testid="SpanTreeOffset--indentGuide"

View File

@ -122,6 +122,7 @@ export const DEFAULT_HEIGHTS = {
}; };
const NUM_TICKS = 5; const NUM_TICKS = 5;
const BUFFER_SIZE = 33;
function generateRowStates( function generateRowStates(
spans: TraceSpan[] | TNil, spans: TraceSpan[] | TNil,
@ -331,9 +332,15 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
renderRow = (key: string, style: React.CSSProperties, index: number, attrs: {}) => { renderRow = (key: string, style: React.CSSProperties, index: number, attrs: {}) => {
const { isDetail, span, spanIndex } = this.getRowStates()[index]; const { isDetail, span, spanIndex } = this.getRowStates()[index];
// Compute the list of currently visible span IDs to pass to the row renderers.
const start = Math.max((this.listView?.getTopVisibleIndex() || 0) - BUFFER_SIZE, 0);
const end = (this.listView?.getBottomVisibleIndex() || 0) + BUFFER_SIZE;
const visibleSpanIds = this.getVisibleSpanIds(start, end);
return isDetail return isDetail
? this.renderSpanDetailRow(span, key, style, attrs) ? this.renderSpanDetailRow(span, key, style, attrs, visibleSpanIds)
: this.renderSpanBarRow(span, spanIndex, key, style, attrs); : this.renderSpanBarRow(span, spanIndex, key, style, attrs, visibleSpanIds);
}; };
scrollToSpan = (headerHeight: number, spanID?: string) => { scrollToSpan = (headerHeight: number, spanID?: string) => {
@ -346,7 +353,14 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
} }
}; };
renderSpanBarRow(span: TraceSpan, spanIndex: number, key: string, style: React.CSSProperties, attrs: {}) { renderSpanBarRow(
span: TraceSpan,
spanIndex: number,
key: string,
style: React.CSSProperties,
attrs: {},
visibleSpanIds: string[]
) {
const { spanID } = span; const { spanID } = span;
const { serviceName } = span.process; const { serviceName } = span.process;
const { const {
@ -406,6 +420,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
}; };
} }
const prevSpan = spanIndex > 0 ? trace.spans[spanIndex - 1] : null;
const styles = getStyles(this.props); const styles = getStyles(this.props);
return ( return (
<div className={styles.row} key={key} style={style} {...attrs}> <div className={styles.row} key={key} style={style} {...attrs}>
@ -434,12 +450,14 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
removeHoverIndentGuideId={removeHoverIndentGuideId} removeHoverIndentGuideId={removeHoverIndentGuideId}
createSpanLink={createSpanLink} createSpanLink={createSpanLink}
datasourceType={datasourceType} datasourceType={datasourceType}
showServiceName={prevSpan === null || prevSpan.process.serviceName !== span.process.serviceName}
visibleSpanIds={visibleSpanIds}
/> />
</div> </div>
); );
} }
renderSpanDetailRow(span: TraceSpan, key: string, style: React.CSSProperties, attrs: {}) { renderSpanDetailRow(span: TraceSpan, key: string, style: React.CSSProperties, attrs: {}, visibleSpanIds: string[]) {
const { spanID } = span; const { spanID } = span;
const { serviceName } = span.process; const { serviceName } = span.process;
const { const {
@ -473,6 +491,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
} }
const color = getColorByKey(serviceName, theme); const color = getColorByKey(serviceName, theme);
const styles = getStyles(this.props); const styles = getStyles(this.props);
return ( return (
<div className={styles.row} key={key} style={{ ...style, zIndex: 1 }} {...attrs}> <div className={styles.row} key={key} style={{ ...style, zIndex: 1 }} {...attrs}>
<SpanDetailRow <SpanDetailRow
@ -500,6 +519,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createFocusSpanLink={createFocusSpanLink} createFocusSpanLink={createFocusSpanLink}
topOfViewRefType={topOfViewRefType} topOfViewRefType={topOfViewRefType}
datasourceType={datasourceType} datasourceType={datasourceType}
visibleSpanIds={visibleSpanIds}
/> />
</div> </div>
); );
@ -516,9 +536,21 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
}); });
}; };
getVisibleSpanIds = memoizeOne((start: number, end: number) => {
const spanIds = [];
for (let i = start; i < end; i++) {
const rowState = this.getRowStates()[i];
if (rowState?.span) {
spanIds.push(rowState.span.spanID);
}
}
return spanIds;
});
render() { render() {
const styles = getStyles(this.props); const styles = getStyles(this.props);
const { scrollElement } = this.props; const { scrollElement } = this.props;
return ( return (
<> <>
<ListView <ListView
@ -526,8 +558,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
dataLength={this.getRowStates().length} dataLength={this.getRowStates().length}
itemHeightGetter={this.getRowHeight} itemHeightGetter={this.getRowHeight}
itemRenderer={this.renderRow} itemRenderer={this.renderRow}
viewBuffer={50} viewBuffer={BUFFER_SIZE}
viewBufferMin={50} viewBufferMin={BUFFER_SIZE}
itemsWrapperClassName={styles.rowsWrapper} itemsWrapperClassName={styles.rowsWrapper}
getKeyFromIndex={this.getKeyFromIndex} getKeyFromIndex={this.getKeyFromIndex}
getIndexFromKey={this.getIndexFromKey} getIndexFromKey={this.getIndexFromKey}