Commit 9bbc9425 by Alexander Zobnin Committed by Torkel Ödegaard

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
parent 13c966c1
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<label class="gf-form-label width-13">Column Header</label> <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"> <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> </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>
<div class="section gf-form-group"> <div class="section gf-form-group">
...@@ -91,6 +92,35 @@ ...@@ -91,6 +92,35 @@
</div> </div>
</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> <div class="clearfix"></div>
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)"> <button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
......
...@@ -10,6 +10,7 @@ import {transformDataToTable} from './transformers'; ...@@ -10,6 +10,7 @@ import {transformDataToTable} from './transformers';
import {tablePanelEditor} from './editor'; import {tablePanelEditor} from './editor';
import {columnOptionsTab} from './column_options'; import {columnOptionsTab} from './column_options';
import {TableRenderer} from './renderer'; import {TableRenderer} from './renderer';
import Drop from 'tether-drop';
class TablePanelCtrl extends MetricsPanelCtrl { class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
...@@ -49,7 +50,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -49,7 +50,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
}; };
/** @ngInject */ /** @ngInject */
constructor($scope, $injector, private annotationsSrv, private $sanitize) { constructor($scope, $injector, templateSrv, private annotationsSrv, private $sanitize) {
super($scope, $injector); super($scope, $injector);
this.pageIndex = 0; this.pageIndex = 0;
...@@ -123,7 +124,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -123,7 +124,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
this.table = transformDataToTable(this.dataRaw, this.panel); this.table = transformDataToTable(this.dataRaw, this.panel);
this.table.sort(this.panel.sort); 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); return super.render(this.table);
} }
...@@ -217,6 +218,11 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -217,6 +218,11 @@ class TablePanelCtrl extends MetricsPanelCtrl {
rootElem.css({'max-height': panel.scroll ? getTableHeight() : '' }); 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); elem.on('click', '.table-panel-page-link', switchPage);
var unbindDestroy = scope.$on('$destroy', function() { var unbindDestroy = scope.$on('$destroy', function() {
......
...@@ -8,7 +8,7 @@ export class TableRenderer { ...@@ -8,7 +8,7 @@ export class TableRenderer {
formatters: any[]; formatters: any[];
colorState: 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(); this.initColumns();
} }
...@@ -123,13 +123,25 @@ export class TableRenderer { ...@@ -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) { formatColumnValue(colIndex, value) {
return this.formatters[colIndex] ? this.formatters[colIndex](value) : 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); value = this.formatColumnValue(columnIndex, value);
var style = ''; var style = '';
var cellClasses = [];
var cellClass = ''; var cellClass = '';
if (this.colorState.cell) { if (this.colorState.cell) {
style = ' style="background-color:' + this.colorState.cell + ';color: white"'; style = ' style="background-color:' + this.colorState.cell + ';color: white"';
...@@ -156,10 +168,34 @@ export class TableRenderer { ...@@ -156,10 +168,34 @@ export class TableRenderer {
var columnStyle = this.table.columns[columnIndex].style; var columnStyle = this.table.columns[columnIndex].style;
if (columnStyle && columnStyle.preserveFormat) { if (columnStyle && columnStyle.preserveFormat) {
cellClass = ' class="table-panel-cell-pre" '; cellClasses.push("table-panel-cell-pre");
}
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(' ') + '"';
} }
return '<td' + cellClass + style + '>' + value + widthHack + '</td>'; columnHtml = '<td' + cellClass + style + '>' + columnHtml + '</td>';
return columnHtml;
} }
render(page) { render(page) {
...@@ -173,7 +209,7 @@ export class TableRenderer { ...@@ -173,7 +209,7 @@ export class TableRenderer {
let cellHtml = ''; let cellHtml = '';
let rowStyle = ''; let rowStyle = '';
for (var i = 0; i < this.table.columns.length; i++) { 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) { if (this.colorState.row) {
......
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import _ from 'lodash';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import {TableRenderer} from '../renderer'; import {TableRenderer} from '../renderer';
...@@ -14,6 +15,10 @@ describe('when rendering table', () => { ...@@ -14,6 +15,10 @@ describe('when rendering table', () => {
{text: 'String'}, {text: 'String'},
{text: 'United', unit: 'bps'}, {text: 'United', unit: 'bps'},
{text: 'Sanitized'}, {text: 'Sanitized'},
{text: 'Link'},
];
table.rows = [
[1388556366666, 1230, 40, undefined, "", "", "my.host.com", "host1"]
]; ];
var panel = { var panel = {
...@@ -55,6 +60,14 @@ describe('when rendering table', () => { ...@@ -55,6 +60,14 @@ describe('when rendering table', () => {
pattern: 'Sanitized', pattern: 'Sanitized',
type: 'string', type: 'string',
sanitize: true, sanitize: true,
},
{
pattern: 'Link',
type: 'string',
link: true,
linkUrl: "/dashboard?param=$__cell&param_1=$__cell_1&param_2=$__cell_2",
linkTooltip: "$__cell $__cell_1 $__cell_6",
linkTargetBlank: true
} }
] ]
}; };
...@@ -63,75 +76,87 @@ describe('when rendering table', () => { ...@@ -63,75 +76,87 @@ describe('when rendering table', () => {
return 'sanitized'; 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', () => { 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>'); expect(html).to.be('<td>2014-01-01T06:06:06Z</td>');
}); });
it('undefined time column should be rendered as -', () => { 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>'); expect(html).to.be('<td>-</td>');
}); });
it('null time column should be rendered as -', () => { 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>'); expect(html).to.be('<td>-</td>');
}); });
it('number column with unit specified should ignore style unit', () => { 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>'); expect(html).to.be('<td>1.23 kbps</td>');
}); });
it('number column should be formated', () => { 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>'); expect(html).to.be('<td>1.230 s</td>');
}); });
it('number style should ignore string values', () => { 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>'); expect(html).to.be('<td>asd</td>');
}); });
it('colored cell should have style', () => { 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>'); expect(html).to.be('<td style="color:green">40.0</td>');
}); });
it('colored cell should have style', () => { 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>'); expect(html).to.be('<td style="color:orange">55.0</td>');
}); });
it('colored cell should have style', () => { 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>'); expect(html).to.be('<td style="color:red">85.0</td>');
}); });
it('unformated undefined should be rendered as string', () => { 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>'); expect(html).to.be('<td>value</td>');
}); });
it('string style with escape html should return escaped html', () => { 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>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>'); expect(html).to.be('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
}); });
it('undefined formater should return escaped html', () => { 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>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>'); expect(html).to.be('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
}); });
it('undefined value should render as -', () => { 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>'); expect(html).to.be('<td></td>');
}); });
it('sanitized value should render as', () => { 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>'); expect(html).to.be('<td>sanitized</td>');
}); });
...@@ -146,7 +171,22 @@ describe('when rendering table', () => { ...@@ -146,7 +171,22 @@ describe('when rendering table', () => {
it('Colored column title should be Colored', () => { it('Colored column title should be Colored', () => {
expect(table.columns[2].title).to.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&param_1=1230&param_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 @@ ...@@ -76,6 +76,21 @@
&.table-panel-cell-pre { &.table-panel-cell-pre {
white-space: 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;
}
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment