The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/panel/table/renderer.ts

340 lines
9.1 KiB

import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
export class TableRenderer {
formatters: any[];
colorState: any;
constructor(private panel, private table, private isUtc, private sanitize, private templateSrv) {
this.initColumns();
}
setTable(table) {
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 regex = kbn.stringToJsRegex(style.pattern);
if (column.text.match(regex)) {
column.style = style;
if (style.alias) {
column.title = column.text.replace(regex, style.alias);
}
break;
}
}
this.formatters[colIndex] = this.createColumnFormatter(column);
}
}
getColorForValue(value, style) {
if (!style.thresholds) {
return null;
}
for (let i = style.thresholds.length; i > 0; i--) {
if (value >= style.thresholds[i - 1]) {
return style.colors[i];
}
}
return _.first(style.colors);
}
defaultCellFormatter(v, style) {
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) {
if (!column.style) {
return this.defaultCellFormatter;
}
if (column.style.type === 'hidden') {
return v => {
return undefined;
};
}
if (column.style.type === 'date') {
return v => {
if (v === undefined || v === null) {
return '-';
}
if (_.isArray(v)) {
v = v[0];
}
let date = moment(v);
if (this.isUtc) {
date = date.utc();
}
return date.format(column.style.dateFormat);
};
}
if (column.style.type === 'string') {
return v => {
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 = kbn.valueFormats[column.unit || column.style.unit];
return v => {
if (v === null || v === void 0) {
return '-';
}
if (_.isString(v) || _.isArray(v)) {
return this.defaultCellFormatter(v, column.style);
}
this.setColorState(v, column.style);
return valueFormatter(v, column.style.decimals, null);
};
}
return value => {
return this.defaultCellFormatter(value, column.style);
};
}
setColorState(value, style) {
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) {
const 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] };
}
return scopedVars;
}
formatColumnValue(colIndex, value) {
return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
}
renderCell(columnIndex, rowIndex, value, addWidthHack = false) {
value = this.formatColumnValue(columnIndex, value);
const column = this.table.columns[columnIndex];
let cellStyle = '';
let textStyle = '';
const cellClasses = [];
let cellClass = '';
if (this.colorState.cell) {
cellStyle = ' style="background-color:' + this.colorState.cell + '"';
cellClasses.push('table-panel-color-cell');
this.colorState.cell = null;
} else if (this.colorState.value) {
textStyle = ' style="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 = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
}
if (value === undefined) {
cellStyle = ' style="display:none;"';
column.hidden = true;
} else {
column.hidden = false;
}
if (column.hidden === true) {
return '';
}
if (column.style && column.style.preserveFormat) {
cellClasses.push('table-panel-cell-pre');
}
if (column.style && column.style.link) {
// Render cell as link
const scopedVars = this.renderRowVariables(rowIndex);
scopedVars['__cell'] = { value: value };
const cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent);
const cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars);
const cellTarget = column.style.linkTargetBlank ? '_blank' : '';
cellClasses.push('table-panel-cell-link');
columnHtml += `
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${textStyle}>
${value}
</a>
`;
} else {
columnHtml += value;
}
if (column.filterable) {
cellClasses.push('table-panel-cell-filterable');
columnHtml += `
<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter out value" data-placement="bottom"
data-row="${rowIndex}" data-column="${columnIndex}" data-operator="!=">
<i class="fa fa-search-minus"></i>
</a>
<a class="table-panel-filter-link" data-link-tooltip data-original-title="Filter for value" data-placement="bottom"
data-row="${rowIndex}" data-column="${columnIndex}" data-operator="=">
<i class="fa fa-search-plus"></i>
</a>`;
}
if (cellClasses.length) {
cellClass = ' class="' + cellClasses.join(' ') + '"';
}
columnHtml = '<td' + cellClass + cellStyle + '>' + columnHtml + '</td>';
return columnHtml;
}
render(page) {
const pageSize = this.panel.pageSize || 100;
const startPos = page * pageSize;
const endPos = Math.min(startPos + pageSize, this.table.rows.length);
let html = '';
const rowClasses = [];
let rowClass = '';
for (let y = startPos; y < endPos; y++) {
const row = this.table.rows[y];
let cellHtml = '';
let rowStyle = '';
for (let i = 0; i < this.table.columns.length; i++) {
cellHtml += this.renderCell(i, y, row[i], y === startPos);
}
if (this.colorState.row) {
rowStyle = ' style="background-color:' + this.colorState.row + '"';
rowClasses.push('table-panel-color-row');
this.colorState.row = null;
}
if (rowClasses.length) {
rowClass = ' class="' + rowClasses.join(' ') + '"';
}
html += '<tr ' + rowClass + rowStyle + '>' + cellHtml + '</tr>';
}
return html;
}
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, row[i]));
}
rows.push(newRow);
}
return {
columns: this.table.columns,
rows: rows,
};
}
}