@ -45,6 +45,7 @@ import {
import {
frameToRecords ,
getCellColors ,
getCellHeightCalculator ,
getComparator ,
getDefaultRowHeight ,
getFooterItemNG ,
@ -121,10 +122,7 @@ export function TableNG(props: TableNGProps) {
const calcsRef = useRef < string [ ] > ( [ ] ) ;
const [ paginationWrapperRef , { height : paginationHeight } ] = useMeasure < HTMLDivElement > ( ) ;
const textWrap = fieldConfig ? . defaults ? . custom ? . cellOptions ? . wrapText ? ? false ;
const theme = useTheme2 ( ) ;
const styles = useStyles2 ( getStyles , textWrap ) ;
const panelContext = usePanelContext ( ) ;
const isFooterVisible = Boolean ( footerOptions ? . show && footerOptions . reducer ? . length ) ;
@ -188,23 +186,6 @@ export function TableNG(props: TableNGProps) {
return fieldConfig ? . defaults ? . custom ? . width || 'auto' ;
} , [ fieldConfig ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
// Create off-screen canvas for measuring rows for virtualized rendering
// This line is like this because Jest doesn't have OffscreenCanvas mocked
// nor is it a part of the jest-canvas-mock package
let osContext = null ;
if ( window . OffscreenCanvas !== undefined ) {
// The canvas size is defined arbitrarily
// As we never actually visualize rendered content
// from the offscreen canvas, only perform text measurements
osContext = new OffscreenCanvas ( 256 , 1024 ) . getContext ( '2d' ) ;
}
// Set font property using theme info
// This will make text measurement accurate
if ( osContext !== undefined && osContext !== null ) {
osContext . font = ` ${ theme . typography . fontSize } px ${ theme . typography . body . fontFamily } ` ;
}
const defaultRowHeight = getDefaultRowHeight ( theme , cellHeight ) ;
const defaultLineHeight = theme . typography . body . lineHeight * theme . typography . fontSize ;
const panelPaddingHeight = theme . components . panel . padding * theme . spacing . gridSize * 2 ;
@ -213,13 +194,77 @@ export function TableNG(props: TableNGProps) {
const rows = useMemo ( ( ) = > frameToRecords ( props . data ) , [ frameToRecords , props . data ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
// Create a map of column key to column type
const columnTypes = useMemo ( ( ) = > {
return props . data . fields . reduce < ColumnTypes > ( ( acc , field ) = > {
acc [ field . name ] = field . type ;
return acc ;
} , { } ) ;
const columnTypes = useMemo (
( ) = > props . data . fields . reduce ( ( acc , { name , type } ) = > ( { . . . acc , [ name ] : type } ) , { } as ColumnTypes ) ,
[ props . data . fields ]
) ;
// Create a map of column key to text wrap
const textWraps = useMemo (
( ) = >
props . data . fields . reduce (
( acc , { name , config } ) = > ( { . . . acc , [ name ] : config ? . custom ? . cellOptions ? . wrapText ? ? false } ) ,
{ } as { [ key : string ] : boolean }
) ,
[ props . data . fields ]
) ;
const textWrap = useMemo ( ( ) = > Object . values ( textWraps ) . some ( Boolean ) , [ textWraps ] ) ;
const styles = useStyles2 ( getStyles ) ;
// Create a function to get column widths for text wrapping calculations
const getColumnWidths = useCallback ( ( ) = > {
const widths : Record < string , number > = { } ;
// Set default widths from field config if they exist
props . data . fields . forEach ( ( { name , config } ) = > {
const configWidth = config ? . custom ? . width ;
const totalWidth = typeof configWidth === 'number' ? configWidth : COLUMN.DEFAULT_WIDTH ;
// subtract out padding and 1px right border
const contentWidth = totalWidth - 2 * TABLE . CELL_PADDING - 1 ;
widths [ name ] = contentWidth ;
} ) ;
// Measure actual widths if available
Object . keys ( headerCellRefs . current ) . forEach ( ( key ) = > {
const headerCell = headerCellRefs . current [ key ] ;
if ( headerCell . offsetWidth > 0 ) {
widths [ key ] = headerCell . offsetWidth ;
}
} ) ;
return widths ;
} , [ props . data . fields ] ) ;
const headersLength = useMemo ( ( ) = > {
return props . data . fields . length ;
} , [ props . data . fields ] ) ;
const fieldDisplayType = useMemo ( ( ) = > {
return props . data . fields . reduce (
( acc , { config , name } ) = > {
if ( config ? . custom ? . cellOptions ? . type ) {
acc [ name ] = config . custom . cellOptions . type ;
}
return acc ;
} ,
{ } as Record < string , TableCellDisplayMode >
) ;
} , [ props . data . fields ] ) ;
// Clean up fieldsData to simplify
const fieldsData = useMemo (
( ) = > ( {
headersLength ,
textWraps ,
columnTypes ,
fieldDisplayType ,
columnWidths : getColumnWidths ( ) ,
} ) ,
[ textWraps , columnTypes , getColumnWidths , headersLength , fieldDisplayType ]
) ;
const getDisplayedValue = ( row : TableRow , key : string ) = > {
const field = props . data . fields . find ( ( field ) = > field . name === key ) ! ;
const displayedValue = formattedValueToString ( field . display ! ( row [ key ] ) ) ;
@ -364,6 +409,27 @@ export function TableNG(props: TableNGProps) {
setResizeTrigger ( ( prev ) = > prev + 1 ) ;
} ;
const { ctx , avgCharWidth } = useMemo ( ( ) = > {
const font = ` ${ theme . typography . fontSize } px ${ theme . typography . fontFamily } ` ;
const canvas = document . createElement ( 'canvas' ) ;
const ctx = canvas . getContext ( '2d' ) ! ;
// set in grafana/data in createTypography.ts
const letterSpacing = 0.15 ;
ctx . letterSpacing = ` ${ letterSpacing } px ` ;
ctx . font = font ;
let txt =
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s" ;
const txtWidth = ctx . measureText ( txt ) . width ;
const avgCharWidth = txtWidth / txt . length + letterSpacing ;
return {
ctx ,
font ,
avgCharWidth ,
} ;
} , [ theme . typography . fontSize , theme . typography . fontFamily ] ) ;
const columns = useMemo (
( ) = >
mapFrameToDataGrid ( {
@ -371,6 +437,7 @@ export function TableNG(props: TableNGProps) {
calcsRef ,
options : {
columnTypes ,
textWraps ,
columnWidth ,
crossFilterOrder ,
crossFilterRows ,
@ -381,8 +448,8 @@ export function TableNG(props: TableNGProps) {
headerCellRefs ,
isCountRowsSet ,
onCellFilterAdded ,
ctx ,
onSortByChange ,
osContext ,
rows ,
// INFO: sortedRows is for correct row indexing for cell background coloring
sortedRows ,
@ -392,7 +459,6 @@ export function TableNG(props: TableNGProps) {
setSortColumns ,
sortColumnsRef ,
styles ,
textWrap ,
theme ,
showTypeIcons ,
. . . props ,
@ -426,6 +492,10 @@ export function TableNG(props: TableNGProps) {
) ;
} ;
const cellHeightCalc = useMemo ( ( ) = > {
return getCellHeightCalculator ( ctx , defaultLineHeight , defaultRowHeight , TABLE . CELL_PADDING ) ;
} , [ ctx , defaultLineHeight , defaultRowHeight ] ) ;
const calculateRowHeight = useCallback (
( row : TableRow ) = > {
// Logic for sub-tables
@ -435,17 +505,9 @@ export function TableNG(props: TableNGProps) {
const headerCount = row ? . data ? . meta ? . custom ? . noHeader ? 0 : 1 ;
return defaultRowHeight * ( row . data ? . length ? ? 0 + headerCount ) ; // TODO this probably isn't very robust
}
return getRowHeight (
row ,
columnTypes ,
headerCellRefs ,
osContext ,
defaultLineHeight ,
defaultRowHeight ,
TABLE . CELL_PADDING
) ;
return getRowHeight ( row , cellHeightCalc , avgCharWidth , defaultRowHeight , fieldsData ) ;
} ,
[ expandedRows , defaultRowHeight , columnTypes , headerCellRefs , osContext , defaultLineHeight ]
[ expandedRows , avgCharWidth , defaultRowHeight , fieldsData , cellHeightCalc ]
) ;
const handleScroll = ( event : React.UIEvent < HTMLDivElement > ) = > {
@ -583,6 +645,7 @@ export function mapFrameToDataGrid({
} ) : TableColumn [ ] {
const {
columnTypes ,
textWraps ,
crossFilterOrder ,
crossFilterRows ,
defaultLineHeight ,
@ -592,8 +655,8 @@ export function mapFrameToDataGrid({
headerCellRefs ,
isCountRowsSet ,
onCellFilterAdded ,
ctx ,
onSortByChange ,
osContext ,
rows ,
sortedRows ,
setContextMenuProps ,
@ -602,7 +665,6 @@ export function mapFrameToDataGrid({
setSortColumns ,
sortColumnsRef ,
styles ,
textWrap ,
theme ,
timeRange ,
getActions ,
@ -717,7 +779,7 @@ export function mapFrameToDataGrid({
key ,
name : field.name ,
field ,
cellClass : styles.cell ,
cellClass : textWraps [ field . name ] ? styles.cellWrapped : styles.cell ,
renderCell : ( props : RenderCellProps < TableRow , TableSummaryRow > ) : JSX . Element = > {
const { row , rowIdx } = props ;
const cellType = field . config ? . custom ? . cellOptions ? . type ? ? TableCellDisplayMode . Auto ;
@ -740,11 +802,11 @@ export function mapFrameToDataGrid({
row ,
columnTypes ,
headerCellRefs ,
osContext ,
ctx ,
defaultLineHeight ,
defaultRowHeight ,
TABLE . CELL_PADDING ,
textWrap ,
textWraps [ field . name ] ,
field ,
cellType
)
@ -798,7 +860,7 @@ export function mapFrameToDataGrid({
showTypeIcons = { showTypeIcons }
/ >
) ,
width : fieldTableOptions.width ,
width : fieldTableOptions.width ? ? COLUMN . DEFAULT_WIDTH ,
minWidth : fieldTableOptions.minWidth || COLUMN . DEFAULT_WIDTH ,
} ) ;
} ) ;
@ -891,7 +953,7 @@ export function onRowLeave(panelContext: PanelContext, enableSharedCrosshair: bo
panelContext . eventBus . publish ( new DataHoverClearEvent ( ) ) ;
}
const getStyles = ( theme : GrafanaTheme2 , textWrap : boolean ) = > ( {
const getStyles = ( theme : GrafanaTheme2 ) = > ( {
dataGrid : css ( {
'--rdg-background-color' : theme . colors . background . primary ,
'--rdg-header-background-color' : theme . colors . background . primary ,
@ -956,7 +1018,18 @@ const getStyles = (theme: GrafanaTheme2, textWrap: boolean) => ({
cell : css ( {
'--rdg-border-color' : theme . colors . border . medium ,
borderLeft : 'none' ,
whiteSpace : ` ${ textWrap ? 'break-spaces' : 'nowrap' } ` ,
whiteSpace : 'nowrap' ,
wordWrap : 'break-word' ,
overflow : 'hidden' ,
textOverflow : 'ellipsis' ,
// Reset default cell styles for custom cell component styling
paddingInline : '0' ,
} ) ,
cellWrapped : css ( {
'--rdg-border-color' : theme . colors . border . medium ,
borderLeft : 'none' ,
whiteSpace : 'pre-line' ,
wordWrap : 'break-word' ,
overflow : 'hidden' ,
textOverflow : 'ellipsis' ,