Explorar el Código

Merge branch 'tablepanel2' into develop

Torkel Ödegaard hace 10 años
padre
commit
673ae1edc0
Se han modificado 34 ficheros con 1319 adiciones y 78 borrados
  1. 17 7
      public/app/core/directives/dropdown_typeahead.js
  2. 1 0
      public/app/core/settings.js
  3. 52 33
      public/app/core/time_series.ts
  4. 1 1
      public/app/core/utils/kbn.js
  5. 2 2
      public/app/features/panel/panel_helper.js
  6. 0 4
      public/app/panels/graph/module.html
  7. 1 1
      public/app/panels/graph/styleEditor.html
  8. 68 0
      public/app/panels/table/controller.ts
  9. 158 0
      public/app/panels/table/editor.html
  10. 110 0
      public/app/panels/table/editor.ts
  11. 24 0
      public/app/panels/table/module.html
  12. 79 0
      public/app/panels/table/module.ts
  13. 2 0
      public/app/panels/table/options.html
  14. 129 0
      public/app/panels/table/renderer.ts
  15. 65 0
      public/app/panels/table/specs/renderer_specs.ts
  16. 107 0
      public/app/panels/table/specs/transformers_specs.ts
  17. 27 0
      public/app/panels/table/table_model.ts
  18. 102 0
      public/app/panels/table/transformers.ts
  19. 15 8
      public/app/plugins/datasource/elasticsearch/datasource.js
  20. 40 7
      public/app/plugins/datasource/elasticsearch/elastic_response.js
  21. 6 2
      public/app/plugins/datasource/elasticsearch/metric_agg.js
  22. 1 1
      public/app/plugins/datasource/elasticsearch/partials/metricAgg.html
  23. 19 0
      public/app/plugins/datasource/elasticsearch/query_builder.js
  24. 9 8
      public/app/plugins/datasource/elasticsearch/query_def.js
  25. 32 0
      public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts
  26. 36 0
      public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts
  27. 10 0
      public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts
  28. 0 1
      public/app/plugins/datasource/influxdb/partials/config.html
  29. 5 3
      public/less/grafana.less
  30. 113 0
      public/less/pagination.less
  31. 0 0
      public/less/panel_dashlist.less
  32. 0 0
      public/less/panel_graph.less
  33. 0 0
      public/less/panel_singlestat.less
  34. 88 0
      public/less/panel_table.less

+ 17 - 7
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;
                 }
               });

+ 1 - 0
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' },

+ 52 - 33
public/app/core/time_series.js → public/app/core/time_series.ts

