mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 23:23:45 -06:00
table-panel: clickable cell link - draft (#8738)
* table-panel: clickable cell link - draft * table-panel: clickable cell link - fix link target option * table-panel: fix undefined columnStyle.link * table-panel: option to highlight cell with link * table-panel: render variables for all cells in row * table-panel: remove cell highlighting * table-panel: add help for URL field * linkPopover directive for link info in table panel * table-panel: add link info popover to cells * table-panel: use native popover instead directive * table-panel: link drop refactor, remove unused code * table-panel: fix unclickable link when drop is opened * refactoring: minor refactoring to #8738, do not think we need a full blown popover for the links, simple tooltip is enough and more efficient, sadly we do not have a modern tooltip framework, still using old bootstrap 2.3 tooltip * table-panel: add tests for link rendering
This commit is contained in:
parent
13c966c178
commit
9bbc942534
@ -27,6 +27,7 @@
|
||||
<label class="gf-form-label width-13">Column Header</label>
|
||||
<input type="text" class="gf-form-input width-13" ng-model="style.alias" ng-change="editor.render()" ng-model-onblur placeholder="Override header label">
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-13" label="Render value as link" checked="style.link" change="editor.render()"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
@ -91,6 +92,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group" ng-if="style.link">
|
||||
<h5 class="section-heading">Link</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Url</label>
|
||||
<input type="text" class="gf-form-input width-29" ng-model="style.linkUrl" ng-blur="editor.render()" ng-model-onblur data-placement="right">
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Specify an URL (relative or absolute)</p>
|
||||
<span>
|
||||
Use special variables to specify cell values: <br>
|
||||
<em>$__cell</em> refers to current cell value <br>
|
||||
<em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
||||
<em>$__cell_1</em> refers to second column's value.
|
||||
</span>
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Tooltip</label>
|
||||
<input type="text" class="gf-form-input width-29" ng-model="style.linkTooltip" ng-blur="editor.render()" ng-model-onblur data-placement="right">
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Specify text for link tooltip.</p>
|
||||
<span>
|
||||
This title appears when user hovers pointer over the cell with link.
|
||||
Use the same variables as for URL.
|
||||
</span>
|
||||
</info-popover>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label-class="width-9" label="Open in new tab" checked="style.linkTargetBlank"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
|
||||
|
@ -10,6 +10,7 @@ import {transformDataToTable} from './transformers';
|
||||
import {tablePanelEditor} from './editor';
|
||||
import {columnOptionsTab} from './column_options';
|
||||
import {TableRenderer} from './renderer';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
@ -49,7 +50,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
};
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private annotationsSrv, private $sanitize) {
|
||||
constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize) {
|
||||
super($scope, $injector);
|
||||
this.pageIndex = 0;
|
||||
|
||||
@ -123,7 +124,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
this.table = transformDataToTable(this.dataRaw, this.panel);
|
||||
this.table.sort(this.panel.sort);
|
||||
|
||||
this.renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
|
||||
this.renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize, this.templateSrv);
|
||||
|
||||
return super.render(this.table);
|
||||
}
|
||||
@ -217,6 +218,11 @@ class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' });
|
||||
}
|
||||
|
||||
// hook up link tooltips
|
||||
elem.tooltip({
|
||||
selector: '[data-link-tooltip]'
|
||||
});
|
||||
|
||||
elem.on('click', '.table-panel-page-link', switchPage);
|
||||
|
||||
var unbindDestroy = scope.$on('$destroy', function() {
|
||||
|
@ -8,7 +8,7 @@ export class TableRenderer {
|
||||
formatters: any[];
|
||||
colorState: any;
|
||||
|
||||
constructor(private panel, private table, private isUtc, private sanitize) {
|
||||
constructor(private panel, private table, private isUtc, private sanitize, private templateSrv) {
|
||||
this.initColumns();
|
||||
}
|
||||
|
||||
@ -123,13 +123,25 @@ export class TableRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
renderRowVariables(rowIndex) {
|
||||
let scopedVars = {};
|
||||
let cell_variable;
|
||||
let row = this.table.rows[rowIndex];
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
cell_variable = `__cell_${i}`;
|
||||
scopedVars[cell_variable] = { value: row[i] };
|
||||
}
|
||||
return scopedVars;
|
||||
}
|
||||
|
||||
formatColumnValue(colIndex, value) {
|
||||
return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
|
||||
}
|
||||
|
||||
renderCell(columnIndex, value, addWidthHack = false) {
|
||||
renderCell(columnIndex, rowIndex, value, addWidthHack = false) {
|
||||
value = this.formatColumnValue(columnIndex, value);
|
||||
var style = '';
|
||||
var cellClasses = [];
|
||||
var cellClass = '';
|
||||
if (this.colorState.cell) {
|
||||
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
||||
@ -156,10 +168,34 @@ export class TableRenderer {
|
||||
|
||||
var columnStyle = this.table.columns[columnIndex].style;
|
||||
if (columnStyle && columnStyle.preserveFormat) {
|
||||
cellClass = ' class="table-panel-cell-pre" ';
|
||||
cellClasses.push("table-panel-cell-pre");
|
||||
}
|
||||
|
||||
return '<td' + cellClass + style + '>' + value + widthHack + '</td>';
|
||||
var columnHtml = value + widthHack;
|
||||
|
||||
if (columnStyle && columnStyle.link) {
|
||||
// Render cell as link
|
||||
var scopedVars = this.renderRowVariables(rowIndex);
|
||||
scopedVars['__cell'] = { value: value };
|
||||
|
||||
var cellLink = this.templateSrv.replace(columnStyle.linkUrl, scopedVars);
|
||||
var cellLinkTooltip = this.templateSrv.replace(columnStyle.linkTooltip, scopedVars);
|
||||
var cellTarget = columnStyle.linkTargetBlank ? '_blank' : '';
|
||||
|
||||
cellClasses.push("table-panel-cell-link");
|
||||
columnHtml = `
|
||||
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right">
|
||||
${columnHtml}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (cellClasses.length) {
|
||||
cellClass = ' class="' + cellClasses.join(' ') + '"';
|
||||
}
|
||||
|
||||
columnHtml = '<td' + cellClass + style + '>' + columnHtml + '</td>';
|
||||
return columnHtml;
|
||||
}
|
||||
|
||||
render(page) {
|
||||
@ -173,7 +209,7 @@ export class TableRenderer {
|
||||
let cellHtml = '';
|
||||
let rowStyle = '';
|
||||
for (var i = 0; i < this.table.columns.length; i++) {
|
||||
cellHtml += this.renderCell(i, row[i], y === startPos);
|
||||
cellHtml += this.renderCell(i, y, row[i], y === startPos);
|
||||
}
|
||||
|
||||
if (this.colorState.row) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
|
||||
import _ from 'lodash';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import {TableRenderer} from '../renderer';
|
||||
|
||||
@ -14,6 +15,10 @@ describe('when rendering table', () => {
|
||||
{text: 'String'},
|
||||
{text: 'United', unit: 'bps'},
|
||||
{text: 'Sanitized'},
|
||||
{text: 'Link'},
|
||||
];
|
||||
table.rows = [
|
||||
[1388556366666, 1230, 40, undefined, "", "", "my.host.com", "host1"]
|
||||
];
|
||||
|
||||
var panel = {
|
||||
@ -55,6 +60,14 @@ describe('when rendering table', () => {
|
||||
pattern: 'Sanitized',
|
||||
type: 'string',
|
||||
sanitize: true,
|
||||
},
|
||||
{
|
||||
pattern: 'Link',
|
||||
type: 'string',
|
||||
link: true,
|
||||
linkUrl: "/dashboard?param=$__cell¶m_1=$__cell_1¶m_2=$__cell_2",
|
||||
linkTooltip: "$__cell $__cell_1 $__cell_6",
|
||||
linkTargetBlank: true
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -63,75 +76,87 @@ describe('when rendering table', () => {
|
||||
return 'sanitized';
|
||||
};
|
||||
|
||||
var renderer = new TableRenderer(panel, table, 'utc', sanitize);
|
||||
var templateSrv = {
|
||||
replace: function(value, scopedVars) {
|
||||
if (scopedVars) {
|
||||
// For testing variables replacement in link
|
||||
_.each(scopedVars, function(val, key) {
|
||||
value = value.replace('$' + key, val.value);
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
var renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
|
||||
|
||||
it('time column should be formated', () => {
|
||||
var html = renderer.renderCell(0, 1388556366666);
|
||||
var html = renderer.renderCell(0, 0, 1388556366666);
|
||||
expect(html).to.be('<td>2014-01-01T06:06:06Z</td>');
|
||||
});
|
||||
|
||||
it('undefined time column should be rendered as -', () => {
|
||||
var html = renderer.renderCell(0, undefined);
|
||||
var html = renderer.renderCell(0, 0, undefined);
|
||||
expect(html).to.be('<td>-</td>');
|
||||
});
|
||||
|
||||
it('null time column should be rendered as -', () => {
|
||||
var html = renderer.renderCell(0, null);
|
||||
var html = renderer.renderCell(0, 0, null);
|
||||
expect(html).to.be('<td>-</td>');
|
||||
});
|
||||
|
||||
it('number column with unit specified should ignore style unit', () => {
|
||||
var html = renderer.renderCell(5, 1230);
|
||||
var html = renderer.renderCell(5, 0, 1230);
|
||||
expect(html).to.be('<td>1.23 kbps</td>');
|
||||
});
|
||||
|
||||
it('number column should be formated', () => {
|
||||
var html = renderer.renderCell(1, 1230);
|
||||
var html = renderer.renderCell(1, 0, 1230);
|
||||
expect(html).to.be('<td>1.230 s</td>');
|
||||
});
|
||||
|
||||
it('number style should ignore string values', () => {
|
||||
var html = renderer.renderCell(1, 'asd');
|
||||
var html = renderer.renderCell(1, 0, 'asd');
|
||||
expect(html).to.be('<td>asd</td>');
|
||||
});
|
||||
|
||||
it('colored cell should have style', () => {
|
||||
var html = renderer.renderCell(2, 40);
|
||||
var html = renderer.renderCell(2, 0, 40);
|
||||
expect(html).to.be('<td style="color:green">40.0</td>');
|
||||
});
|
||||
|
||||
it('colored cell should have style', () => {
|
||||
var html = renderer.renderCell(2, 55);
|
||||
var html = renderer.renderCell(2, 0, 55);
|
||||
expect(html).to.be('<td style="color:orange">55.0</td>');
|
||||
});
|
||||
|
||||
it('colored cell should have style', () => {
|
||||
var html = renderer.renderCell(2, 85);
|
||||
var html = renderer.renderCell(2, 0, 85);
|
||||
expect(html).to.be('<td style="color:red">85.0</td>');
|
||||
});
|
||||
|
||||
it('unformated undefined should be rendered as string', () => {
|
||||
var html = renderer.renderCell(3, 'value');
|
||||
var html = renderer.renderCell(3, 0, 'value');
|
||||
expect(html).to.be('<td>value</td>');
|
||||
});
|
||||
|
||||
it('string style with escape html should return escaped html', () => {
|
||||
var html = renderer.renderCell(4, "&breaking <br /> the <br /> row");
|
||||
var html = renderer.renderCell(4, 0, "&breaking <br /> the <br /> row");
|
||||
expect(html).to.be('<td>&breaking <br /> the <br /> row</td>');
|
||||
});
|
||||
|
||||
it('undefined formater should return escaped html', () => {
|
||||
var html = renderer.renderCell(3, "&breaking <br /> the <br /> row");
|
||||
var html = renderer.renderCell(3, 0, "&breaking <br /> the <br /> row");
|
||||
expect(html).to.be('<td>&breaking <br /> the <br /> row</td>');
|
||||
});
|
||||
|
||||
it('undefined value should render as -', () => {
|
||||
var html = renderer.renderCell(3, undefined);
|
||||
var html = renderer.renderCell(3, 0, undefined);
|
||||
expect(html).to.be('<td></td>');
|
||||
});
|
||||
|
||||
it('sanitized value should render as', () => {
|
||||
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
|
||||
var html = renderer.renderCell(6, 0, 'text <a href="http://google.com">link</a>');
|
||||
expect(html).to.be('<td>sanitized</td>');
|
||||
});
|
||||
|
||||
@ -146,7 +171,22 @@ describe('when rendering table', () => {
|
||||
it('Colored column title should be Colored', () => {
|
||||
expect(table.columns[2].title).to.be('Colored');
|
||||
});
|
||||
|
||||
it('link should render as', () => {
|
||||
var html = renderer.renderCell(7, 0, 'host1');
|
||||
var expectedHtml = `
|
||||
<td class="table-panel-cell-link">
|
||||
<a href="/dashboard?param=host1¶m_1=1230¶m_2=40"
|
||||
target="_blank" data-link-tooltip data-original-title="host1 1230 my.host.com" data-placement="right">
|
||||
host1
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
expect(normalize(html)).to.be(normalize(expectedHtml));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function normalize(str) {
|
||||
return str.replace(/\s+/gm, ' ').trim();
|
||||
}
|
||||
|
@ -76,6 +76,21 @@
|
||||
&.table-panel-cell-pre {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
&.table-panel-cell-link {
|
||||
// Expand internal div to cell size (make all cell clickable)
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
padding: 0.45em 0 0.45em 1.1em;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.cell-highlighted:hover {
|
||||
background-color: $tight-form-func-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user