import { dateTime } from '@grafana/data' ; import $ from 'jquery' ; import kbn from 'grafana/app/core/utils/kbn' ; import _ , { isNumber } from 'lodash' ; import { GetColorForValue , GetColorIndexForValue , StringToJsRegex } from './Utils' ; import 'datatables.net' ; import 'mark.js' ; import 'datatables.mark.js' ; export class DatatableRenderer { formatters : any ; colorState : any ; panel : any ; table : any ; isUtc : boolean ; sanitize : any ; timeSrv : any ; GRID_CELL_HEIGHT = 30 ; TITLE_LINE_HEIGHT = 28 ; constructor ( panel , table , isUtc , sanitize , timeSrv ) { this . formatters = [ ] ; this . colorState = { } ; this . panel = panel ; this . table = table ; this . isUtc = isUtc ; this . sanitize = sanitize ; this . timeSrv = timeSrv ; } param v param style param column param rowIndex return defaultCellFormatter ( v , style , column , rowIndex ) { if ( v === null || v === void 0 || v === undefined || column === null ) { return '' ; } if ( _ . isArray ( v ) ) { v = v . join ( ', ' ) ; } v = String ( v ) ; if ( typeof style === 'undefined' || typeof style !== 'object' ) { style = { } ; } let cellTemplate = style . url ; if ( typeof style . splitPattern === 'undefined' || style . splitPattern === '' ) { style . splitPattern = '/ /' ; } const regex = StringToJsRegex ( String ( style . splitPattern ) ) ; const values = v . split ( regex ) ; if ( typeof cellTemplate !== 'undefined' ) { cellTemplate = this . replaceTimeMacros ( cellTemplate ) ; cellTemplate = cellTemplate . replace ( /\$__cell\b/ , v ) ; values . map ( ( val , i ) => ( cellTemplate = cellTemplate . replace ( `$__pattern_${ i }` , val ) ) ) ; } if ( style && style . sanitize ) { return this . sanitize ( v ) ; } else if ( style && style . link && cellTemplate && column . text === style . column ) { const matches = /\$__cell_(\d+)/g . exec ( cellTemplate ) ; let linkValue = cellTemplate ; if ( matches ) { for ( let matchIndex = 1 ; matchIndex < matches . length ; matchIndex ++ ) { const matchedCellNumber = parseInt ( matches [ matchIndex ] , 10 ) ; if ( ! isNaN ( matchedCellNumber ) ) { const matchedCellContent = this . table . rows [ rowIndex ] [ matchedCellNumber ] ; linkValue = linkValue . replace ( `$__cell_${ matchedCellNumber }` , matchedCellContent ) ; } } const valueFormatter = kbn . valueFormats [ column . unit || style . unit ] ; if ( style && style . decimals ) { v = valueFormatter ( v , style . decimals , null ) ; } else { v = valueFormatter ( v ) ; } return '<a href="' + linkValue + '" target="_blank">' + v + '</a>' ; } else { const linkValue = cellTemplate . replace ( /\{\}|\$__cell_\d*/g , v ) ; return '<a href="' + linkValue + '" target="_blank">' + v + '</a>' ; } return _ . escape ( v ) ; } else if ( style && style . link ) { return '<a href="' + v + '" target="_blank">' + v + '</a>' ; } else { return _ . escape ( v ) ; } } replaceTimeMacros ( content ) { let newContent = content ; if ( content . match ( /\$__from/g ) ) { newContent = newContent . replace ( '$__from' , this . timeSrv . time . from ) ; } if ( content . match ( /\$__to/g ) ) { newContent = newContent . replace ( '$__to' , this . timeSrv . time . to ) ; } if ( content . match ( /\$__keepTime/g ) ) { const keepTime = `from=${ this . timeSrv . time . from }&to=${ this . timeSrv . time . to }` ; newContent = newContent . replace ( '$__keepTime' , keepTime ) ; } return newContent ; } param { [ type ] } style param { [ type ] } column return { [ type ] } createColumnFormatter ( style , column ) { if ( ! style ) { return this . defaultCellFormatter ; } if ( style . type === 'hidden' ) { return ( v , rIndex ) => { return null ; } ; } if ( style . type === 'date' ) { return ( v , rIndex ) => { if ( v === undefined || v === null ) { return '-' ; } if ( _ . isArray ( v ) ) { v = v [ 0 ] ; } if ( _ . isString ( v ) && ! isNaN ( v as any ) && v . length > 12 ) { v = parseInt ( v , 10 ) ; } let date = dateTime ( v ) ; if ( this . isUtc ) { date = date . utc ( ) ; } const fmt = date . format ( style . dateFormat ) ; return fmt ; } ; } if ( style . type === 'number' ) { const valueFormatter = kbn . valueFormats [ column . unit || style . unit ] ; return ( v , rIndex ) => { if ( v === null || v === void 0 ) { return '-' ; } if ( _ . isString ( v ) ) { return this . defaultCellFormatter ( v , style , column , rIndex ) ; } if ( style . colorMode ) { this . colorState [ style . colorMode ] = GetColorForValue ( v , style ) ; } return valueFormatter ( v , style . decimals , null ) ; } ; } if ( style . type === 'string' ) { return ( v , rIndex ) => { if ( _ . isArray ( v ) ) { v = v . join ( ', ' ) ; } const mappingType = style . mappingType || 0 ; if ( mappingType === 1 && style . valueMaps ) { for ( let i = 0 ; i < style . valueMaps . length ; i ++ ) { const map = style . valueMaps [ i ] ; if ( v === null ) { if ( map . value === 'null' ) { return map . text ; } continue ; } if ( ( ! _ . isString ( v ) && Number ( map . value ) === Number ( v ) ) || map . value === v ) { return this . defaultCellFormatter ( map . text , style , column , rIndex ) ; } } } if ( mappingType === 2 && style . rangeMaps ) { for ( let i = 0 ; i < style . rangeMaps . length ; i ++ ) { const map = style . rangeMaps [ i ] ; if ( v === null ) { if ( map . from === 'null' && map . to === 'null' ) { return map . text ; } continue ; } if ( Number ( map . from ) <= Number ( v ) && Number ( map . to ) >= Number ( v ) ) { return this . defaultCellFormatter ( map . text , style , column , rIndex ) ; } } } if ( v === null || v === void 0 ) { return '-' ; } return this . defaultCellFormatter ( v , style , column , rIndex ) ; } ; } return ( v , rIndex ) => { return this . defaultCellFormatter ( v , style , column , rIndex ) ; } ; } param { [ type ] } colIndex param { [ type ] } rowIndex param { [ type ] } value return { [ type ] } formatColumnValue ( colIndex , rowIndex , value ) { if ( ! this . formatters [ colIndex ] ) { for ( let i = 0 ; i < this . panel . styles . length ; i ++ ) { const style = this . panel . styles [ i ] ; const column = this . table . columns [ colIndex ] ; const regex = StringToJsRegex ( style . pattern ) ; if ( column . text . match ( regex ) ) { this . formatters [ colIndex ] = this . createColumnFormatter ( style , column ) ; } } } if ( ! this . formatters [ colIndex ] ) { this . formatters [ colIndex ] = this . defaultCellFormatter ; } let v = this . formatters [ colIndex ] ( value , rowIndex ) ; if ( /\$__cell_\d+/ . exec ( v ) ) { for ( let i = this . table . columns . length - 1 ; i >= 0 ; i -- ) { v = v . replace ( `$__cell_${ i }` , this . table . rows [ rowIndex ] [ i ] ) ; } } return v ; } param { [ type ] } rowData return { [ type ] } generateFormattedData ( rowData ) { const formattedRowData = [ ] ; for ( let y = 0 ; y < rowData . length ; y ++ ) { const row = this . table . rows [ y ] ; const cellData = [ ] ; for ( let i = 0 ; i < this . table . columns . length ; i ++ ) { let value = this . formatColumnValue ( i , y , row [ i ] ) ; if ( value === undefined || value === null ) { this . table . columns [ i ] . hidden = true ; } if ( value === null ) { value = row [ i ] ; } const record = { display : value , raw : row [ i ] , _ : row [ i ] , } ; cellData . push ( record ) ; } if ( this . panel . rowNumbersEnabled ) { cellData . unshift ( 'rowCounter' ) ; } formattedRowData . push ( cellData ) ; } return formattedRowData ; } getStyleForColumn ( columnNumber ) { let colStyle = null ; for ( let i = 0 ; i < this . panel . styles . length ; i ++ ) { const style = this . panel . styles [ i ] ; const column = this . table . columns [ columnNumber ] ; if ( column === undefined ) { break ; } const regex = StringToJsRegex ( style . pattern ) ; if ( column . text . match ( regex ) ) { colStyle = style ; break ; } } return colStyle ; } getCellColors ( colorState , columnNumber , cellData ) { if ( cellData === null || cellData === undefined ) { return null ; } const items = cellData . split ( /([^0-9.,]+)/ ) ; let bgColor = null ; let bgColorIndex = null ; let color = null ; let colorIndex = null ; let colStyle = null ; let value = null ; if ( ! isNaN ( items [ 0 ] ) ) { value = parseFloat ( items [ 0 ] . replace ( ',' , '.' ) ) ; colStyle = this . getStyleForColumn ( columnNumber ) ; } if ( colStyle !== null && colStyle . colorMode != null ) { if ( colorState . cell || colorState . row || colorState . rowcolumn ) { bgColor = GetColorForValue ( value , colStyle ) ; bgColorIndex = GetColorIndexForValue ( value , colStyle ) ; color = 'white' ; } if ( colorState . value ) { color = GetColorForValue ( value , colStyle ) ; colorIndex = GetColorIndexForValue ( value , colStyle ) ; } } return { bgColor : bgColor , bgColorIndex : bgColorIndex , color : color , colorIndex : colorIndex , } ; } getColumnAlias ( columnName ) { let columnAlias = columnName ; if ( this . panel . columnAliases !== undefined ) { for ( let i = 0 ; i < this . panel . columnAliases . length ; i ++ ) { if ( this . panel . columnAliases [ i ] . name === columnName ) { columnAlias = this . panel . columnAliases [ i ] . alias ; break ; } } } return columnAlias ; } getColumnWidthHint ( columnName ) { let columnWidth = '' ; if ( this . panel . columnWidthHints !== undefined ) { for ( let i = 0 ; i < this . panel . columnWidthHints . length ; i ++ ) { if ( this . panel . columnWidthHints [ i ] . name === columnName ) { columnWidth = this . panel . columnWidthHints [ i ] . width ; break ; } } } return columnWidth ; } return { [ Boolean ] } render ( ) { const tableHolderId = '#datatable-panel-table-' + this . panel . id ; try { if ( $ . fn . dataTable . isDataTable ( tableHolderId ) ) { const aDT = $ ( tableHolderId ) . DataTable ( ) ; aDT . destroy ( ) ; $ ( tableHolderId ) . empty ( ) ; } } catch ( err ) { console . log ( 'Exception: ' + err . message ) ; } if ( this . panel . emptyData ) { return ; } const columns = [ ] ; const columnDefs = [ ] ; const _this = this ; let rowNumberOffset = 0 ; if ( this . panel . rowNumbersEnabled ) { rowNumberOffset = 1 ; columns . push ( { title : '' , type : 'number' , } ) ; columnDefs . push ( { searchable : false , orderable : false , targets : 0 , width : '1%' , } ) ; } for ( let i = 0 ; i < this . table . columns . length ; i ++ ) { const columnAlias = this . getColumnAlias ( this . table . columns [ i ] . text ) ; const columnWidthHint = this . getColumnWidthHint ( this . table . columns [ i ] . text ) ; var columnClassName = '' ; let columnType = this . table . columns [ i ] . type ; if ( columnType === 'date' ) { columnType = 'num' ; } if ( columnType === 'num' && this . panel . alignNumbersToRightEnabled ) { columnClassName = 'dt-right' ; } if ( columnType === 'string' ) { columnClassName = 'dt-right' ; } columns . push ( { title : columnAlias , type : columnType , width : columnWidthHint , className : columnClassName , } ) ; columnDefs . push ( { targets : i + rowNumberOffset , data : function ( row , type , val , meta ) { if ( type === undefined ) { return null ; } const idx = meta . col ; let returnValue = row [ idx ] . raw ; if ( type === 'display' ) { returnValue = row [ idx ] . display ; } return returnValue ; } , render : function ( data , type , val , meta ) { if ( type === undefined ) { return null ; } const idx = meta . col ; if ( type === 'type' ) { return val [ idx ] ; } let returnValue = val [ idx ] . raw ; if ( type === 'display' ) { returnValue = val [ idx ] . display ; } return returnValue ; } , createdCell : ( td , cellData , rowData , row , col ) => { const formattedData = $ ( td ) . html ( ) ; if ( ! isNumber ( _this . table . rows [ row ] [ col ] ) ) { return ; } if ( formattedData === null ) { return ; } cellData = formattedData ; $ ( td ) . css ( 'font-size' , _this . panel . fontSize ) ; let actualColumn = col ; if ( _this . panel . rowNumbersEnabled ) { actualColumn -= 1 ; } let rowColor = null ; let color = null ; let rowColorIndex = null ; let rowColorData = null ; if ( _this . colorState . row ) { if ( rowData === null ) { return ; } rowColorIndex = - 1 ; rowColorData = null ; rowColor = _this . colorState . row ; color = 'white' ; for ( let columnNumber = 0 ; columnNumber < _this . table . columns . length ; columnNumber ++ ) { if ( _this . table . columns [ columnNumber ] . type === undefined ) { rowColorData = _this . getCellColors ( _this . colorState , columnNumber , rowData [ columnNumber + rowNumberOffset ] ) ; if ( ! rowColorData ) { continue ; } if ( rowColorData . bgColorIndex !== null ) { if ( rowColorData . bgColorIndex > rowColorIndex ) { rowColorIndex = rowColorData . bgColorIndex ; rowColor = rowColorData . bgColor ; } } } } $ ( td . parentNode ) . children ( ) . css ( 'color' , color ) ; $ ( td . parentNode ) . children ( ) . css ( 'background-color' , rowColor ) ; } if ( _this . colorState . rowcolumn ) { if ( rowData === null ) { return ; } rowColorIndex = - 1 ; rowColorData = null ; rowColor = _this . colorState . rowcolumn ; color = 'white' ; for ( let columnNumber = 0 ; columnNumber < _this . table . columns . length ; columnNumber ++ ) { if ( _this . table . columns [ columnNumber ] . type === undefined ) { rowColorData = _this . getCellColors ( _this . colorState , columnNumber , rowData [ columnNumber + rowNumberOffset ] ) ; if ( ! rowColorData ) { continue ; } if ( rowColorData . bgColorIndex !== null ) { if ( rowColorData . bgColorIndex > rowColorIndex ) { rowColorIndex = rowColorData . bgColorIndex ; rowColor = rowColorData . bgColor ; } } } } if ( _this . table . columns [ 0 ] . type !== undefined ) { const children = $ ( td . parentNode ) . children ( ) ; let aChild = children [ 0 ] ; $ ( aChild ) . css ( 'color' , color ) ; $ ( aChild ) . css ( 'background-color' , rowColor ) ; if ( _this . panel . rowNumbersEnabled ) { aChild = children [ 1 ] ; $ ( aChild ) . css ( 'color' , color ) ; $ ( aChild ) . css ( 'background-color' , rowColor ) ; } } } const colorData = _this . getCellColors ( _this . colorState , actualColumn , cellData ) ; if ( ! colorData ) { return ; } if ( _this . colorState . cell || _this . colorState . rowcolumn ) { if ( colorData . color !== undefined ) { $ ( td ) . css ( 'color' , colorData . color ) ; } if ( colorData . bgColor !== undefined ) { $ ( td ) . css ( 'background-color' , colorData . bgColor ) ; } } else if ( _this . colorState . value ) { if ( colorData . color !== undefined ) { $ ( td ) . css ( 'color' , colorData . color ) ; } } } , } ) ; } try { let shouldDestroy = false ; if ( $ . fn . dataTable . isDataTable ( '#datatable-panel-table-' + this . panel . id ) ) { shouldDestroy = true ; } if ( shouldDestroy ) { const destroyedDT = $ ( '#datatable-panel-table-' + this . panel . id ) . DataTable ( ) ; destroyedDT . destroy ( ) ; $ ( '#datatable-panel-table-' + this . panel . id ) . empty ( ) ; } } catch ( err ) { console . log ( 'Exception: ' + err . message ) ; } if ( this . table . rows [ 0 ] . length === 4 ) { if ( this . table . rows [ 0 ] [ 0 ] === undefined ) { this . table . rows = [ ] ; } } const formattedData = this . generateFormattedData ( this . table . rows ) ; if ( this . panel . rowNumbersEnabled ) { } let panelHeight = this . panel . panelHeight ; if ( this . panel . scroll ) { if ( typeof this . panel . height === 'undefined' ) { panelHeight = this . getGridHeight ( this . panel . gridPos . h ) ; } } else { panelHeight = '' ; } const orderSetting = this . panel . sortByColumnsData ; let selectSettings ; selectSettings = { style : 'os' , } ; const tableOptions = { lengthMenu : [ [ 5 , 10 , 25 , 50 , 75 , 100 , - 1 ] , [ 5 , 10 , 25 , 50 , 75 , 100 , 'All' ] , ] , searching : this . panel . searchEnabled , info : this . panel . infoEnabled , lengthChange : this . panel . lengthChangeEnabled , scrollCollapse : false , scrollX : true , scrollY : panelHeight , stateSave : false , mark : this . panel . searchHighlightingEnabled || false , dom : 'Bfrtip' , buttons : [ 'copy' , 'excel' , 'csv' , 'pdf' , 'print' ] , select : selectSettings , data : formattedData , columns : columns , columnDefs : columnDefs , search : { regex : true , smart : false , } , order : orderSetting , scroll : this . panel . scroll , paging : ! this . panel . scroll , pagingType : this . panel . datatablePagingType , } ; const $datatable = $ ( tableHolderId ) ; const newDT = $datatable . DataTable ( tableOptions ) ; for ( let i = 0 ; i < this . table . columns . length ; i ++ ) { if ( this . table . columns [ i ] . hidden ) { newDT . column ( i + rowNumberOffset ) . visible ( false ) ; } } if ( this . panel . compactRowsEnabled ) { $datatable . addClass ( 'compact' ) ; } if ( this . panel . stripedRowsEnabled ) { $datatable . addClass ( 'stripe' ) ; } if ( this . panel . hoverEnabled ) { $datatable . addClass ( 'hover' ) ; } if ( this . panel . orderColumnEnabled ) { $datatable . addClass ( 'order-column' ) ; } if ( this . panel . showCellBorders ) { $datatable . addClass ( 'cell-border' ) ; } else { if ( this . panel . showRowBorders ) { $datatable . addClass ( 'row-border' ) ; } } if ( ! this . panel . scroll ) { if ( this . panel . rowsPerPage !== null ) { newDT . page . len ( this . panel . rowsPerPage ) . draw ( ) ; } } if ( this . panel . rowNumbersEnabled ) { newDT . on ( 'order.dt search.dt' , ( ) => { newDT . column ( 0 , { search : 'applied' , order : 'applied' } ) . nodes ( ) . each ( ( cell , i ) => { cell . innerHTML = i + 1 ; } ) ; } ) . draw ( ) ; } if ( this . panel . columnFiltersEnabled ) { const header = newDT . table ( 0 ) . header ( ) ; const newHeaders = $ ( header ) . children ( 'tr' ) . clone ( ) ; newHeaders . find ( 'th' ) . removeClass ( ) . addClass ( 'column-filter' ) ; newHeaders . appendTo ( header as Element ) ; $ ( header ) . find ( `tr:eq(1) th` ) . each ( function ( i ) { var title = $ ( this ) . text ( ) ; $ ( this ) . html ( '<input class="column-filter" type="text" placeholder="Search ' + title + '" />' ) ; $ ( 'input' , this ) . on ( 'keyup change' , function ( this ) { if ( newDT . column ( i ) . search ( ) !== this . value ) { newDT . column ( i ) . search ( this . value ) . draw ( ) ; } } ) ; } ) ; } } getGridHeight ( height ) { const gridHeight = Math . ceil ( height * this . GRID_CELL_HEIGHT ) - this . TITLE_LINE_HEIGHT - 30 ; return gridHeight ; } render_values ( ) { const rows = [ ] ; for ( let y = 0 ; y < this . table . rows . length ; y ++ ) { const row = this . table . rows [ y ] ; const newRow = [ ] ; for ( let i = 0 ; i < this . table . columns . length ; i ++ ) { newRow . push ( this . formatColumnValue ( i , y , row [ i ] ) ) ; } rows . push ( newRow ) ; } return { columns : this . table . columns , rows : rows , } ; } }