@@ -1,11 +1,46 @@
-define([
-  'lodash',
-  'app/core/utils/kbn'
-],
-function (_, kbn) {
-  'use strict';
-
-  function TimeSeries(opts) {
+///<reference path="../headers/common.d.ts" />
+
+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;

+ 1 - 1
public/app/core/utils/kbn.js

@@ -192,7 +192,7 @@ function($, _) {
 
   kbn.stringToJsRegex = function(str) {
     if (str[0] !== '/') {
-      return new RegExp(str);
+      return new RegExp('^' + str + '$');
     }
 
     var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));

+ 2 - 2
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) {

+ 0 - 4
public/app/panels/graph/module.html

@@ -3,10 +3,6 @@
 	<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
 		<div class="graph-canvas-wrapper">
 
-			<!-- <span class="graph&#45;time&#45;info" ng&#45;if="panelMeta.timeInfo"> -->
-			<!-- 	<i class="fa fa&#45;clock&#45;o"></i> {{panelMeta.timeInfo}} -->
-		  <!-- </span> -->
-
 			<div ng-if="datapointsWarning" class="datapoints-warning">
 				<span class="small" ng-show="!datapointsCount">
 					No datapoints <tip>No datapoints returned from metric query</tip>

+ 1 - 1
public/app/panels/graph/styleEditor.html

@@ -63,7 +63,7 @@
 <div class="editor-row">
   <div class="section">
 		<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
-		<div>
+		<div class="tight-form-container">
 			<div class="tight-form" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
 				<ul class="tight-form-list">
 					<li class="tight-form-item">

+ 68 - 0
public/app/panels/table/controller.ts

@@ -0,0 +1,68 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular = require('angular');
+import _ = require('lodash');
+import moment = require('moment');
+import PanelMeta = require('app/features/panel/panel_meta');
+
+import {TableModel} from './table_model';
+
+export class TablePanelCtrl {
+
+  constructor($scope, $rootScope, $q, panelSrv, panelHelper) {
+    $scope.ctrl = this;
+    $scope.pageIndex = 0;
+
+    $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');
+
+    var panelDefaults = {
+      targets: [{}],
+      transform: 'timeseries_to_rows',
+      pageSize: 50,
+      showHeader: true,
+      columns: [],
+      fields: []
+    };
+
+    $scope.init = function() {
+      _.defaults($scope.panel, panelDefaults);
+
+      if ($scope.panel.columns.length === 0) {
+      }
+
+      panelSrv.init($scope);
+    };
+
+    $scope.refreshData = function(datasource) {
+      panelHelper.updateTimeRange($scope);
+
+      return panelHelper.issueMetricQuery($scope, datasource)
+      .then($scope.dataHandler, function(err) {
+        $scope.seriesList = [];
+        $scope.render([]);
+        throw err;
+      });
+    };
+
+    $scope.dataHandler = function(results) {
+      $scope.dataRaw = results.data;
+      $scope.render();
+    };
+
+    $scope.render = function() {
+      $scope.table = TableModel.transform($scope.dataRaw, $scope.panel);
+      panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
+    };
+
+    $scope.init();
+  }
+}
+

+ 158 - 0
public/app/panels/table/editor.html

@@ -0,0 +1,158 @@
+<div class="editor-row">
+	<div class="section">
+		<h5>Data</h5>
+		<div class="tight-form-container">
+			<div class="tight-form">
+				<ul class="tight-form-list">
+					<li class="tight-form-item" style="width: 140px">
+						To Table Transform
+					</li>
+					<li>
+						<select class="input-large tight-form-input"
+							ng-model="panel.transform"
+							ng-options="k as v.description for (k, v) in transformers"
+							ng-change="render()"></select>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+			<div class="tight-form" ng-if="panel.transform === 'json'">
+				<ul class="tight-form-list">
+					<li class="tight-form-item" style="width: 140px">
+						Fields
+					</li>
+					<li class="tight-form-item" ng-repeat="field in panel.fields">
+						<i class="pointer fa fa-remove" ng-click="removeJsonField(field)"></i>
+						<span>
+							{{field.name}}
+						</span>
+					</li>
+					<li class="dropdown" dropdown-typeahead="jsonFieldsMenu" dropdown-typeahead-on-select="addJsonField($item, $subItem)">
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+		</div>
+	</div>
+
+	<div class="section">
+		<h5>Table Display</h5>
+		<div class="tight-form-container">
+			<div class="tight-form">
+				<ul class="tight-form-list">
+					<li class="tight-form-item">
+						Pagination (Page size)
+					</li>
+					<li>
+						<input type="text" class="input-small tight-form-input" placeholder="50"
+						empty-to-null ng-model="panel.pageSize" ng-change="render()" ng-model-onblur>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="editor-row" style="margin-top: 20px">
+	<h5>Column Styles</h5>
+
+	<div class="tight-form-container">
+		<div ng-repeat="column in panel.columns">
+			<div class="tight-form">
+				<ul class="tight-form-list pull-right">
+					<li class="tight-form-item last">
+						<i class="fa fa-remove pointer" ng-click="removeColumnStyle(column)"></i>
+					</li>
+				</ul>
+
+				<ul class="tight-form-list">
+					<li class="tight-form-item">
+						Name or regex
+					</li>
+					<li>
+						<input type="text" ng-model="column.pattern" bs-typeahead="getColumnNames" ng-blur="render()" data-min-length=0 data-items=100 class="input-medium tight-form-input">
+					</li>
+					<li class="tight-form-item" style="width: 86px">
+						Type
+					</li>
+					<li>
+						<select class="input-small tight-form-input"
+							ng-model="column.type"
+							ng-options="c.value as c.text for c in columnTypes"
+							ng-change="render()"
+							style="width: 150px"
+							></select>
+					</li>
+				</ul>
+				<ul class="tight-form-list" ng-if="column.type === 'date'">
+					<li class="tight-form-item">
+						Format
+					</li>
+					<li>
+						<input type="text" class="input-large tight-form-input" ng-model="column.dateFormat" ng-change="render()" ng-model-onblur>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+			<div class="tight-form" ng-if="column.type === 'number'">
+				<ul class="tight-form-list">
+					<li class="tight-form-item text-right" style="width: 93px">
+						Coloring
+					</li>
+					<li>
+						<select class="input-small tight-form-input"
+							ng-model="column.colorMode"
+							ng-options="c.value as c.text for c in colorModes"
+							ng-change="render()"
+							style="width: 150px"
+							></select>
+					</li>
+					<li class="tight-form-item">
+						Thresholds<tip>Comma seperated values</tip>
+					</li>
+					<li>
+						<input type="text" class="input-small tight-form-input" style="width: 150px" ng-model="column.thresholds" ng-blur="render()" placeholder="0,50,80" array-join></input>
+					</li>
+					<li class="tight-form-item" style="width: 60px">
+						Colors
+					</li>
+					<li class="tight-form-item">
+						<spectrum-picker ng-model="column.colors[0]" ng-change="render()" ></spectrum-picker>
+						<spectrum-picker ng-model="column.colors[1]" ng-change="render()" ></spectrum-picker>
+						<spectrum-picker ng-model="column.colors[2]" ng-change="render()" ></spectrum-picker>
+					</li>
+					<li class="tight-form-item last">
+						<a class="pointer" ng-click="invertColorOrder()">invert order</a>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+			<div class="tight-form" ng-if="column.type === 'number'">
+				<ul class="tight-form-list">
+					<li class="tight-form-item text-right" style="width: 93px">
+						Unit
+					</li>
+					<li class="dropdown" style="width: 150px"
+						ng-model="column.unit"
+						dropdown-typeahead="unitFormats"
+						dropdown-typeahead-on-select="setUnitFormat(column, $subItem)">
+					</li>
+					<li class="tight-form-item" style="width: 86px">
+						Decimals
+					</li>
+					<li style="width: 105px">
+						<input type="number" class="input-mini tight-form-input" ng-model="column.decimals" ng-change="render()" ng-model-onblur>
+					</li>
+				</ul>
+				<div class="clearfix"></div>
+			</div>
+
+		</div>
+	</div>
+
+	<button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()">
+		Add column display rule
+	</button>
+</div>
+

+ 110 - 0
public/app/panels/table/editor.ts

@@ -0,0 +1,110 @@
+
+///<reference path="../../headers/common.d.ts" />
+
+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 = [
+        {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 = [];
+        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.render();
+      };
+
+      scope.removeJsonField = function(field) {
+        scope.panel.fields = _.without(scope.panel.fields, field);
+        scope.render();
+      };
+
+      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: null,
+          pattern: '/.*/',
+          thresholds: [],
+        };
+
+        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;
+        });
+      };
+
+    }
+  };
+}
+

