From 36c4d01ef8de654b2846803bc0a5dccebd38a2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Nov 2015 17:00:47 +0100 Subject: [PATCH 01/16] feat(tablepanel) began work on new table panel --- public/app/core/settings.js | 1 + .../core/{time_series.js => time_series.ts} | 83 ++++++----- public/app/panels/graph/module.html | 4 - public/app/panels/table/module.html | 4 + public/app/panels/table/module.ts | 136 ++++++++++++++++++ 5 files changed, 192 insertions(+), 36 deletions(-) rename public/app/core/{time_series.js => time_series.ts} (77%) create mode 100644 public/app/panels/table/module.html create mode 100644 public/app/panels/table/module.ts diff --git a/public/app/core/settings.js b/public/app/core/settings.js index b7fb6e53c14..e452b59b904 100644 --- a/public/app/core/settings.js +++ b/public/app/core/settings.js @@ -10,6 +10,7 @@ function (_) { window_title_prefix : 'Grafana - ', panels : { 'graph': { path: 'app/panels/graph', name: 'Graph' }, + 'table': { path: 'app/panels/table', name: 'Table' }, 'singlestat': { path: 'app/panels/singlestat', name: 'Single stat' }, 'text': { path: 'app/panels/text', name: 'Text' }, 'dashlist': { path: 'app/panels/dashlist', name: 'Dashboard list' }, diff --git a/public/app/core/time_series.js b/public/app/core/time_series.ts similarity index 77% rename from public/app/core/time_series.js rename to public/app/core/time_series.ts index 9465232c200..30086be622a 100644 --- a/public/app/core/time_series.js +++ b/public/app/core/time_series.ts @@ -1,11 +1,46 @@ -define([ - 'lodash', - 'app/core/utils/kbn' -], -function (_, kbn) { - 'use strict'; +/// - function TimeSeries(opts) { +import _ = require('lodash'); +import kbn = require('app/core/utils/kbn'); + +function matchSeriesOverride(aliasOrRegex, seriesAlias) { + if (!aliasOrRegex) { return false; } + + if (aliasOrRegex[0] === '/') { + var regex = kbn.stringToJsRegex(aliasOrRegex); + return seriesAlias.match(regex) != null; + } + + return aliasOrRegex === seriesAlias; +} + +function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill/10; +} + +class TimeSeries { + datapoints: any; + id: string; + label: string; + alias: string; + color: string; + valueFormater: any; + stats: any; + legend: boolean; + allIsNull: boolean; + decimals: number; + scaledDecimals: number; + + lines: any; + bars: any; + points: any; + yaxis: any; + zindex: any; + stack: any; + fillBelowTo: any; + transform: any; + + constructor(opts) { this.datapoints = opts.datapoints; this.label = opts.alias; this.id = opts.alias; @@ -16,22 +51,7 @@ function (_, kbn) { this.legend = true; } - function matchSeriesOverride(aliasOrRegex, seriesAlias) { - if (!aliasOrRegex) { return false; } - - if (aliasOrRegex[0] === '/') { - var regex = kbn.stringToJsRegex(aliasOrRegex); - return seriesAlias.match(regex) != null; - } - - return aliasOrRegex === seriesAlias; - } - - function translateFillOption(fill) { - return fill === 0 ? 0.001 : fill/10; - } - - TimeSeries.prototype.applySeriesOverrides = function(overrides) { + applySeriesOverrides(overrides) { this.lines = {}; this.points = {}; this.bars = {}; @@ -64,7 +84,7 @@ function (_, kbn) { } }; - TimeSeries.prototype.getFlotPairs = function (fillStyle) { + getFlotPairs(fillStyle) { var result = []; this.stats.total = 0; @@ -124,18 +144,17 @@ function (_, kbn) { } return result; - }; + } - TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) { + updateLegendValues(formater, decimals, scaledDecimals) { this.valueFormater = formater; this.decimals = decimals; this.scaledDecimals = scaledDecimals; - }; + } - TimeSeries.prototype.formatValue = function(value) { + formatValue(value) { return this.valueFormater(value, this.decimals, this.scaledDecimals); - }; + } +} - return TimeSeries; - -}); +export = TimeSeries; diff --git a/public/app/panels/graph/module.html b/public/app/panels/graph/module.html index b0793780fa8..9c49c42741d 100644 --- a/public/app/panels/graph/module.html +++ b/public/app/panels/graph/module.html @@ -3,10 +3,6 @@
- - - -
No datapoints No datapoints returned from metric query diff --git a/public/app/panels/table/module.html b/public/app/panels/table/module.html new file mode 100644 index 00000000000..0463d751817 --- /dev/null +++ b/public/app/panels/table/module.html @@ -0,0 +1,4 @@ + +
+
+
diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts new file mode 100644 index 00000000000..e65278d44a8 --- /dev/null +++ b/public/app/panels/table/module.ts @@ -0,0 +1,136 @@ +/// + +import angular = require('angular'); +import $ = require('jquery'); +import _ = require('lodash'); +import moment = require('moment'); +import PanelMeta = require('app/features/panel/panel_meta'); +import TimeSeries = require('app/core/time_series'); + +var panelDefaults = { + targets: [{}], +}; + +export class TablePanelCtrl { + + constructor($scope, $rootScope, $q, panelSrv, panelHelper) { + $scope.ctrl = this; + + $scope.panelMeta = new PanelMeta({ + panelName: 'Table', + editIcon: "fa fa-table", + fullscreen: true, + metricsEditor: true, + }); + + $scope.panelMeta.addEditorTab('Options', 'app/panels/table/options.html'); + $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html'); + + _.defaults($scope.panel, panelDefaults); + + $scope.refreshData = function(datasource) { + var data = { + columns: [], + rows: [], + }; + + data.columns.push({text: 'Time'}); + data.columns.push({text: 'Value'}); + data.columns.push({text: 'Value2'}); + data.rows.push([ + moment().format('LLL'), 17.2, 15.12 + ]); + data.rows.push([ + moment().format('LLL'), 12.2, 122.3244 + ]); + data.rows.push([ + moment().format('LLL'), 111.2, 2312.22 + ]); + + panelHelper.broadcastRender($scope, data); + + // panelHelper.updateTimeRange($scope); + // + // return panelHelper.issueMetricQuery($scope, datasource) + // .then($scope.dataHandler, function(err) { + // $scope.seriesList = []; + // $scope.render([]); + // throw err; + // }); + }; + + $scope.dataHandler = function(results) { + $scope.seriesList = _.map(results.data, $scope.seriesHandler); + panelHelper.broadcastRender($scope, $scope.seriesList); + }; + + $scope.seriesHandler = function(seriesData, index) { + var datapoints = seriesData.datapoints; + var alias = seriesData.target; + var colorIndex = index % $rootScope.colors.length; + var color = $scope.panel.aliasColors[alias] || $rootScope.colors[colorIndex]; + + var series = new TimeSeries({ + datapoints: datapoints, + alias: alias, + color: color, + }); + + return series; + }; + + panelSrv.init($scope); + } +} + +export function tablePanelDirective() { + 'use strict'; + return { + restrict: 'E', + templateUrl: 'app/panels/table/module.html', + controller: TablePanelCtrl, + link: function(scope, elem) { + var data; + + function renderPanel() { + var rootDiv = elem.find('.table-panel-container'); + var tableDiv = $('
'); + var i, y, rowElem, colElem, column, row; + + rowElem = $(''); + for (i = 0; i < data.columns.length; i++) { + column = data.columns[i]; + colElem = $('' + column.text + ''); + rowElem.append(colElem); + } + + tableDiv.append(rowElem); + + for (y = 0; y < data.rows.length; y++) { + row = data.rows[y]; + rowElem = $('') + for (i = 0; i < data.columns.length; i++) { + colElem = $('' + row[i] + ''); + rowElem.append(colElem); + } + tableDiv.append(rowElem); + } + + rootDiv.empty(); + rootDiv.append(tableDiv); + } + + scope.$on('render', function(event, renderData) { + data = renderData || data; + if (!data) { + scope.get_data(); + return; + } + renderPanel(); + }); + } + }; +} + +angular.module('grafana.directives').directive('grafanaPanelTable', tablePanelDirective); + From 8171cd51c472d4cb5b82d9b03530ed2f66fa7c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 2 Nov 2015 20:51:49 +0100 Subject: [PATCH 02/16] feat(tablepanel): minor progress on table panel --- public/app/panels/table/module.html | 10 ++-- public/app/panels/table/module.ts | 60 ++++--------------- .../panels/table/specs/table_model_specs.ts | 28 +++++++++ public/app/panels/table/table_model.ts | 43 +++++++++++++ public/less/grafana.less | 7 ++- .../{dashlist.less => panel_dashlist.less} | 0 public/less/{graph.less => panel_graph.less} | 0 ...{singlestat.less => panel_singlestat.less} | 0 public/less/panel_table.less | 49 +++++++++++++++ 9 files changed, 143 insertions(+), 54 deletions(-) create mode 100644 public/app/panels/table/specs/table_model_specs.ts create mode 100644 public/app/panels/table/table_model.ts rename public/less/{dashlist.less => panel_dashlist.less} (100%) rename public/less/{graph.less => panel_graph.less} (100%) rename public/less/{singlestat.less => panel_singlestat.less} (100%) create mode 100644 public/less/panel_table.less diff --git a/public/app/panels/table/module.html b/public/app/panels/table/module.html index 0463d751817..c85252354dc 100644 --- a/public/app/panels/table/module.html +++ b/public/app/panels/table/module.html @@ -1,4 +1,6 @@ - -
-
-
+
+ +
+
+
+
diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index e65278d44a8..36094dd90b2 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -6,6 +6,7 @@ import _ = require('lodash'); import moment = require('moment'); import PanelMeta = require('app/features/panel/panel_meta'); import TimeSeries = require('app/core/time_series'); +import {TableModel} from './table_model'; var panelDefaults = { targets: [{}], @@ -29,54 +30,19 @@ export class TablePanelCtrl { _.defaults($scope.panel, panelDefaults); $scope.refreshData = function(datasource) { - var data = { - columns: [], - rows: [], - }; + panelHelper.updateTimeRange($scope); - data.columns.push({text: 'Time'}); - data.columns.push({text: 'Value'}); - data.columns.push({text: 'Value2'}); - data.rows.push([ - moment().format('LLL'), 17.2, 15.12 - ]); - data.rows.push([ - moment().format('LLL'), 12.2, 122.3244 - ]); - data.rows.push([ - moment().format('LLL'), 111.2, 2312.22 - ]); - - panelHelper.broadcastRender($scope, data); - - // panelHelper.updateTimeRange($scope); - // - // return panelHelper.issueMetricQuery($scope, datasource) - // .then($scope.dataHandler, function(err) { - // $scope.seriesList = []; - // $scope.render([]); - // throw err; - // }); + return panelHelper.issueMetricQuery($scope, datasource) + .then($scope.dataHandler, function(err) { + $scope.seriesList = []; + $scope.render([]); + throw err; + }); }; $scope.dataHandler = function(results) { - $scope.seriesList = _.map(results.data, $scope.seriesHandler); - panelHelper.broadcastRender($scope, $scope.seriesList); - }; - - $scope.seriesHandler = function(seriesData, index) { - var datapoints = seriesData.datapoints; - var alias = seriesData.target; - var colorIndex = index % $rootScope.colors.length; - var color = $scope.panel.aliasColors[alias] || $rootScope.colors[colorIndex]; - - var series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: color, - }); - - return series; + $scope.tableModel = TableModel.transform(results.data, $scope.panel); + panelHelper.broadcastRender($scope, $scope.tableModel); }; panelSrv.init($scope); @@ -94,13 +60,13 @@ export function tablePanelDirective() { function renderPanel() { var rootDiv = elem.find('.table-panel-container'); - var tableDiv = $('
'); + var tableDiv = $('
'); var i, y, rowElem, colElem, column, row; rowElem = $(''); for (i = 0; i < data.columns.length; i++) { column = data.columns[i]; - colElem = $('' + column.text + ''); + colElem = $('' + column.text + ''); rowElem.append(colElem); } @@ -108,7 +74,7 @@ export function tablePanelDirective() { for (y = 0; y < data.rows.length; y++) { row = data.rows[y]; - rowElem = $('') + rowElem = $(''); for (i = 0; i < data.columns.length; i++) { colElem = $('' + row[i] + ''); rowElem.append(colElem); diff --git a/public/app/panels/table/specs/table_model_specs.ts b/public/app/panels/table/specs/table_model_specs.ts new file mode 100644 index 00000000000..7cb9575eb86 --- /dev/null +++ b/public/app/panels/table/specs/table_model_specs.ts @@ -0,0 +1,28 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {TableModel} from '../table_model'; + +describe('when getting tableData', () => { + + describe('simple time series', () => { + var panel = { + }; + + it ('should return 2 columns', () => { + var data = TableModel.transform([ + { + target: 'test', + datapoints: [[12.12, new Date().getTime()]], + } + ], panel); + + expect(data.columns.length).to.be(2); + expect(data.rows.length).to.be(1); + + expect(data.columns[0].text).to.be('Time'); + expect(data.columns[1].text).to.be('Value'); + }); + + }); + +}); diff --git a/public/app/panels/table/table_model.ts b/public/app/panels/table/table_model.ts new file mode 100644 index 00000000000..0351f30b33b --- /dev/null +++ b/public/app/panels/table/table_model.ts @@ -0,0 +1,43 @@ +/// + +import moment = require('moment'); +import _ = require('lodash'); + +export class TableModel { + columns: any[]; + rows: any[]; + + static transform(data, panel) { + var model = new TableModel(); + + if (!data || data.length === 0) { + return model; + } + + model.columns = [ + {text: 'Time'}, + {text: 'Value'}, + ]; + model.rows = []; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + for (var y = 0; y < series.datapoints.length; y++) { + var dp = series.datapoints[y]; + var time = moment(dp[1]).format('LLL'); + var value = dp[0]; + if (value === null) { + value = 'null'; + } else if (_.isNumber(value)) { + value = value.toFixed(2); + } + + model.rows.push([time, value]); + } + } + + return model; + } +} + + diff --git a/public/less/grafana.less b/public/less/grafana.less index 6e23faa214d..e82c3b93c01 100644 --- a/public/less/grafana.less +++ b/public/less/grafana.less @@ -1,18 +1,19 @@ @import "type.less"; @import "login.less"; @import "submenu.less"; -@import "graph.less"; +@import "panel_graph.less"; +@import "panel_dashlist.less"; +@import "panel_singlestat.less"; +@import "panel_table.less"; @import "bootstrap-tagsinput.less"; @import "tables_lists.less"; @import "search.less"; @import "panel.less"; @import "forms.less"; -@import "singlestat.less"; @import "tightform.less"; @import "sidemenu.less"; @import "navbar.less"; @import "gfbox.less"; -@import "dashlist.less"; @import "admin.less"; @import "validation.less"; @import "fonts.less"; diff --git a/public/less/dashlist.less b/public/less/panel_dashlist.less similarity index 100% rename from public/less/dashlist.less rename to public/less/panel_dashlist.less diff --git a/public/less/graph.less b/public/less/panel_graph.less similarity index 100% rename from public/less/graph.less rename to public/less/panel_graph.less diff --git a/public/less/singlestat.less b/public/less/panel_singlestat.less similarity index 100% rename from public/less/singlestat.less rename to public/less/panel_singlestat.less diff --git a/public/less/panel_table.less b/public/less/panel_table.less new file mode 100644 index 00000000000..a46fb7e65b9 --- /dev/null +++ b/public/less/panel_table.less @@ -0,0 +1,49 @@ +.table-panel-wrapper { + .panel-content { + padding: 0; + } + .panel-title-container { + padding-bottom: 4px; + } +} + +.gf-table-panel* { + box-sizing: border-box; +} + +.gf-table-panel { + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +.gf-table-panel tr { + border-bottom: 2px solid @bodyBackground; +} + +.gf-table-panel th { + background: @grafanaTargetFuncBackground; + padding: 5px 0 5px 15px; + text-align: left; + border-top: 2px solid @bodyBackground; + + &:first-child { + padding-left: 15px; + } +} + +.gf-table-panel td { + padding: 15px 0 15px 15px; + + &:first-child { + padding-left: 15px; + } +} + +.gf-table-panel .ellipsis { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} From da9c792ca2edcf79d30569e266c4ea636f86a837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Nov 2015 08:18:35 +0100 Subject: [PATCH 03/16] feat(tablepanel): minor progress --- public/app/panels/table/module.ts | 20 ++++++++++++++++++-- public/less/panel_table.less | 4 ++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 36094dd90b2..22b087bdfa4 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -58,6 +58,15 @@ export function tablePanelDirective() { link: function(scope, elem) { var data; + function getTableHeight() { + var panelHeight = scope.height || scope.panel.height || scope.row.height; + if (_.isString(panelHeight)) { + panelHeight = parseInt(panelHeight.replace('px', ''), 10); + } + + return (panelHeight - 40) + 'px'; + } + function renderPanel() { var rootDiv = elem.find('.table-panel-container'); var tableDiv = $('
'); @@ -70,8 +79,10 @@ export function tablePanelDirective() { rowElem.append(colElem); } - tableDiv.append(rowElem); + var headElem = $(''); + headElem.append(rowElem); + var tbodyElem = $(''); for (y = 0; y < data.rows.length; y++) { row = data.rows[y]; rowElem = $(''); @@ -79,9 +90,14 @@ export function tablePanelDirective() { colElem = $('' + row[i] + ''); rowElem.append(colElem); } - tableDiv.append(rowElem); + tbodyElem.append(rowElem); } + tableDiv.append(headElem); + tableDiv.append(tbodyElem); + + rootDiv.css({'max-height': getTableHeight()}); + rootDiv.empty(); rootDiv.append(tableDiv); } diff --git a/public/less/panel_table.less b/public/less/panel_table.less index a46fb7e65b9..8cd62cbb76b 100644 --- a/public/less/panel_table.less +++ b/public/less/panel_table.less @@ -7,6 +7,10 @@ } } +.table-panel-container { + overflow: auto; +} + .gf-table-panel* { box-sizing: border-box; } From 867b838053d6a903fcad5d73201225cbb23217a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 3 Nov 2015 16:19:51 +0100 Subject: [PATCH 04/16] feat(tablepanel): work on table panel --- public/app/panels/graph/styleEditor.html | 2 +- public/app/panels/table/module.ts | 29 ++++--- public/app/panels/table/options.html | 58 +++++++++++++ .../panels/table/specs/table_model_specs.ts | 49 +++++++++-- public/app/panels/table/table_model.ts | 81 ++++++++++++++++--- public/less/panel_table.less | 1 - 6 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 public/app/panels/table/options.html diff --git a/public/app/panels/graph/styleEditor.html b/public/app/panels/graph/styleEditor.html index 5d5f2fd7401..f692328a0fd 100644 --- a/public/app/panels/graph/styleEditor.html +++ b/public/app/panels/graph/styleEditor.html @@ -63,7 +63,7 @@
Series specific overrides Regex match example: /server[0-3]/i
-
+
  • diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 22b087bdfa4..8cb46d6cc46 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -6,16 +6,13 @@ import _ = require('lodash'); import moment = require('moment'); import PanelMeta = require('app/features/panel/panel_meta'); import TimeSeries = require('app/core/time_series'); -import {TableModel} from './table_model'; - -var panelDefaults = { - targets: [{}], -}; +import {TableModel, transformers} from './table_model'; export class TablePanelCtrl { constructor($scope, $rootScope, $q, panelSrv, panelHelper) { $scope.ctrl = this; + $scope.transformers = transformers; $scope.panelMeta = new PanelMeta({ panelName: 'Table', @@ -27,21 +24,31 @@ export class TablePanelCtrl { $scope.panelMeta.addEditorTab('Options', 'app/panels/table/options.html'); $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html'); + var panelDefaults = { + targets: [{}], + transform: 'timeseries_to_rows' + }; + _.defaults($scope.panel, panelDefaults); $scope.refreshData = function(datasource) { panelHelper.updateTimeRange($scope); return panelHelper.issueMetricQuery($scope, datasource) - .then($scope.dataHandler, function(err) { - $scope.seriesList = []; - $scope.render([]); - throw err; - }); + .then($scope.dataHandler, function(err) { + $scope.seriesList = []; + $scope.render([]); + throw err; + }); }; $scope.dataHandler = function(results) { - $scope.tableModel = TableModel.transform(results.data, $scope.panel); + $scope.dataRaw = results.data; + $scope.render(); + }; + + $scope.render = function() { + $scope.tableModel = TableModel.transform($scope.dataRaw, $scope.panel); panelHelper.broadcastRender($scope, $scope.tableModel); }; diff --git a/public/app/panels/table/options.html b/public/app/panels/table/options.html new file mode 100644 index 00000000000..7f1332b6e18 --- /dev/null +++ b/public/app/panels/table/options.html @@ -0,0 +1,58 @@ +
    +
    +
    Data Table
    +
    +
      +
    • + Data to Table Transform +
    • +
    • + +
    • +
    +
    +
    +
    +
    + +
    +
    +
    Table Display
    +
    +
    + +
    +
    +
    Column Styles
    + +
    +
    +
      +
    • + +
    • + +
    • + alias or regex +
    • + +
    • + +
    • + + +
    +
    +
    +
    + + +
    +
    + diff --git a/public/app/panels/table/specs/table_model_specs.ts b/public/app/panels/table/specs/table_model_specs.ts index 7cb9575eb86..04106100392 100644 --- a/public/app/panels/table/specs/table_model_specs.ts +++ b/public/app/panels/table/specs/table_model_specs.ts @@ -4,25 +4,62 @@ import {TableModel} from '../table_model'; describe('when getting tableData', () => { - describe('simple time series', () => { + describe('timeseries_to_rows', () => { var panel = { + transform: 'timeseries_to_rows' }; - it ('should return 2 columns', () => { + it ('should return 2 rows', () => { var data = TableModel.transform([ { - target: 'test', + target: 'series1', + datapoints: [[12.12, new Date().getTime()]], + }, + { + target: 'series2', datapoints: [[12.12, new Date().getTime()]], } ], panel); - expect(data.columns.length).to.be(2); + expect(data.columns.length).to.be(3); + expect(data.rows.length).to.be(2); + + expect(data.columns[0].text).to.be('Time'); + expect(data.columns[1].text).to.be('Series'); + expect(data.columns[2].text).to.be('Value'); + expect(data.rows[0][1]).to.be('series1'); + expect(data.rows[0][2]).to.be('12.12'); + + expect(data.rows[1][1]).to.be('series2'); + }); + }); + + describe('timeseries_to_rows', () => { + var panel = { + transform: 'timeseries_to_columns' + }; + + it ('should return 3 columns', () => { + var data = TableModel.transform([ + { + target: 'series1', + datapoints: [[12.12, new Date().getTime()]], + }, + { + target: 'series2', + datapoints: [[16.12, new Date().getTime()]], + } + ], panel); + + expect(data.columns.length).to.be(3); expect(data.rows.length).to.be(1); expect(data.columns[0].text).to.be('Time'); - expect(data.columns[1].text).to.be('Value'); + expect(data.columns[1].text).to.be('series1'); + expect(data.columns[2].text).to.be('series2'); + expect(data.rows[0][1]).to.be('12.12'); + expect(data.rows[0][2]).to.be('16.12'); }); - }); }); diff --git a/public/app/panels/table/table_model.ts b/public/app/panels/table/table_model.ts index 0351f30b33b..9410c36badf 100644 --- a/public/app/panels/table/table_model.ts +++ b/public/app/panels/table/table_model.ts @@ -3,21 +3,17 @@ import moment = require('moment'); import _ = require('lodash'); -export class TableModel { - columns: any[]; - rows: any[]; - - static transform(data, panel) { - var model = new TableModel(); - - if (!data || data.length === 0) { - return model; - } +var transformers = {}; +transformers['timeseries_to_rows'] = { + description: 'Time series to rows', + transform: function(data, panel, model) { model.columns = [ {text: 'Time'}, + {text: 'Series'}, {text: 'Value'}, ]; + model.rows = []; for (var i = 0; i < data.length; i++) { @@ -32,12 +28,71 @@ export class TableModel { value = value.toFixed(2); } - model.rows.push([time, value]); + model.rows.push([time, series.target, value]); + } + } + }, +}; + +transformers['timeseries_to_columns'] = { + description: 'Time series to columns', + transform: function(data, panel, model) { + model.columns = [{text: 'Time'}]; + model.rows = []; + + var points = {}; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + model.columns.push({text: series.target}); + + for (var y = 0; y < series.datapoints.length; y++) { + var dp = series.datapoints[y]; + var time = dp[1]; + if (!points[time]) { + points[time] = {}; + points[time][i] = [dp[0]]; + } + else { + points[time][i] = dp[0]; + } } } + for (var time in points) { + var point = points[time]; + var values = [time]; + + for (var i = 0; i < data.length; i++) { + if (point[i] !== undefined) { + values.push(point[i]); + } + } + + model.rows.push(values); + } + } +}; + +export {transformers} + +export class TableModel { + columns: any[]; + rows: any[]; + + static transform(data, panel) { + var model = new TableModel(); + + if (!data || data.length === 0) { + return model; + } + + var transformer = transformers[panel.transform]; + if (!transformer) { + throw {message: 'Transformer ' + panel.transformer + ' not found'}; + } + + transformer.transform(data, panel, model); return model; } } - - diff --git a/public/less/panel_table.less b/public/less/panel_table.less index 8cd62cbb76b..8f7dea39f9a 100644 --- a/public/less/panel_table.less +++ b/public/less/panel_table.less @@ -17,7 +17,6 @@ .gf-table-panel { width: 100%; - table-layout: fixed; border-collapse: collapse; } From 6062930f9a69374670ff671ad1961f7f7db08a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 4 Nov 2015 09:41:03 +0100 Subject: [PATCH 05/16] feat(tablepanel): added more unit tests for table transforms --- public/app/panels/table/module.ts | 8 +- public/app/panels/table/options.html | 11 ++- .../panels/table/specs/table_model_specs.ts | 65 -------------- .../panels/table/specs/transformers_specs.ts | 72 ++++++++++++++++ public/app/panels/table/table_model.ts | 78 +---------------- public/app/panels/table/transformers.ts | 84 +++++++++++++++++++ 6 files changed, 172 insertions(+), 146 deletions(-) delete mode 100644 public/app/panels/table/specs/table_model_specs.ts create mode 100644 public/app/panels/table/specs/transformers_specs.ts create mode 100644 public/app/panels/table/transformers.ts diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 8cb46d6cc46..6103e11113b 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -6,7 +6,9 @@ import _ = require('lodash'); import moment = require('moment'); import PanelMeta = require('app/features/panel/panel_meta'); import TimeSeries = require('app/core/time_series'); -import {TableModel, transformers} from './table_model'; + +import {TableModel} from './table_model'; +import {transformers} from './transformers'; export class TablePanelCtrl { @@ -26,7 +28,9 @@ export class TablePanelCtrl { var panelDefaults = { targets: [{}], - transform: 'timeseries_to_rows' + transform: 'timeseries_to_rows', + pageSize: 50, + showHeader: true, }; _.defaults($scope.panel, panelDefaults); diff --git a/public/app/panels/table/options.html b/public/app/panels/table/options.html index 7f1332b6e18..717259df40f 100644 --- a/public/app/panels/table/options.html +++ b/public/app/panels/table/options.html @@ -1,10 +1,10 @@
    -
    Data Table
    +
    Data
    • - Data to Table Transform + To Table Transform
    • +
    diff --git a/public/app/panels/table/specs/table_model_specs.ts b/public/app/panels/table/specs/table_model_specs.ts deleted file mode 100644 index 04106100392..00000000000 --- a/public/app/panels/table/specs/table_model_specs.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; - -import {TableModel} from '../table_model'; - -describe('when getting tableData', () => { - - describe('timeseries_to_rows', () => { - var panel = { - transform: 'timeseries_to_rows' - }; - - it ('should return 2 rows', () => { - var data = TableModel.transform([ - { - target: 'series1', - datapoints: [[12.12, new Date().getTime()]], - }, - { - target: 'series2', - datapoints: [[12.12, new Date().getTime()]], - } - ], panel); - - expect(data.columns.length).to.be(3); - expect(data.rows.length).to.be(2); - - expect(data.columns[0].text).to.be('Time'); - expect(data.columns[1].text).to.be('Series'); - expect(data.columns[2].text).to.be('Value'); - expect(data.rows[0][1]).to.be('series1'); - expect(data.rows[0][2]).to.be('12.12'); - - expect(data.rows[1][1]).to.be('series2'); - }); - }); - - describe('timeseries_to_rows', () => { - var panel = { - transform: 'timeseries_to_columns' - }; - - it ('should return 3 columns', () => { - var data = TableModel.transform([ - { - target: 'series1', - datapoints: [[12.12, new Date().getTime()]], - }, - { - target: 'series2', - datapoints: [[16.12, new Date().getTime()]], - } - ], panel); - - expect(data.columns.length).to.be(3); - expect(data.rows.length).to.be(1); - - expect(data.columns[0].text).to.be('Time'); - expect(data.columns[1].text).to.be('series1'); - expect(data.columns[2].text).to.be('series2'); - expect(data.rows[0][1]).to.be('12.12'); - expect(data.rows[0][2]).to.be('16.12'); - }); - }); - -}); diff --git a/public/app/panels/table/specs/transformers_specs.ts b/public/app/panels/table/specs/transformers_specs.ts new file mode 100644 index 00000000000..cbe4fd6312b --- /dev/null +++ b/public/app/panels/table/specs/transformers_specs.ts @@ -0,0 +1,72 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {TableModel} from '../table_model'; + +describe('when transforming time series table', () => { + var table; + + describe('given 2 time series', () => { + var time = new Date().getTime(); + var timeSeries = [ + { + target: 'series1', + datapoints: [[12.12, time], [14.44, time+1]], + }, + { + target: 'series2', + datapoints: [[16.12, time]], + } + ]; + + describe('timeseries_to_rows', () => { + var panel = {transform: 'timeseries_to_rows'}; + + beforeEach(() => { + table = TableModel.transform(timeSeries, panel); + }); + + it('should return 3 rows', () => { + expect(table.rows.length).to.be(3); + expect(table.rows[0][1]).to.be('series1'); + expect(table.rows[1][1]).to.be('series1'); + expect(table.rows[2][1]).to.be('series2'); + expect(table.rows[0][2]).to.be('12.12'); + }); + + it('should return 3 rows', () => { + expect(table.columns.length).to.be(3); + expect(table.columns[0].text).to.be('Time'); + expect(table.columns[1].text).to.be('Series'); + expect(table.columns[2].text).to.be('Value'); + }); + }); + + describe('timeseries_to_columns', () => { + var panel = { + transform: 'timeseries_to_columns' + }; + + beforeEach(() => { + table = TableModel.transform(timeSeries, panel); + }); + + it ('should return 3 columns', () => { + expect(table.columns.length).to.be(3); + expect(table.columns[0].text).to.be('Time'); + expect(table.columns[1].text).to.be('series1'); + expect(table.columns[2].text).to.be('series2'); + }); + + it ('should return 2 rows', () => { + expect(table.rows.length).to.be(2); + expect(table.rows[0][1]).to.be('12.12'); + expect(table.rows[0][2]).to.be('16.12'); + }); + + it ('should show - when no value for timestamp', () => { + expect(table.rows[1][2]).to.be('-'); + }); + }); + }); +}); + diff --git a/public/app/panels/table/table_model.ts b/public/app/panels/table/table_model.ts index 9410c36badf..528d250fb99 100644 --- a/public/app/panels/table/table_model.ts +++ b/public/app/panels/table/table_model.ts @@ -1,80 +1,4 @@ -/// - -import moment = require('moment'); -import _ = require('lodash'); - -var transformers = {}; - -transformers['timeseries_to_rows'] = { - description: 'Time series to rows', - transform: function(data, panel, model) { - model.columns = [ - {text: 'Time'}, - {text: 'Series'}, - {text: 'Value'}, - ]; - - model.rows = []; - - for (var i = 0; i < data.length; i++) { - var series = data[i]; - for (var y = 0; y < series.datapoints.length; y++) { - var dp = series.datapoints[y]; - var time = moment(dp[1]).format('LLL'); - var value = dp[0]; - if (value === null) { - value = 'null'; - } else if (_.isNumber(value)) { - value = value.toFixed(2); - } - - model.rows.push([time, series.target, value]); - } - } - }, -}; - -transformers['timeseries_to_columns'] = { - description: 'Time series to columns', - transform: function(data, panel, model) { - model.columns = [{text: 'Time'}]; - model.rows = []; - - var points = {}; - - for (var i = 0; i < data.length; i++) { - var series = data[i]; - model.columns.push({text: series.target}); - - for (var y = 0; y < series.datapoints.length; y++) { - var dp = series.datapoints[y]; - var time = dp[1]; - if (!points[time]) { - points[time] = {}; - points[time][i] = [dp[0]]; - } - else { - points[time][i] = dp[0]; - } - } - } - - for (var time in points) { - var point = points[time]; - var values = [time]; - - for (var i = 0; i < data.length; i++) { - if (point[i] !== undefined) { - values.push(point[i]); - } - } - - model.rows.push(values); - } - } -}; - -export {transformers} +import {transformers} from './transformers'; export class TableModel { columns: any[]; diff --git a/public/app/panels/table/transformers.ts b/public/app/panels/table/transformers.ts new file mode 100644 index 00000000000..7617f03c413 --- /dev/null +++ b/public/app/panels/table/transformers.ts @@ -0,0 +1,84 @@ +/// + +import moment = require('moment'); +import _ = require('lodash'); + +var transformers = {}; + +transformers['timeseries_to_rows'] = { + description: 'Time series to rows', + transform: function(data, panel, model) { + model.columns = [ + {text: 'Time'}, + {text: 'Series'}, + {text: 'Value'}, + ]; + + model.rows = []; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + for (var y = 0; y < series.datapoints.length; y++) { + var dp = series.datapoints[y]; + var time = moment(dp[1]).format('LLL'); + var value = dp[0]; + if (value === null) { + value = 'null'; + } else if (_.isNumber(value)) { + value = value.toFixed(2); + } + + model.rows.push([time, series.target, value]); + } + } + }, +}; + +transformers['timeseries_to_columns'] = { + description: 'Time series to columns', + transform: function(data, panel, model) { + model.columns = [{text: 'Time'}]; + model.rows = []; + + // group by time + var points = {}; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + model.columns.push({text: series.target}); + + for (var y = 0; y < series.datapoints.length; y++) { + var dp = series.datapoints[y]; + var timeKey = dp[1].toString(); + + if (!points[timeKey]) { + points[timeKey] = {time: dp[1]}; + points[timeKey][i] = dp[0]; + } + else { + points[timeKey][i] = dp[0]; + } + } + } + + for (var time in points) { + var point = points[time]; + var values = [moment(point.time).format('LLL')]; + + for (var i = 0; i < data.length; i++) { + var value = point[i]; + if (_.isNumber(value)) { + values.push(value.toFixed(2)); + } else { + values.push('-'); + } + } + + model.rows.push(values); + } + } +}; + +export {transformers} + + From 93b4f3fac8fdda0c48a0a69f35ab90a6e3019a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 4 Nov 2015 12:56:53 +0100 Subject: [PATCH 06/16] feat(tablepanel): minor progress on table panel --- public/app/panels/table/module.html | 2 + public/app/panels/table/module.ts | 69 +++++++++++----- public/app/panels/table/options.html | 19 +++-- public/less/grafana.less | 1 + public/less/pagination.less | 113 +++++++++++++++++++++++++++ public/less/panel_table.less | 12 ++- 6 files changed, 187 insertions(+), 29 deletions(-) create mode 100644 public/less/pagination.less diff --git a/public/app/panels/table/module.html b/public/app/panels/table/module.html index c85252354dc..75d428acf6a 100644 --- a/public/app/panels/table/module.html +++ b/public/app/panels/table/module.html @@ -2,5 +2,7 @@
    +
    diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 6103e11113b..f4da7161f6f 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -15,6 +15,7 @@ export class TablePanelCtrl { constructor($scope, $rootScope, $q, panelSrv, panelHelper) { $scope.ctrl = this; $scope.transformers = transformers; + $scope.pageIndex = 0; $scope.panelMeta = new PanelMeta({ panelName: 'Table', @@ -68,6 +69,7 @@ export function tablePanelDirective() { controller: TablePanelCtrl, link: function(scope, elem) { var data; + var panel = scope.panel; function getTableHeight() { var panelHeight = scope.height || scope.panel.height || scope.row.height; @@ -78,39 +80,67 @@ export function tablePanelDirective() { return (panelHeight - 40) + 'px'; } - function renderPanel() { - var rootDiv = elem.find('.table-panel-container'); - var tableDiv = $('
    '); - var i, y, rowElem, colElem, column, row; - - rowElem = $(''); - for (i = 0; i < data.columns.length; i++) { - column = data.columns[i]; - colElem = $('' + column.text + ''); + function appendTableHeader(tableElem) { + var rowElem = $(''); + for (var i = 0; i < data.columns.length; i++) { + var column = data.columns[i]; + var colElem = $('' + column.text + ''); rowElem.append(colElem); } var headElem = $(''); headElem.append(rowElem); + headElem.appendTo(tableElem); + } + function appendTableRows(tableElem) { var tbodyElem = $(''); - for (y = 0; y < data.rows.length; y++) { - row = data.rows[y]; - rowElem = $(''); - for (i = 0; i < data.columns.length; i++) { - colElem = $('' + row[i] + ''); + var rowEnd = Math.min(panel.pageSize, data.rows.length); + var rowStart = 0; + + for (var y = rowStart; y < rowEnd; y++) { + var row = data.rows[y]; + var rowElem = $(''); + for (var i = 0; i < data.columns.length; i++) { + var colElem = $('' + row[i] + ''); rowElem.append(colElem); } tbodyElem.append(rowElem); } - tableDiv.append(headElem); - tableDiv.append(tbodyElem); + tableElem.append(tbodyElem); + } - rootDiv.css({'max-height': getTableHeight()}); + function appendPaginationControls(footerElem) { + var paginationElem = $('
  • ' + (i+1) + '
  • '); + paginationList.append(pageLinkElem); + } + + var nextLink = $('
  • »
  • '); + paginationList.append(nextLink); + paginationElem.append(paginationList); + + footerElem.empty(); + footerElem.append(paginationElem); + } + + function renderPanel() { + var rootElem = elem.find('.table-panel-container'); + var footerElem = elem.find('.table-panel-footer'); + var tableElem = $('
    '); + + appendTableHeader(tableElem); + appendTableRows(tableElem); + + rootElem.css({'max-height': getTableHeight()}); + rootElem.empty(); + rootElem.append(tableElem); + appendPaginationControls(footerElem); } scope.$on('render', function(event, renderData) { @@ -126,4 +156,3 @@ export function tablePanelDirective() { } angular.module('grafana.directives').directive('grafanaPanelTable', tablePanelDirective); - diff --git a/public/app/panels/table/options.html b/public/app/panels/table/options.html index 717259df40f..8c91cc1ecf9 100644 --- a/public/app/panels/table/options.html +++ b/public/app/panels/table/options.html @@ -21,14 +21,19 @@
    Table Display
    +
    +
      +
    • + Pagination (Page size) +
    • +
    • + +
    • +
    +
    +
    -
  • - Pagination (Page size) -
  • -
  • - -
  • diff --git a/public/less/grafana.less b/public/less/grafana.less index e82c3b93c01..041c0112481 100644 --- a/public/less/grafana.less +++ b/public/less/grafana.less @@ -15,6 +15,7 @@ @import "navbar.less"; @import "gfbox.less"; @import "admin.less"; +@import "pagination.less"; @import "validation.less"; @import "fonts.less"; @import "tabs.less"; diff --git a/public/less/pagination.less b/public/less/pagination.less new file mode 100644 index 00000000000..6e292e5ab37 --- /dev/null +++ b/public/less/pagination.less @@ -0,0 +1,113 @@ +.pagination { +} + +.pagination ul { + display: inline-block; + margin-left: 0; + margin-bottom: 0; + .border-radius(@baseBorderRadius); + .box-shadow(0 1px 2px rgba(0,0,0,.05)); +} +.pagination ul > li { + display: inline; // Remove list-style and block-level defaults +} +.pagination ul > li > a, +.pagination ul > li > span { + float: left; // Collapse white-space + padding: 4px 12px; + line-height: @baseLineHeight; + text-decoration: none; + background-color: @paginationBackground; + border: 1px solid @paginationBorder; + border-left-width: 0; +} +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: @paginationActiveBackground; +} +.pagination ul > .active > a, +.pagination ul > .active > span { + color: @grayLight; + cursor: default; +} +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: @grayLight; + background-color: transparent; + cursor: default; +} +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + .border-left-radius(@baseBorderRadius); +} +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + .border-right-radius(@baseBorderRadius); +} + + +// Alignment +// -------------------------------------------------- + +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} + + +// Sizing +// -------------------------------------------------- + +// Large +.pagination-large { + ul > li > a, + ul > li > span { + padding: @paddingLarge; + font-size: @fontSizeLarge; + } + ul > li:first-child > a, + ul > li:first-child > span { + .border-left-radius(@borderRadiusLarge); + } + ul > li:last-child > a, + ul > li:last-child > span { + .border-right-radius(@borderRadiusLarge); + } +} + +// Small and mini +.pagination-mini, +.pagination-small { + ul > li:first-child > a, + ul > li:first-child > span { + .border-left-radius(@borderRadiusSmall); + } + ul > li:last-child > a, + ul > li:last-child > span { + .border-right-radius(@borderRadiusSmall); + } +} + +// Small +.pagination-small { + ul > li > a, + ul > li > span { + padding: @paddingSmall; + font-size: @fontSizeSmall; + } +} +// Mini +.pagination-mini { + ul > li > a, + ul > li > span { + padding: @paddingMini; + font-size: @fontSizeMini; + } +} diff --git a/public/less/panel_table.less b/public/less/panel_table.less index 8f7dea39f9a..272f5a5c3dc 100644 --- a/public/less/panel_table.less +++ b/public/less/panel_table.less @@ -11,6 +11,13 @@ overflow: auto; } +.table-panel-footer { + text-align: center; + .pagination { + display: inline-block; + } +} + .gf-table-panel* { box-sizing: border-box; } @@ -25,10 +32,11 @@ } .gf-table-panel th { - background: @grafanaTargetFuncBackground; + background: @grafanaListAccent; padding: 5px 0 5px 15px; text-align: left; border-top: 2px solid @bodyBackground; + color: @blue; &:first-child { padding-left: 15px; @@ -36,7 +44,7 @@ } .gf-table-panel td { - padding: 15px 0 15px 15px; + padding: 12px 0 12px 15px; &:first-child { padding-left: 15px; From 7387f2e490ac6b6eadd8bd2487f18d1154c65998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 4 Nov 2015 17:23:16 +0100 Subject: [PATCH 07/16] feat(tablepanel): fixed header, and pagination styling --- public/app/panels/table/module.html | 16 +++++ public/app/panels/table/module.ts | 26 +++---- public/app/panels/table/transformers.ts | 10 ++- public/less/panel_table.less | 91 ++++++++++++++++--------- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/public/app/panels/table/module.html b/public/app/panels/table/module.html index 75d428acf6a..1ceb7f52647 100644 --- a/public/app/panels/table/module.html +++ b/public/app/panels/table/module.html @@ -1,6 +1,22 @@
    +
    +
    + + + + + + + + +
    +
    + {{col.text}} +
    +
    +
    diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index f4da7161f6f..deae2aaed89 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -53,8 +53,8 @@ export class TablePanelCtrl { }; $scope.render = function() { - $scope.tableModel = TableModel.transform($scope.dataRaw, $scope.panel); - panelHelper.broadcastRender($scope, $scope.tableModel); + $scope.table = TableModel.transform($scope.dataRaw, $scope.panel); + panelHelper.broadcastRender($scope, $scope.table); }; panelSrv.init($scope); @@ -93,8 +93,8 @@ export function tablePanelDirective() { headElem.appendTo(tableElem); } - function appendTableRows(tableElem) { - var tbodyElem = $(''); + function appendTableRows(tbodyElem) { + var rowElements = $(document.createDocumentFragment()); var rowEnd = Math.min(panel.pageSize, data.rows.length); var rowStart = 0; @@ -105,14 +105,14 @@ export function tablePanelDirective() { var colElem = $('' + row[i] + ''); rowElem.append(colElem); } - tbodyElem.append(rowElem); + rowElements.append(rowElem); } - tableElem.append(tbodyElem); + tbodyElem.empty(); + tbodyElem.append(rowElements); } function appendPaginationControls(footerElem) { - var paginationElem = $('
+
+
-
-
diff --git a/public/app/panels/table/specs/transformers_specs.ts b/public/app/panels/table/specs/transformers_specs.ts index cbe4fd6312b..ea3eb609976 100644 --- a/public/app/panels/table/specs/transformers_specs.ts +++ b/public/app/panels/table/specs/transformers_specs.ts @@ -30,7 +30,7 @@ describe('when transforming time series table', () => { expect(table.rows[0][1]).to.be('series1'); expect(table.rows[1][1]).to.be('series1'); expect(table.rows[2][1]).to.be('series2'); - expect(table.rows[0][2]).to.be('12.12'); + expect(table.rows[0][2]).to.be(12.12); }); it('should return 3 rows', () => { @@ -59,12 +59,12 @@ describe('when transforming time series table', () => { it ('should return 2 rows', () => { expect(table.rows.length).to.be(2); - expect(table.rows[0][1]).to.be('12.12'); - expect(table.rows[0][2]).to.be('16.12'); + expect(table.rows[0][1]).to.be(12.12); + expect(table.rows[0][2]).to.be(16.12); }); - it ('should show - when no value for timestamp', () => { - expect(table.rows[1][2]).to.be('-'); + it ('should be undefined when no value for timestamp', () => { + expect(table.rows[1][2]).to.be(undefined); }); }); }); From 7d3146ed8d45a8d67a1381821a39cd6e5d2a59ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Nov 2015 08:36:51 +0100 Subject: [PATCH 10/16] feat(tablepanel): fixed header, and pagination styling --- public/app/panels/table/table_model.ts | 5 +++ .../datasource/elasticsearch/datasource.js | 5 +-- .../datasource/elasticsearch/metric_agg.js | 8 +++-- .../elasticsearch/partials/metricAgg.html | 2 +- .../datasource/elasticsearch/query_def.js | 17 +++++----- .../elasticsearch/specs/datasource_specs.ts | 32 +++++++++++++++++++ 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/public/app/panels/table/table_model.ts b/public/app/panels/table/table_model.ts index 528d250fb99..943234bcd89 100644 --- a/public/app/panels/table/table_model.ts +++ b/public/app/panels/table/table_model.ts @@ -4,6 +4,11 @@ export class TableModel { columns: any[]; rows: any[]; + constructor() { + this.columns = []; + this.rows = []; + } + static transform(data, panel) { var model = new TableModel(); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 4e27da73850..f8770064b8e 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -170,7 +170,8 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes target = options.targets[i]; if (target.hide) {return;} - var esQuery = angular.toJson(this.queryBuilder.build(target)); + var queryObj = this.queryBuilder.build(target); + var esQuery = angular.toJson(queryObj); var luceneQuery = angular.toJson(target.query || '*'); // remove inner quotes luceneQuery = luceneQuery.substr(1, luceneQuery.length - 2); @@ -185,7 +186,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); payload = templateSrv.replace(payload, options.scopedVars); - return this._post('/_msearch?search_type=count', payload).then(function(res) { + return this._post('/_msearch', payload).then(function(res) { return new ElasticResponse(sentTargets, res).getTimeSeries(); }); }; diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.js b/public/app/plugins/datasource/elasticsearch/metric_agg.js index fd748a1f8b0..c4e04dad325 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.js +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.js @@ -28,6 +28,7 @@ function (angular, _, queryDef) { $scope.isFirst = $scope.index === 0; $scope.isSingle = metricAggs.length === 1; $scope.settingsLinkText = ''; + $scope.aggDef = _.findWhere($scope.metricAggTypes, {value: $scope.agg.type}); if (!$scope.agg.field) { $scope.agg.field = 'select field'; @@ -53,6 +54,11 @@ function (angular, _, queryDef) { $scope.agg.meta.std_deviation_bounds_lower = true; $scope.agg.meta.std_deviation_bounds_upper = true; } + break; + } + case 'raw_document': { + $scope.target.metrics = [$scope.agg]; + $scope.target.bucketAggs = []; } } }; @@ -65,8 +71,6 @@ function (angular, _, queryDef) { $scope.agg.settings = {}; $scope.agg.meta = {}; $scope.showOptions = false; - - $scope.validateModel(); $scope.onChange(); }; diff --git a/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html b/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html index 7d58be38094..65030af5655 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html @@ -6,7 +6,7 @@
  • -
  • +
  • diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index ca915c68dd4..baab2378e9f 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/public/app/plugins/datasource/elasticsearch/query_def.js @@ -6,14 +6,15 @@ function (_) { return { metricAggTypes: [ - {text: "Count", value: 'count' }, - {text: "Average", value: 'avg' }, - {text: "Sum", value: 'sum' }, - {text: "Max", value: 'max' }, - {text: "Min", value: 'min' }, - {text: "Extended Stats", value: 'extended_stats' }, - {text: "Percentiles", value: 'percentiles' }, - {text: "Unique Count", value: "cardinality" } + {text: "Count", value: 'count', requiresField: false}, + {text: "Average", value: 'avg', requiresField: true}, + {text: "Sum", value: 'sum', requiresField: true}, + {text: "Max", value: 'max', requiresField: true}, + {text: "Min", value: 'min', requiresField: true}, + {text: "Extended Stats", value: 'extended_stats', requiresField: true}, + {text: "Percentiles", value: 'percentiles', requiresField: true}, + {text: "Unique Count", value: "cardinality", requiresField: true}, + {text: "Raw Document", value: "raw_document", requiresField: false} ], bucketAggTypes: [ diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index f087f847a19..4747030ff3d 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -80,4 +80,36 @@ describe('ElasticDatasource', function() { expect(body.query.filtered.query.query_string.query).to.be('escape\\:test'); }); }); + + describe('When issueing document query', function() { + var requestOptions, parts, header; + + beforeEach(function() { + ctx.ds = new ctx.service({url: 'http://es.com', index: 'test', jsonData: {}}); + + ctx.backendSrv.datasourceRequest = function(options) { + requestOptions = options; + return ctx.$q.when({data: {responses: []}}); + }; + + ctx.ds.query({ + range: { from: moment([2015, 4, 30, 10]), to: moment([2015, 5, 1, 10]) }, + targets: [{ bucketAggs: [], metrics: [{type: 'raw_document'}], query: 'test' }] + }); + + ctx.$rootScope.$apply(); + parts = requestOptions.data.split('\n'); + header = angular.fromJson(parts[0]); + }); + + it('should set search type to query_then_fetch', function() { + expect(header.search_type).to.eql('query_then_fetch'); + }); + + it('should set size', function() { + var body = angular.fromJson(parts[1]); + expect(body.query.size).to.be(500); + }); + }); + }); From 4e37290a7f28039251c61b0b3d78df5fda02b2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Nov 2015 09:56:19 +0100 Subject: [PATCH 11/16] feat(tablepanel/elasticsearch): extended elasticsearch data source and query editor to support document queries --- public/app/panels/table/transformers.ts | 20 +++++--- .../datasource/elasticsearch/datasource.js | 18 ++++--- .../elasticsearch/elastic_response.js | 47 ++++++++++++++++--- .../datasource/elasticsearch/query_builder.js | 19 ++++++++ .../elasticsearch/specs/datasource_specs.ts | 2 +- .../specs/elastic_response_specs.ts | 36 ++++++++++++++ .../specs/query_builder_specs.ts | 10 ++++ 7 files changed, 132 insertions(+), 20 deletions(-) diff --git a/public/app/panels/table/transformers.ts b/public/app/panels/table/transformers.ts index afca521c942..42ccae1982e 100644 --- a/public/app/panels/table/transformers.ts +++ b/public/app/panels/table/transformers.ts @@ -14,8 +14,6 @@ transformers['timeseries_to_rows'] = { {text: 'Value'}, ]; - model.rows = []; - for (var i = 0; i < data.length; i++) { var series = data[i]; for (var y = 0; y < series.datapoints.length; y++) { @@ -31,8 +29,7 @@ transformers['timeseries_to_rows'] = { transformers['timeseries_to_columns'] = { description: 'Time series to columns', transform: function(data, panel, model) { - model.columns = [{text: 'Time'}]; - model.rows = []; + model.columns.push({text: 'Time'}); // group by time var points = {}; @@ -75,8 +72,19 @@ transformers['annotations'] = { transformers['json'] = { description: 'JSON', + transform: function(data, panel, model) { + model.columns.push({text: 'JSON'}); + debugger; + + for (var i = 0; i < data.length; i++) { + var series = data[i]; + + for (var y = 0; y < series.datapoints.length; y++) { + var dp = series.datapoints[y]; + model.rows.push([JSON.stringify(dp)]); + } + } + } }; export {transformers} - - diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index f8770064b8e..ce370c639d2 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -153,8 +153,8 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes }); }; - ElasticDatasource.prototype.getQueryHeader = function(timeFrom, timeTo) { - var header = {search_type: "count", "ignore_unavailable": true}; + ElasticDatasource.prototype.getQueryHeader = function(searchType, timeFrom, timeTo) { + var header = {search_type: searchType, "ignore_unavailable": true}; header.index = this.indexPattern.getIndexList(timeFrom, timeTo); return angular.toJson(header); }; @@ -163,8 +163,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var payload = ""; var target; var sentTargets = []; - - var header = this.getQueryHeader(options.range.from, options.range.to); + var headerAdded = false; for (var i = 0; i < options.targets.length; i++) { target = options.targets[i]; @@ -177,7 +176,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes luceneQuery = luceneQuery.substr(1, luceneQuery.length - 2); esQuery = esQuery.replace("$lucene_query", luceneQuery); - payload += header + '\n' + esQuery + '\n'; + if (!headerAdded) { + var searchType = queryObj.size === 0 ? 'count' : 'query_then_fetch'; + var header = this.getQueryHeader(searchType, options.range.from, options.range.to); + payload += header + '\n'; + headerAdded = true; + } + + payload += esQuery + '\n'; sentTargets.push(target); } @@ -230,7 +236,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes ElasticDatasource.prototype.getTerms = function(queryDef) { var range = timeSrv.timeRange(); - var header = this.getQueryHeader(range.from, range.to); + var header = this.getQueryHeader('count', range.from, range.to); var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef)); esQuery = esQuery.replace("$lucene_query", queryDef.query || '*'); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.js b/public/app/plugins/datasource/elasticsearch/elastic_response.js index bfbcb2034ab..aaac2e46ea1 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.js +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.js @@ -173,6 +173,33 @@ function (_, queryDef) { } }; + ElasticResponse.prototype.processHits = function(hits, seriesList) { + var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total}; + var propName, hit, doc, i; + + for (i = 0; i < hits.hits.length; i++) { + hit = hits.hits[i]; + doc = { + _id: hit._id, + _type: hit._type, + _index: hit._index + }; + + if (hit._source) { + for (propName in hit._source) { + doc[propName] = hit._source[propName]; + } + } + + for (propName in hit.fields) { + doc[propName] = hit.fields[propName]; + } + series.datapoints.push(doc); + } + + seriesList.push(series); + }; + ElasticResponse.prototype.getTimeSeries = function() { var seriesList = []; @@ -182,15 +209,21 @@ function (_, queryDef) { throw { message: response.error }; } - var aggregations = response.aggregations; - var target = this.targets[i]; - var tmpSeriesList = []; + if (response.hits) { + this.processHits(response.hits, seriesList); + } - this.processBuckets(aggregations, target, tmpSeriesList, {}); - this.nameSeries(tmpSeriesList, target); + if (response.aggregations) { + var aggregations = response.aggregations; + var target = this.targets[i]; + var tmpSeriesList = []; - for (var y = 0; y < tmpSeriesList.length; y++) { - seriesList.push(tmpSeriesList[y]); + this.processBuckets(aggregations, target, tmpSeriesList, {}); + this.nameSeries(tmpSeriesList, target); + + for (var y = 0; y < tmpSeriesList.length; y++) { + seriesList.push(tmpSeriesList[y]); + } } } diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index d0fbb6a603a..3a956012a86 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -71,6 +71,16 @@ function (angular) { return filterObj; }; + ElasticQueryBuilder.prototype.documentQuery = function(query) { + query.size = 500; + query.sort = {}; + query.sort[this.timeField] = {order: 'desc', unmapped_type: 'boolean'}; + query.fields = ["*", "_source"]; + query.script_fields = {}, + query.fielddata_fields = [this.timeField]; + return query; + }; + ElasticQueryBuilder.prototype.build = function(target) { if (target.rawQuery) { return angular.fromJson(target.rawQuery); @@ -96,6 +106,15 @@ function (angular) { } }; + // handle document query + if (target.bucketAggs.length === 0) { + metric = target.metrics[0]; + if (metric && metric.type !== 'raw_document') { + throw {message: 'Invalid query'}; + } + return this.documentQuery(query, target); + } + nestedAggs = query; for (i = 0; i < target.bucketAggs.length; i++) { diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index 4747030ff3d..1e5d39ba817 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -108,7 +108,7 @@ describe('ElasticDatasource', function() { it('should set size', function() { var body = angular.fromJson(parts[1]); - expect(body.query.size).to.be(500); + expect(body.size).to.be(500); }); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts index df810e3a9d9..5ae67f92d78 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts @@ -411,4 +411,40 @@ describe('ElasticResponse', function() { }); }); + describe('Raw documents query', function() { + beforeEach(function() { + targets = [{ refId: 'A', metrics: [{type: 'raw_document', id: '1'}], bucketAggs: [] }]; + response = { + responses: [{ + hits: { + total: 100, + hits: [ + { + _id: '1', + _type: 'type', + _index: 'index', + _source: {sourceProp: "asd"}, + fields: {fieldProp: "field" }, + }, + { + _source: {sourceProp: "asd2"}, + fields: {fieldProp: "field2" }, + } + ] + } + }] + }; + + result = new ElasticResponse(targets, response).getTimeSeries(); + }); + + it('should return docs', function() { + expect(result.data.length).to.be(1); + expect(result.data[0].type).to.be('docs'); + expect(result.data[0].total).to.be(100); + expect(result.data[0].datapoints.length).to.be(2); + expect(result.data[0].datapoints[0].sourceProp).to.be("asd"); + expect(result.data[0].datapoints[0].fieldProp).to.be("field"); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts index c32b1463ca3..bcae7d6e852 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts @@ -120,4 +120,14 @@ describe('ElasticQueryBuilder', function() { expect(query.aggs["2"].aggs["4"].date_histogram.field).to.be("@timestamp"); }); + it('with raw_document metric', function() { + var query = builder.build({ + metrics: [{type: 'raw_document', id: '1'}], + timeField: '@timestamp', + bucketAggs: [], + }); + + expect(query.size).to.be(500); + }); + }); From 90cca9395110ff84b48b5b5a6287248c31f256c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Nov 2015 13:13:13 +0100 Subject: [PATCH 12/16] feat(tablepanel): lots of work on table panel --- .../app/core/directives/dropdown_typeahead.js | 24 ++- public/app/features/panel/panel_helper.js | 4 +- public/app/panels/table/controller.ts | 43 +---- public/app/panels/table/editor.html | 167 ++++++++++++++++++ public/app/panels/table/editor.ts | 106 +++++++++++ public/app/panels/table/module.ts | 13 +- public/app/panels/table/options.html | 119 +------------ .../panels/table/specs/transformers_specs.ts | 35 ++++ public/app/panels/table/transformers.ts | 36 ++-- 9 files changed, 365 insertions(+), 182 deletions(-) create mode 100644 public/app/panels/table/editor.html create mode 100644 public/app/panels/table/editor.ts diff --git a/public/app/core/directives/dropdown_typeahead.js b/public/app/core/directives/dropdown_typeahead.js index c306b9ff8e5..ad484bb18d7 100644 --- a/public/app/core/directives/dropdown_typeahead.js +++ b/public/app/core/directives/dropdown_typeahead.js @@ -45,16 +45,25 @@ function (_, $, coreModule) { } var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) { - _.each(value.submenu, function(item, subIndex) { - item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; - memo.push(value.text + ' ' + item.text); - }); + if (!value.submenu) { + value.click = 'menuItemSelected(' + index + ')'; + memo.push(value.text); + } else { + _.each(value.submenu, function(item, subIndex) { + item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; + memo.push(value.text + ' ' + item.text); + }); + } return memo; }, []); $scope.menuItemSelected = function(index, subIndex) { - var item = $scope.menuItems[index]; - $scope.dropdownTypeaheadOnSelect({$item: item, $subItem: item.submenu[subIndex]}); + var menuItem = $scope.menuItems[index]; + var payload = {$item: menuItem}; + if (menuItem.submenu && subIndex !== void 0) { + payload.$subItem = menuItem.submenu[subIndex]; + } + $scope.dropdownTypeaheadOnSelect(payload); }; $input.attr('data-provide', 'typeahead'); @@ -65,9 +74,10 @@ function (_, $, coreModule) { updater: function (value) { var result = {}; _.each($scope.menuItems, function(menuItem) { + result.$item = menuItem; + _.each(menuItem.submenu, function(submenuItem) { if (value === (menuItem.text + ' ' + submenuItem.text)) { - result.$item = menuItem; result.$subItem = submenuItem; } }); diff --git a/public/app/features/panel/panel_helper.js b/public/app/features/panel/panel_helper.js index c3ccbfe7e09..52a6727eb2c 100644 --- a/public/app/features/panel/panel_helper.js +++ b/public/app/features/panel/panel_helper.js @@ -32,9 +32,9 @@ function (angular, _, $, kbn, dateMath, rangeUtil) { scope.timing.renderEnd = new Date().getTime(); }; - this.broadcastRender = function(scope, data) { + this.broadcastRender = function(scope, arg1, arg2) { this.setTimeRenderStart(scope); - scope.$broadcast('render', data); + scope.$broadcast('render', arg1, arg2); this.setTimeRenderEnd(scope); if ($rootScope.profilingEnabled) { diff --git a/public/app/panels/table/controller.ts b/public/app/panels/table/controller.ts index 4ae5a1807fa..7c22ba2823f 100644 --- a/public/app/panels/table/controller.ts +++ b/public/app/panels/table/controller.ts @@ -3,24 +3,15 @@ import angular = require('angular'); import _ = require('lodash'); import moment = require('moment'); -import kbn = require('app/core/utils/kbn'); import PanelMeta = require('app/features/panel/panel_meta'); import {TableModel} from './table_model'; -import {transformers} from './transformers'; export class TablePanelCtrl { constructor($scope, $rootScope, $q, panelSrv, panelHelper) { $scope.ctrl = this; - $scope.transformers = transformers; $scope.pageIndex = 0; - $scope.unitFormats = kbn.getUnitFormats(); - $scope.colorModes = { - 'cell': {text: 'Cell'}, - 'value': {text: 'Value'}, - 'row': {text: 'Row'}, - }; $scope.panelMeta = new PanelMeta({ panelName: 'Table', @@ -38,23 +29,18 @@ export class TablePanelCtrl { pageSize: 50, showHeader: true, columns: [], + fields: [] }; $scope.init = function() { _.defaults($scope.panel, panelDefaults); if ($scope.panel.columns.length === 0) { - $scope.addColumnStyle(); } panelSrv.init($scope); }; - $scope.setUnitFormat = function(column, subItem) { - column.unit = subItem.value; - $scope.render(); - }; - $scope.refreshData = function(datasource) { panelHelper.updateTimeRange($scope); @@ -73,32 +59,7 @@ export class TablePanelCtrl { $scope.render = function() { $scope.table = TableModel.transform($scope.dataRaw, $scope.panel); - panelHelper.broadcastRender($scope, $scope.table); - }; - - $scope.getColumnNames = function() { - if (!$scope.table) { - return []; - } - return _.map($scope.table.columns, function(col: any) { - return col.text; - }); - }; - - $scope.addColumnStyle = function() { - var columnStyleDefaults = { - unit: 'short', - decimals: 2, - colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], - pattern: '/.*/', - colorMode: 'value', - }; - - $scope.panel.columns.push(angular.copy(columnStyleDefaults)); - }; - - $scope.removeColumnStyle = function(col) { - $scope.panel.columns = _.without($scope.panel.columns, col); + panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw); }; $scope.init(); diff --git a/public/app/panels/table/editor.html b/public/app/panels/table/editor.html new file mode 100644 index 00000000000..e7c76402974 --- /dev/null +++ b/public/app/panels/table/editor.html @@ -0,0 +1,167 @@ +
    +
    +
    Data
    +
    +
    +
      +
    • + To Table Transform +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Fields +
    • +
    • + + + {{field.name}} + +
    • + +
    +
    +
    +
    +
    + +
    +
    Table Display
    +
    +
    +
      +
    • + Pagination (Page size) +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    + +
    +
    Column Styles
    + +
    +
    +
    +
      +
    • + +
    • +
    • + Name or regex +
    • +
    • + +
    • +
    • + Type +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + Format +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + Coloring +
    • +
    • + +
    • +
    • + ThresholdsComma seperated values +
    • +
    • + +
    • +
    • + Colors +
    • +
    • + + + +
    • +
    • + invert order +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + Unit +
    • + +
    • + Decimals +
    • +
    • + +
    • +
    +
    +
    + +
    +
    + + +
    + diff --git a/public/app/panels/table/editor.ts b/public/app/panels/table/editor.ts new file mode 100644 index 00000000000..4cd4070dd1b --- /dev/null +++ b/public/app/panels/table/editor.ts @@ -0,0 +1,106 @@ + +/// + +import angular = require('angular'); +import $ = require('jquery'); +import _ = require('lodash'); +import kbn = require('app/core/utils/kbn'); +import moment = require('moment'); + +import {transformers} from './transformers'; + +export function tablePanelEditor() { + 'use strict'; + return { + restrict: 'E', + scope: true, + templateUrl: 'app/panels/table/editor.html', + link: function(scope, elem) { + scope.transformers = transformers; + scope.unitFormats = kbn.getUnitFormats(); + scope.colorModes = { + 'cell': {text: 'Cell'}, + 'value': {text: 'Value'}, + 'row': {text: 'Row'}, + }; + scope.columnTypes = { + 'number': {text: 'Number'}, + 'string': {text: 'String'}, + 'date': {text: 'Date'}, + }; + + scope.updateJsonFieldsMenu = function(data) { + scope.jsonFieldsMenu = []; + if (!data || data.length === 0) { + return; + } + + var names = {}; + for (var i = 0; i < data.length; i++) { + var series = data[i]; + if (series.type !== 'docs') { + continue; + } + + for (var y = 0; y < series.datapoints.length; y++) { + var doc = series.datapoints[y]; + for (var propName in doc) { + names[propName] = true; + } + } + } + + _.each(names, function(value, key) { + scope.jsonFieldsMenu.push({text: key}); + }); + }; + + scope.updateJsonFieldsMenu(scope.dataRaw); + + scope.$on('render', function(event, table, rawData) { + scope.updateJsonFieldsMenu(rawData); + }); + + scope.addJsonField = function(menuItem) { + scope.panel.fields.push({name: menuItem.text}); + }; + + scope.removeJsonField = function(field) { + scope.panel.fields = _.without(scope.panel.fields, field); + }; + + scope.setUnitFormat = function(column, subItem) { + column.unit = subItem.value; + scope.render(); + }; + + scope.addColumnStyle = function() { + var columnStyleDefaults = { + unit: 'short', + type: 'number', + decimals: 2, + colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + colorMode: 'value', + pattern: '/.*/', + }; + + scope.panel.columns.push(angular.copy(columnStyleDefaults)); + }; + + scope.removeColumnStyle = function(col) { + scope.panel.columns = _.without(scope.panel.columns, col); + }; + + scope.getColumnNames = function() { + if (!scope.table) { + return []; + } + return _.map(scope.table.columns, function(col: any) { + return col.text; + }); + }; + + } + }; +} + diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 39cd1056290..84955070be4 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -4,10 +4,12 @@ import angular = require('angular'); import $ = require('jquery'); import _ = require('lodash'); import kbn = require('app/core/utils/kbn'); +import moment = require('moment'); import {TablePanelCtrl} from './controller'; +import {tablePanelEditor} from './editor'; -export function tablePanelDirective() { +export function tablePanel() { 'use strict'; return { restrict: 'E', @@ -45,9 +47,13 @@ export function tablePanelDirective() { if (v === null || v === void 0) { return '-'; } - if (_.isString(v) || style) { + if (_.isString(v) || !style) { return v; } + if (style.type === 'date') { + var date = moment(v); + return date.format(style.dateFormat); + } let valueFormater = kbn.valueFormats[style.unit]; return valueFormater(v, style.decimals); }; @@ -136,4 +142,5 @@ export function tablePanelDirective() { }; } -angular.module('grafana.directives').directive('grafanaPanelTable', tablePanelDirective); +angular.module('grafana.directives').directive('grafanaPanelTable', tablePanel); +angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor); diff --git a/public/app/panels/table/options.html b/public/app/panels/table/options.html index c1d4ca658ac..d43ff958c5d 100644 --- a/public/app/panels/table/options.html +++ b/public/app/panels/table/options.html @@ -1,117 +1,2 @@ -
    -
    -
    Data
    -
    -
    -
      -
    • - To Table Transform -
    • -
    • - -
    • -
    -
    -
    -
    -
    - -
    -
    Table Display
    -
    -
    -
      -
    • - Pagination (Page size) -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
    - -
    -
    Column Styles
    - -
    -
    -
    -
      -
    • - -
    • -
    • - Name or regex -
    • -
    • - -
    • -
    • - Unit -
    • - -
    • - Decimals -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - -
    • -
    • - Coloring -
    • -
    • - -
    • -
    • - ThresholdsComma seperated values -
    • -
    • - -
    • -
    • - Colors -
    • -
    • - - - -
    • -
    • - invert order -
    • -
    -
    -
    -
    -
    - - -
    - + + diff --git a/public/app/panels/table/specs/transformers_specs.ts b/public/app/panels/table/specs/transformers_specs.ts index ea3eb609976..af9dd880cf1 100644 --- a/public/app/panels/table/specs/transformers_specs.ts +++ b/public/app/panels/table/specs/transformers_specs.ts @@ -67,6 +67,41 @@ describe('when transforming time series table', () => { expect(table.rows[1][2]).to.be(undefined); }); }); + + describe('JSON Data', () => { + var panel = { + transform: 'json', + fields: [{name: 'timestamp'}, {name: 'message'}] + }; + var rawData = [ + { + type: 'docs', + datapoints: [ + { + timestamp: 'time', + message: 'message' + } + ] + } + ]; + + beforeEach(() => { + table = TableModel.transform(rawData, panel); + }); + + it ('should return 2 columns', () => { + expect(table.columns.length).to.be(2); + expect(table.columns[0].text).to.be('timestamp'); + expect(table.columns[1].text).to.be('message'); + }); + + it ('should return 2 rows', () => { + expect(table.rows.length).to.be(2); + expect(table.rows[0][0]).to.be('time'); + expect(table.rows[0][1]).to.be('message'); + }); + + }); }); }); diff --git a/public/app/panels/table/transformers.ts b/public/app/panels/table/transformers.ts index 42ccae1982e..096ebb73051 100644 --- a/public/app/panels/table/transformers.ts +++ b/public/app/panels/table/transformers.ts @@ -9,7 +9,7 @@ transformers['timeseries_to_rows'] = { description: 'Time series to rows', transform: function(data, panel, model) { model.columns = [ - {text: 'Time'}, + {text: 'Time', type: 'date'}, {text: 'Series'}, {text: 'Value'}, ]; @@ -18,9 +18,7 @@ transformers['timeseries_to_rows'] = { var series = data[i]; for (var y = 0; y < series.datapoints.length; y++) { var dp = series.datapoints[y]; - var time = moment(dp[1]).format('LLL'); - var value = dp[0]; - model.rows.push([time, series.target, value]); + model.rows.push([dp[1], series.target, dp[0]]); } } }, @@ -29,7 +27,7 @@ transformers['timeseries_to_rows'] = { transformers['timeseries_to_columns'] = { description: 'Time series to columns', transform: function(data, panel, model) { - model.columns.push({text: 'Time'}); + model.columns.push({text: 'Time', type: 'date'}); // group by time var points = {}; @@ -54,7 +52,7 @@ transformers['timeseries_to_columns'] = { for (var time in points) { var point = points[time]; - var values = [moment(point.time).format('LLL')]; + var values = [point.time]; for (var i = 0; i < data.length; i++) { var value = point[i]; @@ -71,17 +69,31 @@ transformers['annotations'] = { }; transformers['json'] = { - description: 'JSON', + description: 'JSON Data', transform: function(data, panel, model) { - model.columns.push({text: 'JSON'}); - debugger; + var i, y, z; + for (i = 0; i < panel.fields.length; i++) { + model.columns.push({text: panel.fields[i].name}); + } - for (var i = 0; i < data.length; i++) { + if (model.columns.length === 0) { + model.columns.push({text: 'JSON'}); + } + + for (i = 0; i < data.length; i++) { var series = data[i]; - for (var y = 0; y < series.datapoints.length; y++) { + for (y = 0; y < series.datapoints.length; y++) { var dp = series.datapoints[y]; - model.rows.push([JSON.stringify(dp)]); + var values = []; + for (z = 0; z < panel.fields.length; z++) { + values.push(dp[panel.fields[z].name]); + } + + if (values.length === 0) { + values.push([JSON.stringify(dp)]); + } + model.rows.push(values); } } } From 1b83742e3e9f6fde94e4efa26a940b7a31dd78a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Nov 2015 15:55:42 +0100 Subject: [PATCH 13/16] feat(tablepanel): began refactorin out table row html generation to write unit tests for it --- public/app/panels/table/editor.html | 16 +++------- public/app/panels/table/editor.ts | 2 ++ public/app/panels/table/module.ts | 28 ++++++++--------- public/app/panels/table/renderer.ts | 31 +++++++++++++++++++ .../app/panels/table/specs/renderer_specs.ts | 25 +++++++++++++++ .../panels/table/specs/transformers_specs.ts | 2 +- public/app/panels/table/transformers.ts | 2 +- 7 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 public/app/panels/table/renderer.ts create mode 100644 public/app/panels/table/specs/renderer_specs.ts diff --git a/public/app/panels/table/editor.html b/public/app/panels/table/editor.html index e7c76402974..8540885514d 100644 --- a/public/app/panels/table/editor.html +++ b/public/app/panels/table/editor.html @@ -60,10 +60,13 @@
    -
      -
    • +
        +
      • +
      + +
      • Name or regex
      • @@ -86,9 +89,6 @@
      -
    • - -
    • Format
    • @@ -100,9 +100,6 @@
      -
    • - -
    • Coloring
    • @@ -136,9 +133,6 @@
      -
    • - -
    • Unit
    • diff --git a/public/app/panels/table/editor.ts b/public/app/panels/table/editor.ts index 4cd4070dd1b..ab10cba7f1d 100644 --- a/public/app/panels/table/editor.ts +++ b/public/app/panels/table/editor.ts @@ -63,10 +63,12 @@ export function tablePanelEditor() { scope.addJsonField = function(menuItem) { scope.panel.fields.push({name: menuItem.text}); + scope.render(); }; scope.removeJsonField = function(field) { scope.panel.fields = _.without(scope.panel.fields, field); + scope.render(); }; scope.setUnitFormat = function(column, subItem) { diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 84955070be4..00610ca0e72 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -29,19 +29,6 @@ export function tablePanel() { return (panelHeight - 40) + 'px'; } - function appendTableHeader(tableElem) { - var rowElem = $(''); - for (var i = 0; i < data.columns.length; i++) { - var column = data.columns[i]; - var colElem = $('' + column.text + ''); - rowElem.append(colElem); - } - - var headElem = $(''); - headElem.append(rowElem); - headElem.appendTo(tableElem); - } - function createColumnFormater(style) { return function(v) { if (v === null || v === void 0) { @@ -50,12 +37,23 @@ export function tablePanel() { if (_.isString(v) || !style) { return v; } + if (style.type === 'date') { + if (_.isArray(v)) { v = v[0]; } var date = moment(v); return date.format(style.dateFormat); } - let valueFormater = kbn.valueFormats[style.unit]; - return valueFormater(v, style.decimals); + + if (_.isNumber(v) && style.type === 'number') { + let valueFormater = kbn.valueFormats[style.unit]; + return valueFormater(v, style.decimals); + } + + if (_.isArray(v)) { + v = v.join(', '); + } + + return v; }; } diff --git a/public/app/panels/table/renderer.ts b/public/app/panels/table/renderer.ts new file mode 100644 index 00000000000..c3b09ee45da --- /dev/null +++ b/public/app/panels/table/renderer.ts @@ -0,0 +1,31 @@ + +export class TableRenderer { + constructor(private panel, private table) { + } + + formatColumnValue(columnIndex, value) { + return "value"; + } + + renderCell(columnIndex, value) { + var colValue = this.formatColumnValue(columnIndex, value); + return '' + colValue + ''; + } + + render(page) { + let endPos = Math.min(this.panel.pageSize, this.table.rows.length); + let startPos = 0; + var html = ""; + + for (var y = startPos; y < endPos; y++) { + let row = this.table.rows[y]; + html += ''; + for (var i = 0; i < this.table.columns.length; i++) { + html += this.renderCell(i, row[i]); + } + html += ''; + } + + return html; + } +} diff --git a/public/app/panels/table/specs/renderer_specs.ts b/public/app/panels/table/specs/renderer_specs.ts new file mode 100644 index 00000000000..c4c5ff5d3c3 --- /dev/null +++ b/public/app/panels/table/specs/renderer_specs.ts @@ -0,0 +1,25 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {TableModel} from '../table_model'; +import {TableRenderer} from '../renderer'; + +describe('when rendering table', () => { + describe('given 2 columns', () => { + var table = new TableModel(); + table.columns = [{text: 'Time'}, {text: 'Value'}]; + table.rows.push([1446733230253, 12.4]); + table.rows.push([1446733231253, 10.4]); + + var panel = { + pageSize: 10 + }; + + it('render should return html', () => { + var html = new TableRenderer(panel, table).render(0); + expect(html).to.be('value'); + }); + + }); +}); + + diff --git a/public/app/panels/table/specs/transformers_specs.ts b/public/app/panels/table/specs/transformers_specs.ts index af9dd880cf1..597b8913b82 100644 --- a/public/app/panels/table/specs/transformers_specs.ts +++ b/public/app/panels/table/specs/transformers_specs.ts @@ -96,7 +96,7 @@ describe('when transforming time series table', () => { }); it ('should return 2 rows', () => { - expect(table.rows.length).to.be(2); + expect(table.rows.length).to.be(1); expect(table.rows[0][0]).to.be('time'); expect(table.rows[0][1]).to.be('message'); }); diff --git a/public/app/panels/table/transformers.ts b/public/app/panels/table/transformers.ts index 096ebb73051..dce0098dde3 100644 --- a/public/app/panels/table/transformers.ts +++ b/public/app/panels/table/transformers.ts @@ -91,7 +91,7 @@ transformers['json'] = { } if (values.length === 0) { - values.push([JSON.stringify(dp)]); + values.push(JSON.stringify(dp)); } model.rows.push(values); } From e1433ebb417cc94dc9df3c36c8864a11da8be4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 Nov 2015 12:42:47 -0500 Subject: [PATCH 14/16] feat(tablepanel) more refactoring --- public/app/panels/table/module.ts | 71 +------------------ public/app/panels/table/renderer.ts | 63 +++++++++++++++- .../app/panels/table/specs/renderer_specs.ts | 19 +++-- .../datasource/influxdb/partials/config.html | 1 - 4 files changed, 76 insertions(+), 78 deletions(-) diff --git a/public/app/panels/table/module.ts b/public/app/panels/table/module.ts index 00610ca0e72..beb6273fd62 100644 --- a/public/app/panels/table/module.ts +++ b/public/app/panels/table/module.ts @@ -7,6 +7,7 @@ import kbn = require('app/core/utils/kbn'); import moment = require('moment'); import {TablePanelCtrl} from './controller'; +import {TableRenderer} from './renderer'; import {tablePanelEditor} from './editor'; export function tablePanel() { @@ -29,76 +30,10 @@ export function tablePanel() { return (panelHeight - 40) + 'px'; } - function createColumnFormater(style) { - return function(v) { - if (v === null || v === void 0) { - return '-'; - } - if (_.isString(v) || !style) { - return v; - } - - if (style.type === 'date') { - if (_.isArray(v)) { v = v[0]; } - var date = moment(v); - return date.format(style.dateFormat); - } - - if (_.isNumber(v) && style.type === 'number') { - let valueFormater = kbn.valueFormats[style.unit]; - return valueFormater(v, style.decimals); - } - - if (_.isArray(v)) { - v = v.join(', '); - } - - return v; - }; - } - - function formatColumnValue(colIndex, value) { - if (formaters[colIndex]) { - return formaters[colIndex](value); - } - - for (let i = 0; i < panel.columns.length; i++) { - let style = panel.columns[i]; - let column = data.columns[colIndex]; - var regex = kbn.stringToJsRegex(style.pattern); - if (column.text.match(regex)) { - formaters[colIndex] = createColumnFormater(style); - return formaters[colIndex](value); - } - } - - formaters[colIndex] = function(v) { - return v; - }; - - return formaters[colIndex](value); - } - function appendTableRows(tbodyElem) { - let rowElements = $(document.createDocumentFragment()); - let rowEnd = Math.min(panel.pageSize, data.rows.length); - let rowStart = 0; - // reset formater cache - formaters = []; - - for (var y = rowStart; y < rowEnd; y++) { - let row = data.rows[y]; - let rowElem = $(''); - for (var i = 0; i < data.columns.length; i++) { - var colValue = formatColumnValue(i, row[i]); - let colElem = $(' ' + colValue + ''); - rowElem.append(colElem); - } - rowElements.append(rowElem); - } - + var renderer = new TableRenderer(panel, data, scope.dashboard.timezone); tbodyElem.empty(); - tbodyElem.append(rowElements); + tbodyElem.html(renderer.render(0)); } function appendPaginationControls(footerElem) { diff --git a/public/app/panels/table/renderer.ts b/public/app/panels/table/renderer.ts index c3b09ee45da..8672629a23b 100644 --- a/public/app/panels/table/renderer.ts +++ b/public/app/panels/table/renderer.ts @@ -1,10 +1,67 @@ +/// + +import _ = require('lodash'); +import kbn = require('app/core/utils/kbn'); +import moment = require('moment'); export class TableRenderer { - constructor(private panel, private table) { + formaters: any[]; + + constructor(private panel, private table, private timezone) { + this.formaters = []; } - formatColumnValue(columnIndex, value) { - return "value"; + createColumnFormater(style) { + return (v) => { + if (v === null || v === void 0) { + return '-'; + } + if (_.isString(v) || !style) { + return v; + } + + if (style.type === 'date') { + if (_.isArray(v)) { v = v[0]; } + var date = moment(v); + if (this.timezone === 'utc') { + date = date.utc(); + } + return date.format(style.dateFormat); + } + + if (_.isNumber(v) && style.type === 'number') { + let valueFormater = kbn.valueFormats[style.unit]; + return valueFormater(v, style.decimals); + } + + if (_.isArray(v)) { + v = v.join(', '); + } + + return v; + }; + } + + formatColumnValue(colIndex, value) { + if (this.formaters[colIndex]) { + return this.formaters[colIndex](value); + } + + for (let i = 0; i < this.panel.columns.length; i++) { + let style = this.panel.columns[i]; + let column = this.table.columns[colIndex]; + var regex = kbn.stringToJsRegex(style.pattern); + if (column.text.match(regex)) { + this.formaters[colIndex] = this.createColumnFormater(style); + return this.formaters[colIndex](value); + } + } + + this.formaters[colIndex] = function(v) { + return v; + }; + + return this.formaters[colIndex](value); } renderCell(columnIndex, value) { diff --git a/public/app/panels/table/specs/renderer_specs.ts b/public/app/panels/table/specs/renderer_specs.ts index c4c5ff5d3c3..8bf6dd308d0 100644 --- a/public/app/panels/table/specs/renderer_specs.ts +++ b/public/app/panels/table/specs/renderer_specs.ts @@ -7,16 +7,23 @@ describe('when rendering table', () => { describe('given 2 columns', () => { var table = new TableModel(); table.columns = [{text: 'Time'}, {text: 'Value'}]; - table.rows.push([1446733230253, 12.4]); - table.rows.push([1446733231253, 10.4]); var panel = { - pageSize: 10 + pageSize: 10, + columns: [ + { + pattern: 'Time', + type: 'date', + format: 'LLL' + } + ] }; - it('render should return html', () => { - var html = new TableRenderer(panel, table).render(0); - expect(html).to.be('value'); + var renderer = new TableRenderer(panel, table, 'utc'); + + it('time column should be formated', () => { + var html = renderer.renderCell(0, 1388556366666); + expect(html).to.be('2014-01-01T06:06:06+00:00'); }); }); diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 66c39fe7b69..4d51a33e6b2 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -1,4 +1,3 @@ -

      From b8e6fcfeaebad99da576e08e5d90214c428ef6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 6 Nov 2015 13:16:17 +0100 Subject: [PATCH 15/16] feat(tablepanel): worked on cell / value threshold coloring --- public/app/panels/table/editor.html | 15 ++--- public/app/panels/table/editor.ts | 24 +++---- public/app/panels/table/renderer.ts | 65 +++++++++++++++---- .../app/panels/table/specs/renderer_specs.ts | 30 ++++++++- 4 files changed, 99 insertions(+), 35 deletions(-) diff --git a/public/app/panels/table/editor.html b/public/app/panels/table/editor.html index 8540885514d..72bc1cf8a1e 100644 --- a/public/app/panels/table/editor.html +++ b/public/app/panels/table/editor.html @@ -79,21 +79,18 @@
    -
    -
    -
    -
      -
    • +
        +
      • Format
      • - +
      @@ -106,7 +103,7 @@
    • @@ -115,7 +112,7 @@ ThresholdsComma seperated values
    • - +
    • Colors diff --git a/public/app/panels/table/editor.ts b/public/app/panels/table/editor.ts index ab10cba7f1d..8aae1840629 100644 --- a/public/app/panels/table/editor.ts +++ b/public/app/panels/table/editor.ts @@ -18,16 +18,17 @@ export function tablePanelEditor() { link: function(scope, elem) { scope.transformers = transformers; scope.unitFormats = kbn.getUnitFormats(); - scope.colorModes = { - 'cell': {text: 'Cell'}, - 'value': {text: 'Value'}, - 'row': {text: 'Row'}, - }; - scope.columnTypes = { - 'number': {text: 'Number'}, - 'string': {text: 'String'}, - 'date': {text: 'Date'}, - }; + scope.colorModes = [ + {text: 'Disabled', value: null}, + {text: 'Cell', value: 'cell'}, + {text: 'Value', value: 'value'}, + {text: 'Row', value: 'row'}, + ]; + scope.columnTypes = [ + {text: 'Number', value: 'number'}, + {text: 'String', value: 'string'}, + {text: 'Date', value: 'date'}, + ]; scope.updateJsonFieldsMenu = function(data) { scope.jsonFieldsMenu = []; @@ -82,8 +83,9 @@ export function tablePanelEditor() { type: 'number', decimals: 2, colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], - colorMode: 'value', + colorMode: null, pattern: '/.*/', + thresholds: [], }; scope.panel.columns.push(angular.copy(columnStyleDefaults)); diff --git a/public/app/panels/table/renderer.ts b/public/app/panels/table/renderer.ts index 8672629a23b..a2c86d940b9 100644 --- a/public/app/panels/table/renderer.ts +++ b/public/app/panels/table/renderer.ts @@ -6,32 +6,59 @@ import moment = require('moment'); export class TableRenderer { formaters: any[]; + colorState: any; constructor(private panel, private table, private timezone) { this.formaters = []; + this.colorState = {}; + } + + getColorForValue(value, style) { + if (!style.thresholds) { return null; } + + for (var i = style.thresholds.length - 1; i >= 0 ; i--) { + if (value >= style.thresholds[i]) { + return style.colors[i]; + } + } + return null; } createColumnFormater(style) { - return (v) => { - if (v === null || v === void 0) { - return '-'; - } - if (_.isString(v) || !style) { - return v; - } + if (!style) { + return v => v; + } - if (style.type === 'date') { + if (style.type === 'date') { + return v => { if (_.isArray(v)) { v = v[0]; } var date = moment(v); if (this.timezone === 'utc') { date = date.utc(); } return date.format(style.dateFormat); - } + }; + } - if (_.isNumber(v) && style.type === 'number') { - let valueFormater = kbn.valueFormats[style.unit]; - return valueFormater(v, style.decimals); + if (style.type === 'number') { + let valueFormater = kbn.valueFormats[style.unit]; + + return v => { + if (v === null || v === void 0) { + return '-'; + } + + if (style.colorMode) { + this.colorState[style.colorMode] = this.getColorForValue(v, style); + } + + return valueFormater(v, style.decimals, null); + }; + } + + return v => { + if (v === null || v === void 0) { + return '-'; } if (_.isArray(v)) { @@ -65,8 +92,18 @@ export class TableRenderer { } renderCell(columnIndex, value) { - var colValue = this.formatColumnValue(columnIndex, value); - return '' + colValue + ''; + var value = this.formatColumnValue(columnIndex, value); + var style = ''; + if (this.colorState.cell) { + style = ' style="background-color:' + this.colorState.cell + ';color: white"'; + this.colorState.cell = null; + } + else if (this.colorState.value) { + style = ' style="color:' + this.colorState.value + '"'; + this.colorState.value = null; + } + + return '' + value + ''; } render(page) { diff --git a/public/app/panels/table/specs/renderer_specs.ts b/public/app/panels/table/specs/renderer_specs.ts index 8bf6dd308d0..08f63dfe799 100644 --- a/public/app/panels/table/specs/renderer_specs.ts +++ b/public/app/panels/table/specs/renderer_specs.ts @@ -6,7 +6,11 @@ import {TableRenderer} from '../renderer'; describe('when rendering table', () => { describe('given 2 columns', () => { var table = new TableModel(); - table.columns = [{text: 'Time'}, {text: 'Value'}]; + table.columns = [ + {text: 'Time'}, + {text: 'Value'}, + {text: 'Colored'} + ]; var panel = { pageSize: 10, @@ -15,6 +19,21 @@ describe('when rendering table', () => { pattern: 'Time', type: 'date', format: 'LLL' + }, + { + pattern: 'Value', + type: 'number', + unit: 'ms', + decimals: 3, + }, + { + pattern: 'Colored', + type: 'number', + unit: 'none', + decimals: 1, + colorMode: 'value', + thresholds: [0, 50, 80], + colors: ['green', 'orange', 'red'] } ] }; @@ -26,6 +45,15 @@ describe('when rendering table', () => { expect(html).to.be('2014-01-01T06:06:06+00:00'); }); + it('number column should be formated', () => { + var html = renderer.renderCell(1, 1230); + expect(html).to.be('1.230 s'); + }); + + it('colored cell should have style', () => { + var html = renderer.renderCell(2, 55); + expect(html).to.be('55.0'); + }); }); }); From 5d166dc8cb52e2c63e7fb342c9b7a06cfbc06db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Nov 2015 09:46:49 +0100 Subject: [PATCH 16/16] feat(tablepanel): added new renderer spec --- public/app/panels/table/renderer.ts | 4 ++++ public/app/panels/table/specs/renderer_specs.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/public/app/panels/table/renderer.ts b/public/app/panels/table/renderer.ts index a2c86d940b9..acfef961f07 100644 --- a/public/app/panels/table/renderer.ts +++ b/public/app/panels/table/renderer.ts @@ -48,6 +48,10 @@ export class TableRenderer { return '-'; } + if (_.isString(v)) { + return v; + } + if (style.colorMode) { this.colorState[style.colorMode] = this.getColorForValue(v, style); } diff --git a/public/app/panels/table/specs/renderer_specs.ts b/public/app/panels/table/specs/renderer_specs.ts index 08f63dfe799..fad4f993acf 100644 --- a/public/app/panels/table/specs/renderer_specs.ts +++ b/public/app/panels/table/specs/renderer_specs.ts @@ -50,6 +50,11 @@ describe('when rendering table', () => { expect(html).to.be('1.230 s'); }); + it('number style should ignore string values', () => { + var html = renderer.renderCell(1, 'asd'); + expect(html).to.be('asd'); + }); + it('colored cell should have style', () => { var html = renderer.renderCell(2, 55); expect(html).to.be('55.0');