import { find, first, isArray, isString, escape } from 'lodash'; import { escapeStringForRegex, formattedValueToString, getValueFormat, ScopedVars, stringStartsAsRegEx, stringToJsRegex, textUtil, unEscapeStringFromRegex, TimeZone, dateTimeFormatISO, dateTimeFormat, GrafanaTheme, } from '@grafana/data'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { ColumnOptionsCtrl } from './column_options'; import { ColumnRender, TableRenderModel, ColumnStyle } from './types'; export class TableRenderer { formatters: any[] = []; colorState: any; constructor( private panel: { styles: ColumnStyle[]; pageSize: number }, private table: TableRenderModel, private timeZone: TimeZone, private sanitize: (v: any) => any, private templateSrv: TemplateSrv = getTemplateSrv(), private theme: GrafanaTheme ) { this.initColumns(); } setTable(table: TableRenderModel) { this.table = table; this.initColumns(); } initColumns() { this.formatters = []; this.colorState = {}; for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) { const column = this.table.columns[colIndex]; column.title = column.text; for (let i = 0; i < this.panel.styles.length; i++) { const style = this.panel.styles[i]; const escapedPattern = stringStartsAsRegEx(style.pattern) ? style.pattern : escapeStringForRegex(unEscapeStringFromRegex(style.pattern)); const regex = stringToJsRegex(escapedPattern); if (column.text.match(regex)) { column.style = style; if (style.alias) { column.title = textUtil.escapeHtml(column.text.replace(regex, style.alias)); } break; } } this.formatters[colIndex] = this.createColumnFormatter(column); } } getColorForValue(value: number, style: ColumnStyle) { if (!style.thresholds || !style.colors) { return null; } for (let i = style.thresholds.length; i > 0; i--) { if (value >= style.thresholds[i - 1]) { return this.theme.visualization.getColorByName(style.colors[i]); } } return this.theme.visualization.getColorByName(first(style.colors)); } defaultCellFormatter(v: any, style: ColumnStyle) { if (v === null || v === void 0 || v === undefined) { return ''; } if (isArray(v)) { v = v.join(', '); } if (style && style.sanitize) { return this.sanitize(v); } else { return escape(v); } } createColumnFormatter(column: ColumnRender) { if (!column.style) { return this.defaultCellFormatter; } if (column.style.type === 'hidden') { return (v: any): undefined => undefined; } if (column.style.type === 'date') { return (v: any) => { if (v === undefined || v === null) { return '-'; } if (isArray(v)) { v = v[0]; } // if is an epoch (numeric string and len > 12) if (isString(v) && !isNaN(v as any) && v.length > 12) { v = parseInt(v, 10); } if (!column.style.dateFormat) { return dateTimeFormatISO(v, { timeZone: this.timeZone, }); } return dateTimeFormat(v, { format: column.style.dateFormat, timeZone: this.timeZone, }); }; } if (column.style.type === 'string') { return (v: any): any => { if (isArray(v)) { v = v.join(', '); } const mappingType = column.style.mappingType || 0; if (mappingType === 1 && column.style.valueMaps) { for (let i = 0; i < column.style.valueMaps.length; i++) { const map = column.style.valueMaps[i]; if (v === null) { if (map.value === 'null') { return map.text; } continue; } // Allow both numeric and string values to be mapped if ((!isString(v) && Number(map.value) === Number(v)) || map.value === v) { this.setColorState(v, column.style); return this.defaultCellFormatter(map.text, column.style); } } } if (mappingType === 2 && column.style.rangeMaps) { for (let i = 0; i < column.style.rangeMaps.length; i++) { const map = column.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)) { this.setColorState(v, column.style); return this.defaultCellFormatter(map.text, column.style); } } } if (v === null || v === void 0) { return '-'; } this.setColorState(v, column.style); return this.defaultCellFormatter(v, column.style); }; } if (column.style.type === 'number') { const valueFormatter = getValueFormat(column.unit || column.style.unit); return (v: any): any => { if (v === null || v === void 0) { return '-'; } if (isNaN(v) || isArray(v)) { return this.defaultCellFormatter(v, column.style); } this.setColorState(v, column.style); return formattedValueToString(valueFormatter(v, column.style.decimals, null)); }; } return (value: any) => { return this.defaultCellFormatter(value, column.style); }; } setColorState(value: any, style: ColumnStyle) { if (!style.colorMode) { return; } if (value === null || value === void 0 || isArray(value)) { return; } const numericValue = Number(value); if (isNaN(numericValue)) { return; } this.colorState[style.colorMode] = this.getColorForValue(numericValue, style); } renderRowVariables(rowIndex: number) { const scopedVars: ScopedVars = {}; let cellVariable; const row = this.table.rows[rowIndex]; for (let i = 0; i < row.length; i++) { cellVariable = `__cell_${i}`; scopedVars[cellVariable] = { value: row[i], text: row[i] ? row[i].toString() : '' }; } return scopedVars; } formatColumnValue(colIndex: number, value: any) { const fmt = this.formatters[colIndex]; if (fmt) { return fmt(value); } return value; } renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { value = this.formatColumnValue(columnIndex, value); const column = this.table.columns[columnIndex]; const cellStyles = []; let cellStyle = ''; const cellClasses = []; let cellClass = ''; if (this.colorState.cell) { cellStyles.push('background-color:' + this.colorState.cell); cellClasses.push('table-panel-color-cell'); this.colorState.cell = null; } else if (this.colorState.value) { cellStyles.push('color:' + this.colorState.value); this.colorState.value = null; } // because of the fixed table headers css only solution // there is an issue if header cell is wider the cell // this hack adds header content to cell (not visible) let columnHtml = ''; if (addWidthHack) { columnHtml = '