+ 24 - 0
public/app/panels/table/module.html

@@ -0,0 +1,24 @@
+<div class="table-panel-wrapper">
+	<grafana-panel>
+		<div class="table-panel-container">
+			<div class="table-panel-header-bg"></div>
+			<div class="table-panel-scroll">
+				<table class="table-panel-table">
+					<thead>
+						<tr>
+							<th ng-repeat="col in table.columns">
+								<div class="table-panel-table-header-inner">
+									{{col.text}}
+								</div>
+							</th>
+						</tr>
+					</thead>
+					<tbody>
+					</tbody>
+				</table>
+			</div>
+		</div>
+		<div class="table-panel-footer">
+		</div>
+	</grafana-panel>
+</div>

+ 79 - 0
public/app/panels/table/module.ts

@@ -0,0 +1,79 @@
+///<reference path="../../headers/common.d.ts" />
+
+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 {TableRenderer} from './renderer';
+import {tablePanelEditor} from './editor';
+
+export function tablePanel() {
+  'use strict';
+  return {
+    restrict: 'E',
+    templateUrl: 'app/panels/table/module.html',
+    controller: TablePanelCtrl,
+    link: function(scope, elem) {
+      var data;
+      var panel = scope.panel;
+      var formaters = [];
+
+      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 appendTableRows(tbodyElem) {
+        var renderer = new TableRenderer(panel, data, scope.dashboard.timezone);
+        tbodyElem.empty();
+        tbodyElem.html(renderer.render(0));
+      }
+
+      function appendPaginationControls(footerElem) {
+        var paginationList = $('<ul></ul>');
+
+        var pageCount = data.rows.length / panel.pageSize;
+        for (var i = 0; i < pageCount; i++) {
+          var pageLinkElem = $('<li><a href="#">' + (i+1) + '</a></li>');
+          paginationList.append(pageLinkElem);
+        }
+
+        var nextLink = $('<li><a href="#">»</a></li>');
+        paginationList.append(nextLink);
+
+        footerElem.empty();
+        footerElem.append(paginationList);
+      }
+
+      function renderPanel() {
+        var rootElem = elem.find('.table-panel-scroll');
+        var tbodyElem = elem.find('tbody');
+        var footerElem = elem.find('.table-panel-footer');
+
+        appendTableRows(tbodyElem);
+
+        rootElem.css({'max-height': getTableHeight()});
+        appendPaginationControls(footerElem);
+      }
+
+      scope.$on('render', function(event, renderData) {
+        data = renderData || data;
+        if (!data) {
+          scope.get_data();
+          return;
+        }
+        renderPanel();
+      });
+    }
+  };
+}
+
+angular.module('grafana.directives').directive('grafanaPanelTable', tablePanel);
+angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);

