Просмотр исходного кода

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
Alexander Zobnin 8 лет назад
Родитель
Сommit
9bbc942534

+ 30 - 0
public/app/plugins/panel/table/column_options.html

@@ -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)">

+ 8 - 2
public/app/plugins/panel/table/module.ts

@@ -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() {

+ 41 - 5
public/app/plugins/panel/table/renderer.ts

@@ -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");
+    }
+
+    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) {
@@ -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) {

+ 56 - 16
public/app/plugins/panel/table/specs/renderer_specs.ts

@@ -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&param_1=$__cell_1&param_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>&amp;breaking &lt;br /&gt; the &lt;br /&gt; 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>&amp;breaking &lt;br /&gt; the &lt;br /&gt; 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&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();
+}

+ 15 - 0
public/sass/components/_panel_table.scss

@@ -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;
+    }
   }
 }