Browse Source

Merge branch 'tablepanel2' into develop

Torkel Ödegaard 10 năm trước cách đây
mục cha
commit
673ae1edc0
34 tập tin đã thay đổi với 1319 bổ sung78 xóa
  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;
+}
+