+ 2 - 0
public/app/panels/table/options.html

@@ -0,0 +1,2 @@
+<grafana-panel-table-editor>
+</grafana-panel-table-editor>

+ 129 - 0
public/app/panels/table/renderer.ts

@@ -0,0 +1,129 @@
+///<reference path="../../headers/common.d.ts" />
+
+import _ = require('lodash');
+import kbn = require('app/core/utils/kbn');
+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) {
+    if (!style) {
+      return v => v;
+    }
+
+    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 (style.type === 'number') {
+      let valueFormater = kbn.valueFormats[style.unit];
+
+      return v =>  {
+        if (v === null || v === void 0) {
+          return '-';
+        }
+
+        if (_.isString(v)) {
+          return v;
+        }
+
+        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)) {
+        v = v.join(',&nbsp;');
+      }
+
+      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) {
+    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 '<td' + style + '>' + value + '</td>';
+  }
+
+  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 += '<tr>';
+      for (var i = 0; i < this.table.columns.length; i++) {
+        html += this.renderCell(i, row[i]);
+      }
+      html += '</tr>';
+    }
+
+    return html;
+  }
+}

+ 65 - 0
public/app/panels/table/specs/renderer_specs.ts

@@ -0,0 +1,65 @@
+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'},
+      {text: 'Colored'}
+    ];
+
+    var panel = {
+      pageSize: 10,
+      columns: [
+        {
+          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']
+        }
+      ]
+    };
+
+    var renderer = new TableRenderer(panel, table, 'utc');
+
+    it('time column should be formated', () => {
+      var html = renderer.renderCell(0, 1388556366666);
+      expect(html).to.be('<td>2014-01-01T06:06:06+00:00</td>');
+    });
+
+    it('number column should be formated', () => {
+      var html = renderer.renderCell(1, 1230);
+      expect(html).to.be('<td>1.230 s</td>');
+    });
+
+    it('number style should ignore string values', () => {
+      var html = renderer.renderCell(1, 'asd');
+      expect(html).to.be('<td>asd</td>');
+    });
+
+    it('colored cell should have style', () => {
+      var html = renderer.renderCell(2, 55);
+      expect(html).to.be('<td style="color:orange">55.0</td>');
+    });
+  });
+});
+
+

+ 107 - 0
public/app/panels/table/specs/transformers_specs.ts

@@ -0,0 +1,107 @@
+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 be undefined when no value for timestamp', () => {
+        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(1);
+        expect(table.rows[0][0]).to.be('time');
+        expect(table.rows[0][1]).to.be('message');
+      });
+
+    });
+  });
+});
+

+ 27 - 0
public/app/panels/table/table_model.ts

@@ -0,0 +1,27 @@
+import {transformers} from './transformers';
+
+export class TableModel {
+  columns: any[];
+  rows: any[];
+
+  constructor() {
+    this.columns = [];
+    this.rows = [];
+  }
+
+  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;
+  }
+}

+ 102 - 0
public/app/panels/table/transformers.ts

