-
Notifications
You must be signed in to change notification settings - Fork 11.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Table: Highlight row on shared crosshair (#78392)
* bidirectional shared crosshair table WIP * add shared crosshair to table panel * lower around point threshold * add feature toggle * add index based verification * add adaptive threshold * switch to debounceTime * lower debounce to 100 * raise debounce back to 200 * revert azure dashboard * re-render only rows list on data hover event * further break down table component * refactor * raise debounce time * fix build
- Loading branch information
Showing
12 changed files
with
397 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
import { css, cx } from '@emotion/css'; | ||
import React, { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react'; | ||
import { Cell, Row, TableState } from 'react-table'; | ||
import { VariableSizeList } from 'react-window'; | ||
import { Subscription, debounceTime } from 'rxjs'; | ||
|
||
import { | ||
DataFrame, | ||
DataHoverClearEvent, | ||
DataHoverEvent, | ||
Field, | ||
FieldType, | ||
TimeRange, | ||
hasTimeField, | ||
} from '@grafana/data'; | ||
import { TableCellHeight } from '@grafana/schema'; | ||
|
||
import { useTheme2 } from '../../themes'; | ||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; | ||
import { usePanelContext } from '../PanelChrome'; | ||
|
||
import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow'; | ||
import { TableCell } from './TableCell'; | ||
import { TableStyles } from './styles'; | ||
import { TableFilterActionCallback } from './types'; | ||
import { calculateAroundPointThreshold, isPointTimeValAroundTableTimeVal } from './utils'; | ||
|
||
interface RowsListProps { | ||
data: DataFrame; | ||
rows: Row[]; | ||
enableSharedCrosshair: boolean; | ||
headerHeight: number; | ||
rowHeight: number; | ||
itemCount: number; | ||
pageIndex: number; | ||
listHeight: number; | ||
width: number; | ||
cellHeight?: TableCellHeight; | ||
listRef: React.RefObject<VariableSizeList>; | ||
tableState: TableState; | ||
tableStyles: TableStyles; | ||
nestedDataField?: Field; | ||
prepareRow: (row: Row) => void; | ||
onCellFilterAdded?: TableFilterActionCallback; | ||
timeRange?: TimeRange; | ||
footerPaginationEnabled: boolean; | ||
} | ||
|
||
export const RowsList = (props: RowsListProps) => { | ||
const { | ||
data, | ||
rows, | ||
headerHeight, | ||
footerPaginationEnabled, | ||
rowHeight, | ||
itemCount, | ||
pageIndex, | ||
tableState, | ||
prepareRow, | ||
onCellFilterAdded, | ||
width, | ||
cellHeight = TableCellHeight.Sm, | ||
timeRange, | ||
tableStyles, | ||
nestedDataField, | ||
listHeight, | ||
listRef, | ||
enableSharedCrosshair = false, | ||
} = props; | ||
|
||
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(undefined); | ||
|
||
const theme = useTheme2(); | ||
const panelContext = usePanelContext(); | ||
|
||
const threshold = useMemo(() => { | ||
const timeField = data.fields.find((f) => f.type === FieldType.time); | ||
|
||
if (!timeField) { | ||
return 0; | ||
} | ||
|
||
return calculateAroundPointThreshold(timeField); | ||
}, [data]); | ||
|
||
const onRowHover = useCallback( | ||
(idx: number, frame: DataFrame) => { | ||
if (!panelContext || !enableSharedCrosshair || !hasTimeField(frame)) { | ||
return; | ||
} | ||
|
||
const timeField: Field = frame!.fields.find((f) => f.type === FieldType.time)!; | ||
|
||
panelContext.eventBus.publish( | ||
new DataHoverEvent({ | ||
point: { | ||
time: timeField.values[idx], | ||
}, | ||
}) | ||
); | ||
}, | ||
[enableSharedCrosshair, panelContext] | ||
); | ||
|
||
const onRowLeave = useCallback(() => { | ||
if (!panelContext || !enableSharedCrosshair) { | ||
return; | ||
} | ||
|
||
panelContext.eventBus.publish(new DataHoverClearEvent()); | ||
}, [enableSharedCrosshair, panelContext]); | ||
|
||
const onDataHoverEvent = useCallback( | ||
(evt: DataHoverEvent) => { | ||
if (evt.payload.point?.time && evt.payload.rowIndex !== undefined) { | ||
const timeField = data.fields.find((f) => f.type === FieldType.time); | ||
const time = timeField!.values[evt.payload.rowIndex]; | ||
const pointTime = evt.payload.point.time; | ||
|
||
// If the time value of the hovered point is around the time value of the | ||
// row with same index, highlight the row | ||
if (isPointTimeValAroundTableTimeVal(pointTime, time, threshold)) { | ||
setRowHighlightIndex(evt.payload.rowIndex); | ||
return; | ||
} | ||
|
||
// If the time value of the hovered point is not around the time value of the | ||
// row with same index, try to find a row with same time value | ||
const matchedRowIndex = timeField!.values.findIndex((t) => | ||
isPointTimeValAroundTableTimeVal(pointTime, t, threshold) | ||
); | ||
|
||
if (matchedRowIndex !== -1) { | ||
setRowHighlightIndex(matchedRowIndex); | ||
return; | ||
} | ||
|
||
setRowHighlightIndex(undefined); | ||
} | ||
}, | ||
[data.fields, threshold] | ||
); | ||
|
||
useEffect(() => { | ||
if (!panelContext || !enableSharedCrosshair || !hasTimeField(data) || footerPaginationEnabled) { | ||
return; | ||
} | ||
|
||
const subs = new Subscription(); | ||
|
||
subs.add( | ||
panelContext.eventBus | ||
.getStream(DataHoverEvent) | ||
.pipe(debounceTime(250)) | ||
.subscribe({ | ||
next: (evt) => { | ||
if (panelContext.eventBus === evt.origin) { | ||
return; | ||
} | ||
|
||
onDataHoverEvent(evt); | ||
}, | ||
}) | ||
); | ||
|
||
subs.add( | ||
panelContext.eventBus | ||
.getStream(DataHoverClearEvent) | ||
.pipe(debounceTime(250)) | ||
.subscribe({ | ||
next: (evt) => { | ||
if (panelContext.eventBus === evt.origin) { | ||
return; | ||
} | ||
|
||
setRowHighlightIndex(undefined); | ||
}, | ||
}) | ||
); | ||
|
||
return () => { | ||
subs.unsubscribe(); | ||
}; | ||
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]); | ||
|
||
let scrollTop: number | undefined = undefined; | ||
if (rowHighlightIndex !== undefined) { | ||
const firstMatchedRowIndex = rows.findIndex((row) => row.index === rowHighlightIndex); | ||
|
||
if (firstMatchedRowIndex !== -1) { | ||
scrollTop = headerHeight + (firstMatchedRowIndex - 1) * rowHeight; | ||
} | ||
} | ||
|
||
const rowIndexForPagination = useCallback( | ||
(index: number) => { | ||
return tableState.pageIndex * tableState.pageSize + index; | ||
}, | ||
[tableState.pageIndex, tableState.pageSize] | ||
); | ||
|
||
const RenderRow = useCallback( | ||
({ index, style, rowHighlightIndex }: { index: number; style: CSSProperties; rowHighlightIndex?: number }) => { | ||
const indexForPagination = rowIndexForPagination(index); | ||
const row = rows[indexForPagination]; | ||
|
||
prepareRow(row); | ||
|
||
const expandedRowStyle = tableState.expanded[row.index] ? css({ '&:hover': { background: 'inherit' } }) : {}; | ||
|
||
if (rowHighlightIndex !== undefined && row.index === rowHighlightIndex) { | ||
style = { ...style, backgroundColor: theme.components.table.rowHoverBackground }; | ||
} | ||
|
||
return ( | ||
<div | ||
{...row.getRowProps({ style })} | ||
className={cx(tableStyles.row, expandedRowStyle)} | ||
onMouseEnter={() => onRowHover(index, data)} | ||
onMouseLeave={onRowLeave} | ||
> | ||
{/*add the nested data to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/} | ||
{nestedDataField && tableState.expanded[row.index] && ( | ||
<ExpandedRow | ||
nestedData={nestedDataField} | ||
tableStyles={tableStyles} | ||
rowIndex={index} | ||
width={width} | ||
cellHeight={cellHeight} | ||
/> | ||
)} | ||
{row.cells.map((cell: Cell, index: number) => ( | ||
<TableCell | ||
key={index} | ||
tableStyles={tableStyles} | ||
cell={cell} | ||
onCellFilterAdded={onCellFilterAdded} | ||
columnIndex={index} | ||
columnCount={row.cells.length} | ||
timeRange={timeRange} | ||
frame={data} | ||
/> | ||
))} | ||
</div> | ||
); | ||
}, | ||
[ | ||
cellHeight, | ||
data, | ||
nestedDataField, | ||
onCellFilterAdded, | ||
onRowHover, | ||
onRowLeave, | ||
prepareRow, | ||
rowIndexForPagination, | ||
rows, | ||
tableState.expanded, | ||
tableStyles, | ||
theme.components.table.rowHoverBackground, | ||
timeRange, | ||
width, | ||
] | ||
); | ||
|
||
const getItemSize = (index: number): number => { | ||
const indexForPagination = rowIndexForPagination(index); | ||
const row = rows[indexForPagination]; | ||
if (tableState.expanded[row.index] && nestedDataField) { | ||
return getExpandedRowHeight(nestedDataField, index, tableStyles); | ||
} | ||
|
||
return tableStyles.rowHeight; | ||
}; | ||
|
||
const handleScroll: UIEventHandler = (event) => { | ||
const { scrollTop } = event.currentTarget; | ||
|
||
if (listRef.current !== null) { | ||
listRef.current.scrollTo(scrollTop); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}> | ||
<VariableSizeList | ||
// This component needs an unmount/remount when row height or page changes | ||
key={rowHeight + pageIndex} | ||
height={listHeight} | ||
itemCount={itemCount} | ||
itemSize={getItemSize} | ||
width={'100%'} | ||
ref={listRef} | ||
style={{ overflow: undefined }} | ||
> | ||
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })} | ||
</VariableSizeList> | ||
</CustomScrollbar> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.