@@ -0,0 +1,102 @@
+///<reference path="../../headers/common.d.ts" />
+
+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', type: 'date'},
+      {text: 'Series'},
+      {text: 'Value'},
+    ];
+
+    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([dp[1], series.target, dp[0]]);
+      }
+    }
+  },
+};
+
+transformers['timeseries_to_columns'] = {
+  description: 'Time series to columns',
+  transform: function(data, panel, model) {
+    model.columns.push({text: 'Time', type: 'date'});
+
+    // 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 = [point.time];
+
+      for (var i = 0; i < data.length; i++) {
+        var value = point[i];
+        values.push(value);
+      }
+
+      model.rows.push(values);
+    }
+  }
+};
+
+transformers['annotations'] = {
+  description: 'Annotations',
+};
+
+transformers['json'] = {
+  description: 'JSON Data',
+  transform: function(data, panel, model) {
+    var i, y, z;
+    for (i = 0; i < panel.fields.length; i++) {
+      model.columns.push({text: panel.fields[i].name});
+    }
+
+    if (model.columns.length === 0) {
+      model.columns.push({text: 'JSON'});
+    }
+
+    for (i = 0; i < data.length; i++) {
+      var series = data[i];
+
+      for (y = 0; y < series.datapoints.length; y++) {
+        var dp = series.datapoints[y];
+        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);
+      }
+    }
+  }
+};
+
+export {transformers}

+ 15 - 8
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,20 +163,27 @@ 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];
         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);
         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);
       }
 
@@ -185,7 +192,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();
       });
     };
@@ -229,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 || '*');

+ 40 - 7
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]);
+        }
       }
     }
 

+ 6 - 2
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();
     };
 

+ 1 - 1
public/app/plugins/datasource/elasticsearch/partials/metricAgg.html

@@ -6,7 +6,7 @@
 		<li>
 			<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
 		</li>
-		<li ng-if="agg.type !== 'count'">
+		<li ng-if="aggDef.requiresField">
 			<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
 		</li>
 		<li class="tight-form-item last" ng-if="settingsLinkText">

+ 19 - 0
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++) {

+ 9 - 8
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: [

+ 32 - 0
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.size).to.be(500);
+    });
+  });
+
 });

+ 36 - 0
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");
+    });
+  });
 });

+ 10 - 0
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);
+  });
+
 });

+ 0 - 1
public/app/plugins/datasource/influxdb/partials/config.html

@@ -1,4 +1,3 @@
-
 <div ng-include="httpConfigPartialSrc"></div>
 <br>
 

+ 5 - 3
public/less/grafana.less

@@ -1,19 +1,21 @@
 @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 "pagination.less";
 @import "validation.less";
 @import "fonts.less";
 @import "tabs.less";

+ 113 - 0
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;
+  }
+}

+ 0 - 0
public/less/dashlist.less → public/less/panel_dashlist.less


+ 0 - 0
public/less/graph.less → public/less/panel_graph.less


+ 0 - 0
public/less/singlestat.less → public/less/panel_singlestat.less


+ 88 - 0
public/less/panel_table.less

@@ -0,0 +1,88 @@
+.table-panel-wrapper {
+  .panel-content {
+    padding: 0;
+  }
+  .panel-title-container {
+    padding-bottom: 4px;
+  }
+}
+
+.table-panel-scroll {
+  overflow: auto;
+}
+
+.table-panel-container {
+  padding-top: 32px;
+  position: relative;
+}
+
+.table-panel-footer {
+  text-align: center;
+  font-size: 80%;
+  line-height: 2px;
+
+  ul {
+    position: relative;
+    display: inline-block;
+    margin-left: 0;
+    margin-bottom: 0;
+  }
+  ul > li {
+    display: inline; // Remove list-style and block-level defaults
+  }
+  ul > li > a {
+    float: left; // Collapse white-space
+    padding: 4px 12px;
+    text-decoration: none;
+    border-left-width: 0;
+  }
+}
+
+.table-panel-table {
+  width: 100%;
+  border-collapse: collapse;
+
+  tr {
+    border-bottom: 2px solid @bodyBackground;
+  }
+
+  th {
+    padding: 0;
+
+    &:first-child {
+      .table-panel-table-header-inner {
+        padding-left: 15px;
+      }
+    }
+  }
+
+  td {
+    padding: 10px 0 10px 15px;
+
+    &:first-child {
+      padding-left: 15px;
+    }
+  }
+}
+
+.table-panel-header-bg {
+  background: @grafanaListAccent;
+  border-top: 2px solid @bodyBackground;
+  border-bottom: 2px solid @bodyBackground;
+  height: 30px;
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+}
+
+.table-panel-table-header-inner {
+  padding: 5px 0 5px 15px;
+  text-align: left;
+  color: @blue;
+
+  position: absolute;
+  top: 0;
+  line-height: 23px;
+}
+