Browse Source

Merge branch 'dynamic-directives'

Torkel Ödegaard 10 years ago
parent
commit
88a132b878
89 changed files with 1490 additions and 1338 deletions
  1. 1 1
      docs/sources/reference/keyboard_shortcuts.md
  2. 2 2
      package.json
  3. 6 4
      pkg/api/dtos/apps.go
  4. 8 7
      public/app/core/controllers/grafana_ctrl.ts
  5. 4 5
      public/app/core/controllers/signup_ctrl.ts
  6. 6 6
      public/app/core/core.ts
  7. 0 1
      public/app/core/directives/array_join.ts
  8. 1 2
      public/app/core/directives/give_focus.ts
  9. 1 2
      public/app/core/filters/filters.ts
  10. 1 0
      public/app/core/services/all.js
  11. 64 0
      public/app/core/services/dynamic_directive_srv.ts
  12. 1 1
      public/app/core/time_series2.ts
  13. 1 7
      public/app/core/utils/datemath.ts
  14. 0 1
      public/app/core/utils/rangeutil.ts
  15. 1 1
      public/app/features/all.js
  16. 2 1
      public/app/features/annotations/annotations_srv.js
  17. 4 5
      public/app/features/annotations/editor_ctrl.js
  18. 2 1
      public/app/features/annotations/partials/editor.html
  19. 25 0
      public/app/features/annotations/query_editor.ts
  20. 1 0
      public/app/features/apps/all.ts
  21. 0 43
      public/app/features/apps/app_srv.ts
  22. 23 0
      public/app/features/apps/config_view.ts
  23. 0 1
      public/app/features/apps/edit_ctrl.ts
  24. 0 1
      public/app/features/apps/list_ctrl.ts
  25. 4 2
      public/app/features/apps/partials/edit.html
  26. 1 1
      public/app/features/dashboard/keybindings.js
  27. 0 2
      public/app/features/dashboard/timepicker/input_date.ts
  28. 2 4
      public/app/features/dashboard/timepicker/timepicker.ts
  29. 1 0
      public/app/features/datasources/all.js
  30. 25 0
      public/app/features/datasources/config_view.ts
  31. 1 1
      public/app/features/datasources/partials/edit.html
  32. 2 0
      public/app/features/panel/all.js
  33. 1 118
      public/app/features/panel/panel_directive.js
  34. 22 0
      public/app/features/panel/panel_loader.ts
  35. 48 0
      public/app/features/panel/query_editor.ts
  36. 2 2
      public/app/features/playlist/specs/playlist-edit-ctrl-specs.ts
  37. 4 1
      public/app/grafana.ts
  38. 0 220
      public/app/panels/graph/alerting.html
  39. 1 1
      public/app/partials/dashboard.html
  40. 4 4
      public/app/partials/help_modal.html
  41. 3 3
      public/app/partials/metrics.html
  42. 0 1
      public/app/plugins/datasource/cloudwatch/datasource.js
  43. 12 24
      public/app/plugins/datasource/cloudwatch/module.js
  44. 1 1
      public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html
  45. 1 1
      public/app/plugins/datasource/cloudwatch/partials/query.editor.html
  46. 16 6
      public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js
  47. 0 2
      public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts
  48. 14 0
      public/app/plugins/datasource/elasticsearch/bucket_agg.js
  49. 15 0
      public/app/plugins/datasource/elasticsearch/metric_agg.js
  50. 11 41
      public/app/plugins/datasource/elasticsearch/module.js
  51. 7 7
      public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
  52. 1 6
      public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts
  53. 7 3
      public/app/plugins/datasource/grafana/module.ts
  54. 13 12
      public/app/plugins/datasource/graphite/module.js
  55. 2 2
      public/app/plugins/datasource/graphite/partials/annotations.editor.html
  56. 1 2
      public/app/plugins/datasource/graphite/specs/gfunc_specs.ts
  57. 14 13
      public/app/plugins/datasource/influxdb/module.js
  58. 4 4
      public/app/plugins/datasource/influxdb/partials/annotations.editor.html
  59. 2 5
      public/app/plugins/datasource/influxdb/query_part.ts
  60. 0 7
      public/app/plugins/datasource/mixed/module.ts
  61. 8 9
      public/app/plugins/datasource/opentsdb/module.js
  62. 8 9
      public/app/plugins/datasource/prometheus/module.js
  63. 14 9
      public/app/plugins/panel/dashlist/module.js
  64. 1 1
      public/app/plugins/panel/graph/graph.js
  65. 2 0
      public/app/plugins/panel/graph/graph_tooltip.d.ts
  66. 0 0
      public/app/plugins/panel/graph/graph_tooltip.js
  67. 3 0
      public/app/plugins/panel/graph/module.d.ts
  68. 13 11
      public/app/plugins/panel/graph/module.js
  69. 53 0
      public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
  70. 230 0
      public/app/plugins/panel/graph/specs/graph_specs.ts
  71. 171 0
      public/app/plugins/panel/graph/specs/tooltip_specs.ts
  72. 245 0
      public/app/plugins/panel/singlestat/controller.js
  73. 1 1
      public/app/plugins/panel/singlestat/module.html
  74. 188 210
      public/app/plugins/panel/singlestat/module.js
  75. 1 1
      public/app/plugins/panel/table/controller.ts
  76. 1 1
      public/app/plugins/panel/table/editor.ts
  77. 4 3
      public/app/plugins/panel/table/module.ts
  78. 2 3
      public/app/plugins/panel/table/renderer.ts
  79. 1 2
      public/app/plugins/panel/table/transformers.ts
  80. 13 12
      public/app/plugins/panel/text/module.js
  81. 46 2
      public/app/plugins/plugin_api.md
  82. 0 51
      public/test/specs/graph-ctrl-specs.js
  83. 0 231
      public/test/specs/graph-specs.js
  84. 0 171
      public/test/specs/graph-tooltip-specs.js
  85. 2 4
      public/test/specs/helpers.d.ts
  86. 7 4
      public/test/specs/singlestat-specs.js
  87. 13 12
      tasks/options/tslint.js
  88. 5 3
      tasks/options/watch.js
  89. 62 0
      tslint.json

+ 1 - 1
docs/sources/reference/keyboard_shortcuts.md

@@ -18,10 +18,10 @@ Press `Shift`+`?` to open the keyboard shortcut dialog from anywhere within the
 |---|---|
 |---|---|
 |`Esc`|Exit fullscreen edit/view mode, close search or any editor view|
 |`Esc`|Exit fullscreen edit/view mode, close search or any editor view|
 |`F`|Open dashboard search view (also contains import/playlist controls)|
 |`F`|Open dashboard search view (also contains import/playlist controls)|
+|`R`|Refresh (Fetches new data and rerenders panels)|
 |`CTRL`+`S`|Save dashboard|
 |`CTRL`+`S`|Save dashboard|
 |`CTRL`+`H`|Hide row controls|
 |`CTRL`+`H`|Hide row controls|
 |`CTRL`+`Z`|Zoom out|
 |`CTRL`+`Z`|Zoom out|
-|`CTRL`+`R`|Refresh (Fetches new data and rerenders panels)|
 |`CTRL`+`O`|Enable/Disable shared graph crosshair|
 |`CTRL`+`O`|Enable/Disable shared graph crosshair|
 
 
 
 

+ 2 - 2
package.json

@@ -34,7 +34,7 @@
     "grunt-ng-annotate": "^1.0.1",
     "grunt-ng-annotate": "^1.0.1",
     "grunt-string-replace": "~1.2.1",
     "grunt-string-replace": "~1.2.1",
     "grunt-systemjs-builder": "^0.2.5",
     "grunt-systemjs-builder": "^0.2.5",
-    "grunt-tslint": "^2.5.0",
+    "grunt-tslint": "^3.0.1",
     "grunt-typescript": "^0.8.0",
     "grunt-typescript": "^0.8.0",
     "grunt-usemin": "3.0.0",
     "grunt-usemin": "3.0.0",
     "jshint-stylish": "~0.1.5",
     "jshint-stylish": "~0.1.5",
@@ -70,7 +70,7 @@
     "lodash": "^2.4.1",
     "lodash": "^2.4.1",
     "sinon": "1.16.1",
     "sinon": "1.16.1",
     "systemjs-builder": "^0.14.15",
     "systemjs-builder": "^0.14.15",
-    "tslint": "^3.2.0",
+    "tslint": "^3.2.1",
     "typescript": "^1.7.5"
     "typescript": "^1.7.5"
   }
   }
 }
 }

+ 6 - 4
pkg/api/dtos/apps.go

@@ -10,6 +10,7 @@ type AppSettings struct {
 	AppId    string                   `json:"appId"`
 	AppId    string                   `json:"appId"`
 	Enabled  bool                     `json:"enabled"`
 	Enabled  bool                     `json:"enabled"`
 	Pinned   bool                     `json:"pinned"`
 	Pinned   bool                     `json:"pinned"`
+	Module   string                   `json:"module"`
 	Info     *plugins.PluginInfo      `json:"info"`
 	Info     *plugins.PluginInfo      `json:"info"`
 	Pages    []*plugins.AppPluginPage `json:"pages"`
 	Pages    []*plugins.AppPluginPage `json:"pages"`
 	JsonData map[string]interface{}   `json:"jsonData"`
 	JsonData map[string]interface{}   `json:"jsonData"`
@@ -17,10 +18,11 @@ type AppSettings struct {
 
 
 func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
 func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
 	dto := &AppSettings{
 	dto := &AppSettings{
-		AppId: def.Id,
-		Name:  def.Name,
-		Info:  &def.Info,
-		Pages: def.Pages,
+		AppId:  def.Id,
+		Name:   def.Name,
+		Info:   &def.Info,
+		Module: def.Module,
+		Pages:  def.Pages,
 	}
 	}
 
 
 	if data != nil {
 	if data != nil {

+ 8 - 7
public/app/core/controllers/grafana_ctrl.ts

@@ -47,13 +47,13 @@ coreModule.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, $rootSc
   };
   };
 
 
   $rootScope.colors = [
   $rootScope.colors = [
-    "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
-    "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
-    "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
-    "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
-    "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
-    "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
-    "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"  //7
+    "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
+    "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
+    "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
+    "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
+    "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
+    "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
+    "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
   ];
   ];
 
 
   $scope.getTotalWatcherCount = function() {
   $scope.getTotalWatcherCount = function() {
@@ -85,6 +85,7 @@ coreModule.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, $rootSc
     $scope.$watch(function digestCounter() {
     $scope.$watch(function digestCounter() {
       count++;
       count++;
     }, function() {
     }, function() {
+      // something
     });
     });
 
 
     $rootScope.performance.panels = [];
     $rootScope.performance.panels = [];

+ 4 - 5
public/app/core/controllers/signup_ctrl.ts

@@ -1,6 +1,5 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import angular from 'angular';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import coreModule from '../core_module';
 import coreModule from '../core_module';
 
 
@@ -8,10 +7,10 @@ export class SignUpCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(
   constructor(
-      private $scope : any,
-      private $location : any,
-      private contextSrv : any,
-      private backendSrv : any) {
+      private $scope: any,
+      private $location: any,
+      private contextSrv: any,
+      private backendSrv: any) {
 
 
     contextSrv.sidemenu = false;
     contextSrv.sidemenu = false;
     $scope.ctrl = this;
     $scope.ctrl = this;

+ 6 - 6
public/app/core/core.ts

@@ -17,15 +17,15 @@ import "./directives/spectrum_picker";
 import "./directives/tags";
 import "./directives/tags";
 import "./directives/topnav";
 import "./directives/topnav";
 import "./directives/value_select_dropdown";
 import "./directives/value_select_dropdown";
+import "./directives/give_focus";
 import './jquery_extended';
 import './jquery_extended';
 import './partials';
 import './partials';
 
 
 import {arrayJoin} from './directives/array_join';
 import {arrayJoin} from './directives/array_join';
-import * as controllers from 'app/core/controllers/all';
-import * as services from 'app/core/services/all';
-import * as routes from 'app/core/routes/all';
+import 'app/core/controllers/all';
+import 'app/core/services/all';
+import 'app/core/routes/all';
 import './filters/filters';
 import './filters/filters';
+import coreModule from './core_module';
 
 
-// export * from './directives/give_focus'
-
-export {arrayJoin, controllers, services, routes};
+export {arrayJoin, coreModule};

+ 0 - 1
public/app/core/directives/array_join.ts

@@ -1,7 +1,6 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
 import _ from 'lodash';
 import _ from 'lodash';
-import angular from 'angular';
 import coreModule from '../core_module';
 import coreModule from '../core_module';
 
 
 export function arrayJoin() {
 export function arrayJoin() {

+ 1 - 2
public/app/core/directives/give_focus.ts

@@ -1,9 +1,8 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import angular = require('angular');
 import coreModule from '../core_module';
 import coreModule from '../core_module';
 
 
-coreModule.default.directive('giveFocus', function() {
+coreModule.directive('giveFocus', function() {
   return function(scope, element, attrs) {
   return function(scope, element, attrs) {
     element.click(function(e) {
     element.click(function(e) {
       e.stopPropagation();
       e.stopPropagation();

+ 1 - 2
public/app/core/filters/filters.ts

@@ -1,6 +1,5 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import jquery from 'jquery';
 import _ from 'lodash';
 import _ from 'lodash';
 import angular from 'angular';
 import angular from 'angular';
 import moment from 'moment';
 import moment from 'moment';
@@ -59,7 +58,7 @@ coreModule.filter('noXml', function() {
 });
 });
 
 
 coreModule.filter('interpolateTemplateVars', function (templateSrv) {
 coreModule.filter('interpolateTemplateVars', function (templateSrv) {
-  var filterFunc : any = function (text, scope) {
+  var filterFunc: any = function(text, scope) {
     if (scope.panel) {
     if (scope.panel) {
       return templateSrv.replaceWithText(text, scope.panel.scopedVars);
       return templateSrv.replaceWithText(text, scope.panel.scopedVars);
     } else {
     } else {

+ 1 - 0
public/app/core/services/all.js

@@ -9,5 +9,6 @@ define([
   './popover_srv',
   './popover_srv',
   './segment_srv',
   './segment_srv',
   './backend_srv',
   './backend_srv',
+  './dynamic_directive_srv',
 ],
 ],
 function () {});
 function () {});

+ 64 - 0
public/app/core/services/dynamic_directive_srv.ts

@@ -0,0 +1,64 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import coreModule from '../core_module';
+
+class DynamicDirectiveSrv {
+
+  /** @ngInject */
+  constructor(private $compile, private $parse, private $rootScope) {}
+
+  addDirective(element, name, scope) {
+    var child = angular.element(document.createElement(name));
+    this.$compile(child)(scope);
+
+    element.empty();
+    element.append(child);
+  }
+
+  link(scope, elem, attrs, options) {
+    options.directive(scope).then(directiveInfo => {
+      if (!directiveInfo || !directiveInfo.fn) {
+        elem.empty();
+        return;
+      }
+
+      if (!directiveInfo.fn.registered) {
+        coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
+        directiveInfo.fn.registered = true;
+      }
+
+      this.addDirective(elem, directiveInfo.name, scope);
+    }).catch(err => {
+      console.log('Plugin load:', err);
+      this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
+    });
+  }
+
+  create(options) {
+    let directiveDef = {
+      restrict: 'E',
+      scope: options.scope,
+      link: (scope, elem, attrs) => {
+        if (options.watch) {
+          let childScope = null;
+          scope.$watch(options.watch, () => {
+            if (childScope) {
+              childScope.$destroy();
+            }
+            childScope = scope.$new();
+            this.link(childScope, elem, attrs, options);
+          });
+        } else {
+          this.link(scope, elem, attrs, options);
+        }
+      }
+    };
+
+    return directiveDef;
+  }
+}
+
+coreModule.service('dynamicDirectiveSrv', DynamicDirectiveSrv);
+
+

+ 1 - 1
public/app/core/time_series2.ts

@@ -132,7 +132,7 @@ export default class TimeSeries {
         }
         }
       }
       }
 
 
-      if (currentValue != 0) {
+      if (currentValue !== 0) {
         this.allIsZero = false;
         this.allIsZero = false;
       }
       }
 
 

+ 1 - 7
public/app/core/utils/datemath.ts

@@ -4,11 +4,6 @@ import _ from 'lodash';
 import moment from 'moment';
 import moment from 'moment';
 
 
 var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
 var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
-var unitsAsc = _.sortBy(units, function (unit) {
-  return moment.duration(1, unit).valueOf();
-});
-
-var unitsDesc = unitsAsc.reverse();
 
 
 export function parse(text, roundUp?) {
 export function parse(text, roundUp?) {
   if (!text) { return undefined; }
   if (!text) { return undefined; }
@@ -104,8 +99,7 @@ export function parseDateMath(mathString, time, roundUp?) {
       if (type === 0) {
       if (type === 0) {
         if (roundUp) {
         if (roundUp) {
           dateTime.endOf(unit);
           dateTime.endOf(unit);
-        }
-        else {
+        } else {
           dateTime.startOf(unit);
           dateTime.startOf(unit);
         }
         }
       } else if (type === 1) {
       } else if (type === 1) {

+ 0 - 1
public/app/core/utils/rangeutil.ts

@@ -1,7 +1,6 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
 import _ from 'lodash';
 import _ from 'lodash';
-import angular from 'angular';
 import moment from 'moment';
 import moment from 'moment';
 import * as dateMath from './datemath';
 import * as dateMath from './datemath';
 
 

+ 1 - 1
public/app/features/all.js

@@ -1,7 +1,7 @@
 define([
 define([
   './panellinks/module',
   './panellinks/module',
   './dashlinks/module',
   './dashlinks/module',
-  './annotations/annotationsSrv',
+  './annotations/annotations_srv',
   './templating/templateSrv',
   './templating/templateSrv',
   './dashboard/all',
   './dashboard/all',
   './playlist/all',
   './playlist/all',

+ 2 - 1
public/app/features/annotations/annotationsSrv.js → public/app/features/annotations/annotations_srv.js

@@ -1,7 +1,8 @@
 define([
 define([
   'angular',
   'angular',
   'lodash',
   'lodash',
-  './editorCtrl'
+  './editor_ctrl',
+  './query_editor'
 ], function (angular, _) {
 ], function (angular, _) {
   'use strict';
   'use strict';
 
 

+ 4 - 5
public/app/features/annotations/editorCtrl.js → public/app/features/annotations/editor_ctrl.js

@@ -31,10 +31,10 @@ function (angular, _, $) {
     };
     };
 
 
     $scope.datasourceChanged = function() {
     $scope.datasourceChanged = function() {
-      $scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
-      if (!$scope.currentDatasource) {
-        $scope.currentDatasource = $scope.datasources[0];
-      }
+      datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
+        $scope.currentDatasource = ds;
+        $scope.currentAnnotation.datasource = ds.name;
+      });
     };
     };
 
 
     $scope.edit = function(annotation) {
     $scope.edit = function(annotation) {
@@ -50,7 +50,6 @@ function (angular, _, $) {
       $scope.currentAnnotation = angular.copy(annotationDefaults);
       $scope.currentAnnotation = angular.copy(annotationDefaults);
       $scope.currentIsNew = true;
       $scope.currentIsNew = true;
       $scope.datasourceChanged();
       $scope.datasourceChanged();
-      $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
     };
     };
 
 
     $scope.update = function() {
     $scope.update = function() {

+ 2 - 1
public/app/features/annotations/partials/editor.html

@@ -91,7 +91,8 @@
 				</div>
 				</div>
 			</div>
 			</div>
 
 
-			<datasource-editor-view datasource="currentAnnotation.datasource" name="annotations-query-editor"></datasource-editor-view>
+			<annotations-query-editor datasource="currentDatasource" annotation="currentAnnotation">
+			</annotations-query-editor>
 
 
 			<br>
 			<br>
 			<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>
 			<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>

+ 25 - 0
public/app/features/annotations/query_editor.ts

@@ -0,0 +1,25 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+
+/** @ngInject */
+function annotationsQueryEditor(dynamicDirectiveSrv) {
+  return dynamicDirectiveSrv.create({
+    scope: {
+      annotation: "=",
+      datasource: "="
+    },
+    watch: "datasource.type",
+    directive: scope => {
+      return System.import(scope.datasource.meta.module).then(function(dsModule) {
+        return {
+          name: 'annotation-query-editor-' + scope.datasource.meta.id,
+          fn: dsModule.annotationsQueryEditor,
+        };
+      });
+    },
+  });
+}
+
+
+angular.module('grafana.directives').directive('annotationsQueryEditor', annotationsQueryEditor);

+ 1 - 0
public/app/features/apps/all.ts

@@ -1,2 +1,3 @@
 import './edit_ctrl';
 import './edit_ctrl';
 import './list_ctrl';
 import './list_ctrl';
+import './config_view';

+ 0 - 43
public/app/features/apps/app_srv.ts

@@ -1,43 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import _ from 'lodash';
-import angular from 'angular';
-
-export class AppSrv {
-  apps: any = {};
-
-  /** @ngInject */
-  constructor(
-    private $rootScope,
-    private $timeout,
-    private $q,
-    private backendSrv) {
-  }
-
-  get(type) {
-    return this.getAll().then(() => {
-      return this.apps[type];
-    });
-  }
-
-  getAll() {
-    if (!_.isEmpty(this.apps)) {
-      return this.$q.when(this.apps);
-    }
-
-    return this.backendSrv.get('api/org/apps').then(results => {
-      return results.reduce((prev, current) => {
-        prev[current.type] = current;
-        return prev;
-      }, this.apps);
-    });
-  }
-
-  update(app) {
-    return this.backendSrv.post('api/org/apps', app).then(resp => {
-
-    });
-  }
-}
-
-angular.module('grafana.services').service('appSrv', AppSrv);

+ 23 - 0
public/app/features/apps/config_view.ts

@@ -0,0 +1,23 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+
+/** @ngInject */
+function appConfigView(dynamicDirectiveSrv) {
+  return dynamicDirectiveSrv.create({
+    scope: {
+      appModel: "="
+    },
+    directive: scope => {
+      return System.import(scope.appModel.module).then(function(appModule) {
+        return {
+          name: 'app-config-' + scope.appModel.appId,
+          fn: appModule.configView,
+        };
+      });
+    },
+  });
+}
+
+
+angular.module('grafana.directives').directive('appConfigView', appConfigView);

+ 0 - 1
public/app/features/apps/edit_ctrl.ts

@@ -1,6 +1,5 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import config from 'app/core/config';
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 
 

+ 0 - 1
public/app/features/apps/list_ctrl.ts

@@ -1,6 +1,5 @@
 ///<reference path="../../headers/common.d.ts" />
 ///<reference path="../../headers/common.d.ts" />
 
 
-import config = require('app/core/config');
 import angular from 'angular';
 import angular from 'angular';
 
 
 export class AppListCtrl {
 export class AppListCtrl {

+ 4 - 2
public/app/features/apps/partials/edit.html

@@ -29,7 +29,6 @@
 			<span style="small">
 			<span style="small">
 			Version: {{ctrl.appModel.info.version}} &nbsp; &nbsp; Updated: {{ctrl.appModel.info.updated}}
 			Version: {{ctrl.appModel.info.version}} &nbsp; &nbsp; Updated: {{ctrl.appModel.info.updated}}
 		</span>
 		</span>
-
 		</em>
 		</em>
 		<br><br>
 		<br><br>
 
 
@@ -94,9 +93,12 @@
 		<section class="simple-box">
 		<section class="simple-box">
 			<h3 class="simple-box-header">Configuration:</h3>
 			<h3 class="simple-box-header">Configuration:</h3>
 			<div class="simple-box-body">
 			<div class="simple-box-body">
+				<div ng-if="ctrl.appModel.appId">
+					<app-config-view app-model="ctrl.appModel"></app-config-view>
+				</div>
 			</div>
 			</div>
 		</section>
 		</section>
 
 
-		<app-config-loader></app-config-loader>
+
 	</div>
 	</div>
 </div>
 </div>

+ 1 - 1
public/app/features/dashboard/keybindings.js

@@ -52,7 +52,7 @@ function(angular, $) {
         scope.appEvent('save-dashboard', evt);
         scope.appEvent('save-dashboard', evt);
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 
-      keyboardManager.bind('ctrl+r', function() {
+      keyboardManager.bind('r', function() {
         scope.broadcastRefresh();
         scope.broadcastRefresh();
       }, { inputDisabled: true });
       }, { inputDisabled: true });
 
 

+ 0 - 2
public/app/features/dashboard/timepicker/input_date.ts

@@ -1,7 +1,5 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
 
 
-import _ from 'lodash';
-import angular from 'angular';
 import moment from 'moment';
 import moment from 'moment';
 
 
 export function inputDateDirective() {
 export function inputDateDirective() {

+ 2 - 4
public/app/features/dashboard/timepicker/timepicker.ts

@@ -1,19 +1,17 @@
 ///<reference path="../../../headers/common.d.ts" />
 ///<reference path="../../../headers/common.d.ts" />
 
 
 import _ from 'lodash';
 import _ from 'lodash';
-import kbn  from 'app/core/utils/kbn';
 import angular from 'angular';
 import angular from 'angular';
 import moment from 'moment';
 import moment from 'moment';
 
 
-import * as dateMath from 'app/core/utils/datemath';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 
 
 export class TimePickerCtrl {
 export class TimePickerCtrl {
 
 
   static tooltipFormat = 'MMM D, YYYY HH:mm:ss';
   static tooltipFormat = 'MMM D, YYYY HH:mm:ss';
   static defaults = {
   static defaults = {
-    time_options  : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
-    refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
+    time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
+    refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
   };
   };
 
 
   dashboard: any;
   dashboard: any;

+ 1 - 0
public/app/features/datasources/all.js

@@ -1,4 +1,5 @@
 define([
 define([
   './list_ctrl',
   './list_ctrl',
   './edit_ctrl',
   './edit_ctrl',
+  './config_view',
 ], function () {});
 ], function () {});

+ 25 - 0
public/app/features/datasources/config_view.ts

@@ -0,0 +1,25 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+
+/** @ngInject */
+function dsConfigView(dynamicDirectiveSrv) {
+  return dynamicDirectiveSrv.create({
+    scope: {
+      dsMeta: "=",
+      current: "="
+    },
+    watch: "dsMeta.module",
+    directive: scope => {
+      return System.import(scope.dsMeta.module).then(function(dsModule) {
+        return {
+          name: 'ds-config-' + scope.dsMeta.id,
+          fn: dsModule.configView,
+        };
+      });
+    },
+  });
+}
+
+
+angular.module('grafana.directives').directive('dsConfigView', dsConfigView);

+ 1 - 1
public/app/features/datasources/partials/edit.html

@@ -42,7 +42,7 @@
 				<div class="clearfix"></div>
 				<div class="clearfix"></div>
 			</div>
 			</div>
 
 
-			<datasource-custom-settings-view ds-meta="datasourceMeta" current="current"></datasource-custom-settings-view>
+			<ds-config-view ng-if="datasourceMeta.id" ds-meta="datasourceMeta" current="current"></ds-config-view>
 
 
 			<div ng-if="testing" style="margin-top: 25px">
 			<div ng-if="testing" style="margin-top: 25px">
 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
 				<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

+ 2 - 0
public/app/features/panel/all.js

@@ -4,4 +4,6 @@ define([
   './panel_srv',
   './panel_srv',
   './panel_helper',
   './panel_helper',
   './solo_panel_ctrl',
   './solo_panel_ctrl',
+  './panel_loader',
+  './query_editor',
 ], function () {});
 ], function () {});

+ 1 - 118
public/app/features/panel/panel_directive.js

@@ -1,32 +1,12 @@
 define([
 define([
   'angular',
   'angular',
   'jquery',
   'jquery',
-  'app/core/config',
 ],
 ],
-function (angular, $, config) {
+function (angular, $) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
 
 
-  module.directive('panelLoader', function($compile, $parse) {
-    return {
-      restrict: 'E',
-      link: function(scope, elem, attr) {
-        var getter = $parse(attr.type), panelType = getter(scope);
-        var module = config.panels[panelType].module;
-
-        System.import(module).then(function() {
-          var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
-          elem.append(panelEl);
-          $compile(panelEl)(scope);
-        }).catch(function(err) {
-          console.log('Failed to load panel:', err);
-          scope.appEvent('alert-error', ['Panel Load Error', 'Failed to load panel ' + panelType + ', ' + err]);
-        });
-      }
-    };
-  });
-
   module.directive('grafanaPanel', function() {
   module.directive('grafanaPanel', function() {
     return {
     return {
       restrict: 'E',
       restrict: 'E',
@@ -43,103 +23,6 @@ function (angular, $, config) {
     };
     };
   });
   });
 
 
-  module.directive('datasourceCustomSettingsView', function($compile) {
-    return {
-      restrict: 'E',
-      scope: {
-        dsMeta: "=",
-        current: "=",
-      },
-      link: function(scope, elem) {
-        scope.$watch("dsMeta.module", function() {
-          if (!scope.dsMeta) {
-            return;
-          }
-
-          System.import(scope.dsMeta.module).then(function() {
-            elem.empty();
-            var panelEl = angular.element(document.createElement('datasource-custom-settings-view-' + scope.dsMeta.id));
-            elem.append(panelEl);
-            $compile(panelEl)(scope);
-          }).catch(function(err) {
-            console.log('Failed to load plugin:', err);
-            scope.appEvent('alert-error', ['Plugin Load Error', 'Failed to load plugin ' + scope.dsMeta.id + ', ' + err]);
-          });
-        });
-      }
-    };
-  });
-
-  module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) {
-    var self = this;
-
-    this.addDirective = function(options, type, editorScope) {
-      var panelEl = angular.element(document.createElement(options.name + '-' + type));
-      options.parentElem.append(panelEl);
-      $compile(panelEl)(editorScope);
-    };
-
-    this.define = function(options) {
-      var editorScope;
-      options.scope.$watch(options.datasourceProperty, function(newVal) {
-        if (editorScope) {
-          editorScope.$destroy();
-          options.parentElem.empty();
-        }
-
-        editorScope = options.scope.$new();
-        datasourceSrv.get(newVal).then(function(ds) {
-          self.addDirective(options, ds.meta.id, editorScope);
-        });
-      });
-    };
-  });
-
-  module.directive('datasourceEditorView', function(dynamicDirectiveSrv) {
-    return {
-      restrict: 'E',
-      link: function(scope, elem, attrs) {
-        dynamicDirectiveSrv.define({
-          datasourceProperty: attrs.datasource,
-          name: attrs.name,
-          scope: scope,
-          parentElem: elem,
-        });
-      }
-    };
-  });
-
-  module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) {
-    return {
-      restrict: 'E',
-      link: function(scope, elem) {
-        var editorScope;
-
-        scope.$watch("panel.datasource", function() {
-          var datasource = scope.target.datasource || scope.panel.datasource;
-
-          datasourceSrv.get(datasource).then(function(ds) {
-            if (editorScope) {
-              editorScope.$destroy();
-              elem.empty();
-            }
-
-            editorScope = scope.$new();
-            editorScope.datasource = ds;
-
-            if (!scope.target.refId) {
-              scope.target.refId = 'A';
-            }
-
-            var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.id));
-            elem.append(panelEl);
-            $compile(panelEl)(editorScope);
-          });
-        });
-      }
-    };
-  });
-
   module.directive('panelResizer', function($rootScope) {
   module.directive('panelResizer', function($rootScope) {
     return {
     return {
       restrict: 'E',
       restrict: 'E',

+ 22 - 0
public/app/features/panel/panel_loader.ts

@@ -0,0 +1,22 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+import config from 'app/core/config';
+
+/** @ngInject */
+function panelLoader($parse, dynamicDirectiveSrv) {
+  return dynamicDirectiveSrv.create({
+    directive: scope => {
+      let modulePath = config.panels[scope.panel.type].module;
+
+      return System.import(modulePath).then(function(panelModule) {
+        return {
+          name: 'panel-directive-' + scope.panel.type,
+          fn: panelModule.panel,
+        };
+      });
+    },
+  });
+}
+
+angular.module('grafana.directives').directive('panelLoader', panelLoader);

+ 48 - 0
public/app/features/panel/query_editor.ts

@@ -0,0 +1,48 @@
+///<reference path="../../headers/common.d.ts" />
+
+import angular from 'angular';
+
+/** @ngInject */
+function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
+  return dynamicDirectiveSrv.create({
+    watch: "panel.datasource",
+    directive: scope => {
+      let datasource = scope.target.datasource || scope.panel.datasource;
+      return datasourceSrv.get(datasource).then(ds => {
+        scope.datasource = ds;
+
+        if (!scope.target.refId) {
+          scope.target.refId = 'A';
+        }
+
+        return System.import(ds.meta.module).then(dsModule => {
+          return {
+            name: 'metrics-query-editor-' + ds.meta.id,
+            fn: dsModule.metricsQueryEditor,
+          };
+        });
+      });
+    }
+  });
+}
+
+/** @ngInject */
+function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
+  return dynamicDirectiveSrv.create({
+    watch: "panel.datasource",
+    directive: scope => {
+      return datasourceSrv.get(scope.panel.datasource).then(ds => {
+        return System.import(ds.meta.module).then(dsModule => {
+          return {
+            name: 'metrics-query-options-' + ds.meta.id,
+            fn: dsModule.metricsQueryOptions
+          };
+        });
+      });
+    }
+  });
+}
+
+angular.module('grafana.directives')
+  .directive('metricsQueryEditor', metricsQueryEditor)
+  .directive('metricsQueryOptions', metricsQueryOptions);

+ 2 - 2
public/app/features/playlist/specs/playlist-edit-ctrl-specs.ts

@@ -1,5 +1,5 @@
 import '../playlist_edit_ctrl';
 import '../playlist_edit_ctrl';
-import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
 import helpers from 'test/specs/helpers';
 
 
 describe('PlaylistEditCtrl', function() {
 describe('PlaylistEditCtrl', function() {
@@ -83,4 +83,4 @@ describe('PlaylistEditCtrl', function() {
             });
             });
         });
         });
     });
     });
-});
+});

+ 4 - 1
public/app/grafana.ts

@@ -8,12 +8,12 @@ import 'angular-sanitize';
 import 'angular-dragdrop';
 import 'angular-dragdrop';
 import 'angular-bindonce';
 import 'angular-bindonce';
 import 'angular-ui';
 import 'angular-ui';
-import 'app/core/core';
 
 
 import $ from 'jquery';
 import $ from 'jquery';
 import angular from 'angular';
 import angular from 'angular';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import _ from 'lodash';
 import _ from 'lodash';
+import {coreModule} from './core/core';
 
 
 export class GrafanaApp {
 export class GrafanaApp {
   registerFunctions: any;
   registerFunctions: any;
@@ -67,6 +67,9 @@ export class GrafanaApp {
       this.useModule(angular.module(moduleName, []));
       this.useModule(angular.module(moduleName, []));
     });
     });
 
 
+    // makes it possible to add dynamic stuff
+    this.useModule(coreModule);
+
     var preBootRequires = [System.import('app/features/all')];
     var preBootRequires = [System.import('app/features/all')];
     var pluginModules = config.bootData.pluginModules || [];
     var pluginModules = config.bootData.pluginModules || [];
 
 

+ 0 - 220
public/app/panels/graph/alerting.html

@@ -1,220 +0,0 @@
-<div class="editor-row" style="margin-bottom: 20px;">
-  <span style="float: right; font-size: 12px;"><i>Last updated by Grafana October 4, 2015 12:15:04 by $username</i></span>
-  <div class="section">
-    <h5>General Alerting Options</h5>
-    <div class="tight-form last">
-      <ul class="tight-form-list">
-        <li class="tight-form-item">
-          Alert Title
-        </li>
-        <li>
-          <input type="text" class="input-xlarge tight-form-input"></input>
-        </li>
-        <li class="tight-form-item">
-          Alerting Backend
-        </li>
-        <li>
-          <select class="input-medium tight-form-input">
-            <option>Grafana Alerting</option>
-          </select>
-        </li>
-        <li class="tight-form-item last">
-          <label class="checkbox-label" for="alerting-enabled">Enabled</label>
-          <input class="cr1" id="alerting-enabled" type="checkbox">
-          <label for="alerting-enabled" class="cr1"></label>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-  </div>
-</div>
-<div class="editor-row" style="margin-bottom: 20px;">
-  <h5>Choose your query:</h5>
-  <p>Select an exising query to alert on:</p>
-  <div class="tight-form">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-      <li class="tight-form-item">None</li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="tight-form">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-      <li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
-      <li class="tight-form-item">apps</li>
-      <li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
-      <li class="tight-form-item">fakesite</li>
-      <li class="tight-form-item">counters</li>
-      <li class="tight-form-item">requests</li>
-      <li class="tight-form-item">count</li>
-      <li class="tight-form-item">scaleToSeconds(1)</li>
-      <li class="tight-form-item last">aliasByNode(2)</li>
-      <li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="tight-form">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-      <li class="tight-form-item" style="min-width: 15px; text-align: center">B</li>
-      <li class="tight-form-item last"><span class="query-keyword">Metric:</span> us-west-2 AWS/EC2 CPUUtilization <span class="query-keyword">Stats:</span> Minimum Maximum <span class="query-keyword">Dimensions</span> InstanceIS <span class="query-segment-operator">=</span> i-b0e8a447 <span class="query-keyword">Alias</span> {{stat}} <span class="query-keyword">Period</span> 60</li>
-      <li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="tight-form">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-      <li class="tight-form-item" style="min-width: 15px; text-align: center">C</li>
-      <li class="tight-form-item last"><span class="query-keyword">Query:</span> avg(counters_logins) by(server) <span class="query-keyword">Legend Format:</span> {{app}} - {{server}} <span class="query-keyword">Step:</span> 1s <span class="query-keyword">Resolution:</span> 1/2</li>
-      <li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="tight-form">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-      <li class="tight-form-item" style="min-width: 15px; text-align: center">D</li>
-      <li class="tight-form-item last"><span class="query-keyword">SELECT</span> mean(value) <span class="query-keyword">FROM</span> logins.count <span class="query-keyword">WHERE</span> hostname <span class="query-segment-operator">=</span> /$Hostname$/ <span class="query-keyword">GROUP BY</span> time($internal) hostname</li>
-      <li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-  <div class="tight-form last">
-    <ul class="tight-form-list">
-      <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" checked /></li>
-      <li class="tight-form-item" style="min-width: 15px; text-align: center">E</li>
-      <li class="tight-form-item last"><span class="query-keyword">Metric:</span> apps.backend.backend_01.counters.requests.count <span class="query-keyword">Alias:</span> Bristow <span class="query-keyword">Aggregator:</span> Sum <span class="query-keyword">Downsample:</span> 1m <span class="query-keyword">Aggregator</span> Sum <span class="query-keyword">Tags</span> host = test</li>
-      <li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
-    </ul>
-    <div class="clearfix"></div>
-  </div>
-</div>
-<div class="editor-row" style="margin-bottom: 20px;">
-  <p>Or write a new custom alerting query:</p>
-  <div class="section">
-    <div class="tight-form last">
-      <ul class="tight-form-list">
-        <li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
-        <li class="tight-form-item">
-          <a class="pointer">
-            <i class="fa fa-pencil"></i>
-          </a>
-        </li>
-        <li class="tight-form-item">
-          select metric
-        </li>
-        <li>
-          <a class="tight-form-item tight-form-func last dropdown-toggle"><i class="fa fa-plus"></i></a>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-  </div>
-</div>
-<div class="editor-row" style="margin-bottom: 10px;">
-  <div class="section">
-    <h5>Define Your States</h5>
-    <div class="tight-form last">
-      <ul class="tight-form-list">
-        <li class="tight-form-item">
-          by
-        </li>
-        <li>
-          <select class="input-medium tight-form-input">
-            <option>Averaging</option>
-          </select>
-        </li>
-        <li class="tight-form-item">
-          the values in the query over the last
-        </li>
-        <li>
-          <input type="text" class="input-mini tight-form-input last"></input>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-  </div>
-</div>
-<div class="editor-row" style="margin-bottom: 20px;">
-  <div class="section">
-    <div class="tight-form">
-      <ul class="tight-form-list">
-        <li class="tight-form-item" style="width: 100px;">
-          <span class="alert-state alert-state-warning">Warn</span>
-        </li>
-        <li>
-          <input type="text" class="input-mini tight-form-input" value=">" style="text-align: center;"></input>
-        </li>
-        <li>
-          <input type="text" class="input-mini tight-form-input" value="#B" style="text-align: center;"></input>
-        </li>
-        <li class="tight-form-item">
-          .notify
-        </li>
-        <li class="alert-notify-emails">
-          <bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
-        </li>
-        <li class="tight-form-item last">
-          <label class="checkbox-label" for="state-enabled">Enabled</label>
-          <input class="cr1" id="state-enabled" type="checkbox">
-          <label for="state-enabled" class="cr1"></label>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-    <div class="tight-form last">
-      <ul class="tight-form-list">
-        <li class="tight-form-item" style="width: 100px;">
-          <span class="alert-state alert-state-critical">Critical</span>
-        </li>
-        <li>
-          <input type="text" class="input-mini tight-form-input"></input>
-        </li>
-        <li>
-          <input type="text" class="input-mini tight-form-input"></input>
-        </li>
-        <li class="tight-form-item">
-          .notify
-        </li>
-        <li class="alert-notify-emails">
-          <bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
-        </li>
-        <li class="tight-form-item last">
-          <label class="checkbox-label" for="state-enabled2">Enabled</label>
-          <input class="cr1" id="state-enabled2" type="checkbox">
-          <label for="state-enabled2" class="cr1"></label>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-  </div>
-</div>
-<div class="editor-row">
-  <div class="section">
-    <h5>What to Say <span style="float: right; font-size: 12px; font-weight: normal;"><a href="#">Variables</a> | <a href="#">Preview</a></span></h5>
-    <div class="tight-form">
-      <ul class="tight-form-list">
-        <li class="tight-form-item" style="width: 100px;">
-          Summary
-        </li>
-        <li>
-          <input type="text" class="input-xxlarge tight-form-input last"></input>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-    <div class="tight-form last">
-      <ul class="tight-form-list">
-        <li class="tight-form-item" style="width: 100px;">
-          Description
-        </li>
-        <li>
-          <textarea class="tight-form-textarea input-xxlarge last"></textarea>
-        </li>
-      </ul>
-      <div class="clearfix"></div>
-    </div>
-  </div>
-</div>

+ 1 - 1
public/app/partials/dashboard.html

@@ -83,7 +83,7 @@
 
 
 					<div ng-repeat="(name, panel) in row.panels track by panel.id" class="panel" ui-draggable="!dashboardViewState.fullscreen" drag="panel.id"
 					<div ng-repeat="(name, panel) in row.panels track by panel.id" class="panel" ui-draggable="!dashboardViewState.fullscreen" drag="panel.id"
 						ui-on-Drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
 						ui-on-Drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
-						<panel-loader type="panel.type" class="panel-margin"></panel-loader>
+						<panel-loader class="panel-margin"></panel-loader>
 					</div>
 					</div>
 
 
 					<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">
 					<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">

+ 4 - 4
public/app/partials/help_modal.html

@@ -24,6 +24,10 @@
 				<td><span class="label label-info">F</span></td>
 				<td><span class="label label-info">F</span></td>
 				<td>Open dashboard search view (also contains import/playlist controls)</td>
 				<td>Open dashboard search view (also contains import/playlist controls)</td>
 			</tr>
 			</tr>
+			<tr>
+				<td><span class="label label-info">R</span></td>
+				<td>Refresh (Fetches new data and rerenders panels)</td>
+			</tr>
 			<tr>
 			<tr>
 				<td><span class="label label-info">CTRL+S</span></td>
 				<td><span class="label label-info">CTRL+S</span></td>
 				<td>Save dashboard</td>
 				<td>Save dashboard</td>
@@ -36,10 +40,6 @@
 				<td><span class="label label-info">CTRL+Z</span></td>
 				<td><span class="label label-info">CTRL+Z</span></td>
 				<td>Zoom out</td>
 				<td>Zoom out</td>
 			</tr>
 			</tr>
-			<tr>
-				<td><span class="label label-info">CTRL+R</span></td>
-				<td>Refresh (Fetches new data and rerenders panels)</td>
-			</tr>
 			<tr>
 			<tr>
 				<td><span class="label label-info">CTRL+O</span></td>
 				<td><span class="label label-info">CTRL+O</span></td>
 				<td>Enable/Disable shared graph crosshair</td>
 				<td>Enable/Disable shared graph crosshair</td>

+ 3 - 3
public/app/partials/metrics.html

@@ -1,8 +1,8 @@
 <div class="editor-row">
 <div class="editor-row">
 
 
 	<div class="tight-form-container">
 	<div class="tight-form-container">
-		<query-editor-loader ng-repeat="target in panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
-		</query-editor-loader>
+		<metrics-query-editor ng-repeat="target in panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
+		</metrics-query-editor>
 	</div>
 	</div>
 
 
 	<div style="margin: 20px 0 0 0">
 	<div style="margin: 20px 0 0 0">
@@ -26,7 +26,7 @@
 
 
 	</div>
 	</div>
 
 
-	<datasource-editor-view datasource="panel.datasource" name="metric-query-options"></datasource-editor-view>
+	<metrics-query-options></metrics-query-options>
 </div>
 </div>
 
 
 <div class="editor-row" style="margin-top: 30px">
 <div class="editor-row" style="margin-top: 30px">

+ 0 - 1
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -3,7 +3,6 @@ define([
   'lodash',
   'lodash',
   'moment',
   'moment',
   'app/core/utils/datemath',
   'app/core/utils/datemath',
-  './query_ctrl',
 ],
 ],
 function (angular, _, moment, dateMath) {
 function (angular, _, moment, dateMath) {
   'use strict';
   'use strict';

+ 12 - 24
public/app/plugins/datasource/cloudwatch/module.js

@@ -1,39 +1,27 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
   './query_parameter_ctrl',
   './query_parameter_ctrl',
+  './query_ctrl',
 ],
 ],
-function (angular, CloudWatchDatasource) {
+function (CloudWatchDatasource) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorCloudwatch', function() {
+  function metricsQueryEditor() {
     return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
     return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
-  });
+  }
 
 
-  module.directive('annotationsQueryEditorCloudwatch', function() {
+  function annotationsQueryEditor() {
     return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
     return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
-  });
-
-  module.directive('cloudwatchQueryParameter', function() {
-    return {
-      templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
-      controller: 'CloudWatchQueryParameterCtrl',
-      restrict: 'E',
-      scope: {
-        target: "=",
-        datasourceName: "@",
-        onChange: "&",
-      }
-    };
-  });
+  }
 
 
-  module.directive('datasourceCustomSettingsViewCloudwatch', function() {
+  function configView() {
     return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/edit_view.html'};
     return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/edit_view.html'};
-  });
+  }
 
 
   return  {
   return  {
-    Datasource: CloudWatchDatasource
+    Datasource: CloudWatchDatasource,
+    configView: configView,
+    annotationsQueryEditor: annotationsQueryEditor,
+    metricsQueryEditor: metricsQueryEditor,
   };
   };
 });
 });

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html

@@ -1 +1 @@
-<cloudwatch-query-parameter target="currentAnnotation" datasource-name="{{currentAnnotation.datasource}}"></cloudwatch-query-parameter>
+<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>

+ 1 - 1
public/app/plugins/datasource/cloudwatch/partials/query.editor.html

@@ -35,4 +35,4 @@
 	<div class="clearfix"></div>
 	<div class="clearfix"></div>
 </div>
 </div>
 
 
-<cloudwatch-query-parameter target="target" datasource-name="{{datasource.name}}" on-change="refreshMetricData()"></cloudwatch-query-parameter>
+<cloudwatch-query-parameter target="target" datasource="datasource" on-change="refreshMetricData()"></cloudwatch-query-parameter>

+ 16 - 6
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js

@@ -7,6 +7,19 @@ function (angular, _) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
+  module.directive('cloudwatchQueryParameter', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
+      controller: 'CloudWatchQueryParameterCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        datasource: "=",
+        onChange: "&",
+      }
+    };
+  });
+
   module.controller('CloudWatchQueryParameterCtrl', function($scope, templateSrv, uiSegmentSrv, datasourceSrv, $q) {
   module.controller('CloudWatchQueryParameterCtrl', function($scope, templateSrv, uiSegmentSrv, datasourceSrv, $q) {
 
 
     $scope.init = function() {
     $scope.init = function() {
@@ -38,12 +51,9 @@ function (angular, _) {
       $scope.removeDimSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove dimension --'});
       $scope.removeDimSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove dimension --'});
       $scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
       $scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
 
 
-      datasourceSrv.get($scope.datasourceName).then(function(datasource) {
-        $scope.datasource = datasource;
-        if (_.isEmpty($scope.target.region)) {
-          $scope.target.region = $scope.datasource.getDefaultRegion();
-        }
-      });
+      if (_.isEmpty($scope.target.region)) {
+        $scope.target.region = $scope.datasource.getDefaultRegion();
+      }
 
 
       if (!$scope.onChange) {
       if (!$scope.onChange) {
         $scope.onChange = function() {};
         $scope.onChange = function() {};

+ 0 - 2
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -228,10 +228,8 @@ describe('CloudWatchDatasource', function() {
         switch (params.data.action) {
         switch (params.data.action) {
         case 'DescribeAlarmsForMetric':
         case 'DescribeAlarmsForMetric':
           return ctx.$q.when({data: alarmResponse});
           return ctx.$q.when({data: alarmResponse});
-          break;
         case 'DescribeAlarmHistory':
         case 'DescribeAlarmHistory':
           return ctx.$q.when({data: historyResponse});
           return ctx.$q.when({data: historyResponse});
-          break;
         }
         }
       };
       };
     });
     });

+ 14 - 0
public/app/plugins/datasource/elasticsearch/bucket_agg.js

@@ -8,6 +8,20 @@ function (angular, _, queryDef) {
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
 
 
+  module.directive('elasticBucketAgg', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
+      controller: 'ElasticBucketAggCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        index: "=",
+        onChange: "&",
+        getFields: "&",
+      }
+    };
+  });
+
   module.controller('ElasticBucketAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
   module.controller('ElasticBucketAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
     var bucketAggs = $scope.target.bucketAggs;
     var bucketAggs = $scope.target.bucketAggs;
 
 

+ 15 - 0
public/app/plugins/datasource/elasticsearch/metric_agg.js

@@ -8,6 +8,21 @@ function (angular, _, queryDef) {
 
 
   var module = angular.module('grafana.directives');
   var module = angular.module('grafana.directives');
 
 
+  module.directive('elasticMetricAgg', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
+      controller: 'ElasticMetricAggCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        index: "=",
+        onChange: "&",
+        getFields: "&",
+        esVersion: '='
+      }
+    };
+  });
+
   module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
   module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
     var metricAggs = $scope.target.metrics;
     var metricAggs = $scope.target.metrics;
 
 

+ 11 - 41
public/app/plugins/datasource/elasticsearch/module.js

@@ -1,60 +1,30 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
   './edit_view',
   './edit_view',
   './bucket_agg',
   './bucket_agg',
   './metric_agg',
   './metric_agg',
 ],
 ],
-function (angular, ElasticDatasource, editView) {
+function (ElasticDatasource, editView) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorElasticsearch', function() {
+  function metricsQueryEditor() {
     return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
     return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
-  });
+  }
 
 
-  module.directive('metricQueryOptionsElasticsearch', function() {
+  function metricsQueryOptions() {
     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
-  });
+  }
 
 
-  module.directive('annotationsQueryEditorElasticsearch', function() {
+  function annotationsQueryEditor() {
     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
     return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
-  });
-
-  module.directive('elasticMetricAgg', function() {
-    return {
-      templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
-      controller: 'ElasticMetricAggCtrl',
-      restrict: 'E',
-      scope: {
-        target: "=",
-        index: "=",
-        onChange: "&",
-        getFields: "&",
-        esVersion: '='
-      }
-    };
-  });
-
-  module.directive('elasticBucketAgg', function() {
-    return {
-      templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
-      controller: 'ElasticBucketAggCtrl',
-      restrict: 'E',
-      scope: {
-        target: "=",
-        index: "=",
-        onChange: "&",
-        getFields: "&",
-      }
-    };
-  });
-
-  module.directive('datasourceCustomSettingsViewElasticsearch', editView.default);
+  }
 
 
   return {
   return {
     Datasource: ElasticDatasource,
     Datasource: ElasticDatasource,
+    configView: editView.default,
+    annotationsQueryEditor: annotationsQueryEditor,
+    metricsQueryEditor: metricsQueryEditor,
+    metricsQueryOptions: metricsQueryOptions,
   };
   };
 
 
 });
 });

+ 7 - 7
public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html

@@ -1,14 +1,14 @@
 <div class="editor-row">
 <div class="editor-row">
-	<div class="section" ng-if="currentAnnotation.index">
+	<div class="section" ng-if="annotation.index">
 		<h5>Index name</h5>
 		<h5>Index name</h5>
 		<div class="editor-option">
 		<div class="editor-option">
-			<input type="text" class="span4" ng-model='currentAnnotation.index' placeholder="events-*"></input>
+			<input type="text" class="span4" ng-model='annotation.index' placeholder="events-*"></input>
 		</div>
 		</div>
 	</div>
 	</div>
 	<div class="section">
 	<div class="section">
 		<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
 		<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
 		<div class="editor-option">
 		<div class="editor-option">
-			<input type="text" class="span6" ng-model='currentAnnotation.query' placeholder="tags:deploy"></input>
+			<input type="text" class="span6" ng-model='annotation.query' placeholder="tags:deploy"></input>
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>
@@ -18,22 +18,22 @@
 		<h5>Field mappings</h5>
 		<h5>Field mappings</h5>
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Time</label>
 			<label class="small">Time</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.timeField' placeholder="@timestamp"></input>
+			<input type="text" class="input-small" ng-model='annotation.timeField' placeholder="@timestamp"></input>
 		</div>
 		</div>
 
 
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Title</label>
 			<label class="small">Title</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.titleField' placeholder="desc"></input>
+			<input type="text" class="input-small" ng-model='annotation.titleField' placeholder="desc"></input>
 		</div>
 		</div>
 
 
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Tags</label>
 			<label class="small">Tags</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.tagsField' placeholder="tags"></input>
+			<input type="text" class="input-small" ng-model='annotation.tagsField' placeholder="tags"></input>
 		</div>
 		</div>
 
 
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Text</label>
 			<label class="small">Text</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.textField' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='annotation.textField' placeholder=""></input>
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>

+ 1 - 6
public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts

@@ -1,5 +1,5 @@
 
 
-import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+import {describe, beforeEach, it, expect} from 'test/lib/common';
 import ElasticResponse from '../elastic_response';
 import ElasticResponse from '../elastic_response';
 
 
 describe('ElasticResponse', function() {
 describe('ElasticResponse', function() {
@@ -497,11 +497,6 @@ describe('ElasticResponse', function() {
     });
     });
   });
   });
 
 
-  describe('', function() {
-
-
-  });
-
   describe('Raw documents query', function() {
   describe('Raw documents query', function() {
     beforeEach(function() {
     beforeEach(function() {
       targets = [{ refId: 'A', metrics: [{type: 'raw_document', id: '1'}], bucketAggs: [] }];
       targets = [{ refId: 'A', metrics: [{type: 'raw_document', id: '1'}], bucketAggs: [] }];

+ 7 - 3
public/app/plugins/datasource/grafana/module.ts

@@ -5,10 +5,14 @@ import {GrafanaDatasource} from './datasource';
 
 
 var module = angular.module('grafana.directives');
 var module = angular.module('grafana.directives');
 
 
-module.directive('metricQueryEditorGrafana', function() {
+function grafanaMetricsQueryEditor() {
   return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'};
   return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'};
-});
+}
 
 
 
 
-export {GrafanaDatasource, GrafanaDatasource as Datasource};
+export {
+  GrafanaDatasource,
+  GrafanaDatasource as Datasource,
+  grafanaMetricsQueryEditor as metricsQueryEditor
+};
 
 

+ 13 - 12
public/app/plugins/datasource/graphite/module.js

@@ -1,29 +1,30 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
 ],
 ],
-function (angular, GraphiteDatasource) {
+function (GraphiteDatasource) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorGraphite', function() {
+  function metricsQueryEditor() {
     return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'};
     return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'};
-  });
+  }
 
 
-  module.directive('metricQueryOptionsGraphite', function() {
+  function metricsQueryOptions() {
     return {templateUrl: 'app/plugins/datasource/graphite/partials/query.options.html'};
     return {templateUrl: 'app/plugins/datasource/graphite/partials/query.options.html'};
-  });
+  }
 
 
-  module.directive('annotationsQueryEditorGraphite', function() {
+  function annotationsQueryEditor() {
     return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
     return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
-  });
+  }
 
 
-  module.directive('datasourceCustomSettingsViewGraphite', function() {
+  function configView() {
     return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
     return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
-  });
+  }
 
 
   return {
   return {
     Datasource: GraphiteDatasource,
     Datasource: GraphiteDatasource,
+    configView: configView,
+    annotationsQueryEditor: annotationsQueryEditor,
+    metricsQueryEditor: metricsQueryEditor,
+    metricsQueryOptions: metricsQueryOptions,
   };
   };
 });
 });

+ 2 - 2
public/app/plugins/datasource/graphite/partials/annotations.editor.html

@@ -1,14 +1,14 @@
 <div class="editor-row">
 <div class="editor-row">
 	<div class="editor-option">
 	<div class="editor-option">
 		<label class="small">Graphite target expression</label>
 		<label class="small">Graphite target expression</label>
-		<input type="text" class="span10" ng-model='currentAnnotation.target' placeholder=""></input>
+		<input type="text" class="span10" ng-model='annotation.target' placeholder=""></input>
 	</div>
 	</div>
 </div>
 </div>
 
 
 <div class="editor-row">
 <div class="editor-row">
 	<div class="editor-option">
 	<div class="editor-option">
 		<label class="small">Graphite event tags</label>
 		<label class="small">Graphite event tags</label>
-		<input type="text" ng-model='currentAnnotation.tags' placeholder=""></input>
+		<input type="text" ng-model='annotation.tags' placeholder=""></input>
 	</div>
 	</div>
 </div>
 </div>
 
 

+ 1 - 2
public/app/plugins/datasource/graphite/specs/gfunc_specs.ts

@@ -1,6 +1,5 @@
-///<amd-dependency path="app/plugins/datasource/graphite/gfunc" name="gfunc" />
 
 
-import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+import {describe, it, expect} from 'test/lib/common';
 import gfunc from '../gfunc';
 import gfunc from '../gfunc';
 
 
 describe('when creating func instance from func names', function() {
 describe('when creating func instance from func names', function() {

+ 14 - 13
public/app/plugins/datasource/influxdb/module.js

@@ -1,29 +1,30 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
 ],
 ],
-function (angular, InfluxDatasource) {
+function (InfluxDatasource) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorInfluxdb', function() {
+  function influxMetricsQueryEditor() {
     return {controller: 'InfluxQueryCtrl', templateUrl: 'app/plugins/datasource/influxdb/partials/query.editor.html'};
     return {controller: 'InfluxQueryCtrl', templateUrl: 'app/plugins/datasource/influxdb/partials/query.editor.html'};
-  });
+  }
 
 
-  module.directive('metricQueryOptionsInfluxdb', function() {
+  function influxMetricsQueryOptions() {
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/query.options.html'};
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/query.options.html'};
-  });
+  }
 
 
-  module.directive('annotationsQueryEditorInfluxdb', function() {
+  function influxAnnotationsQueryEditor() {
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'};
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'};
-  });
+  }
 
 
-  module.directive('datasourceCustomSettingsViewInfluxdb', function() {
+  function influxConfigView() {
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/config.html'};
     return {templateUrl: 'app/plugins/datasource/influxdb/partials/config.html'};
-  });
+  }
 
 
   return {
   return {
-    Datasource: InfluxDatasource
+    Datasource:               InfluxDatasource,
+    metricsQueryEditor:       influxMetricsQueryEditor,
+    metricsQueryOptions:      influxMetricsQueryOptions,
+    annotationsQueryEditor:   influxAnnotationsQueryEditor,
+    configView:               influxConfigView,
   };
   };
 });
 });

+ 4 - 4
public/app/plugins/datasource/influxdb/partials/annotations.editor.html

@@ -2,7 +2,7 @@
 	<div class="section">
 	<div class="section">
 		<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
 		<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
 		<div class="editor-option">
 		<div class="editor-option">
-			<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where $timeFilter"></input>
+			<input type="text" class="span10" ng-model='annotation.query' placeholder="select text from events where $timeFilter"></input>
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>
@@ -12,17 +12,17 @@
 		<h5>Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
 		<h5>Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Title</label>
 			<label class="small">Title</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.titleColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='annotation.titleColumn' placeholder=""></input>
 		</div>
 		</div>
 
 
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Tags</label>
 			<label class="small">Tags</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.tagsColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='annotation.tagsColumn' placeholder=""></input>
 		</div>
 		</div>
 
 
 		<div class="editor-option">
 		<div class="editor-option">
 			<label class="small">Text</label>
 			<label class="small">Text</label>
-			<input type="text" class="input-small" ng-model='currentAnnotation.textColumn' placeholder=""></input>
+			<input type="text" class="input-small" ng-model='annotation.textColumn' placeholder=""></input>
 		</div>
 		</div>
 	</div>
 	</div>
 </div>
 </div>

+ 2 - 5
public/app/plugins/datasource/influxdb/query_part.ts

@@ -126,9 +126,7 @@ function addMathStrategy(selectParts, partModel) {
     if (selectParts[partCount-2].def.type === 'math') {
     if (selectParts[partCount-2].def.type === 'math') {
       selectParts[partCount-2] = partModel;
       selectParts[partCount-2] = partModel;
       return;
       return;
-    }
-    // if last is alias add it before
-    else if (selectParts[partCount-1].def.type === 'alias') {
+    } else if (selectParts[partCount-1].def.type === 'alias') { // if last is alias add it before
       selectParts.splice(partCount-1, 0, partModel);
       selectParts.splice(partCount-1, 0, partModel);
       return;
       return;
     }
     }
@@ -399,8 +397,7 @@ class QueryPart {
 
 
     if (strValue === '' && this.def.params[index].optional) {
     if (strValue === '' && this.def.params[index].optional) {
       this.params.splice(index, 1);
       this.params.splice(index, 1);
-    }
-    else {
+    } else {
       this.params[index] = strValue;
       this.params[index] = strValue;
     }
     }
 
 

+ 0 - 7
public/app/plugins/datasource/mixed/module.ts

@@ -3,12 +3,5 @@
 import angular from 'angular';
 import angular from 'angular';
 import {MixedDatasource} from './datasource';
 import {MixedDatasource} from './datasource';
 
 
-var module = angular.module('grafana.directives');
-
-module.directive('metricQueryEditorMixed', function() {
-  return {templateUrl: 'app/plugins/datasource/mixed/partials/query.editor.html'};
-});
-
-
 export {MixedDatasource, MixedDatasource as Datasource};
 export {MixedDatasource, MixedDatasource as Datasource};
 
 

+ 8 - 9
public/app/plugins/datasource/opentsdb/module.js

@@ -1,24 +1,23 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
 ],
 ],
-function (angular, OpenTsDatasource) {
+function (OpenTsDatasource) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorOpentsdb', function() {
+  function metricsQueryEditor() {
     return {
     return {
       controller: 'OpenTSDBQueryCtrl',
       controller: 'OpenTSDBQueryCtrl',
       templateUrl: 'app/plugins/datasource/opentsdb/partials/query.editor.html',
       templateUrl: 'app/plugins/datasource/opentsdb/partials/query.editor.html',
     };
     };
-  });
+  }
 
 
-  module.directive('datasourceCustomSettingsViewOpentsdb', function() {
+  function configView() {
     return {templateUrl: 'app/plugins/datasource/opentsdb/partials/config.html'};
     return {templateUrl: 'app/plugins/datasource/opentsdb/partials/config.html'};
-  });
+  }
 
 
   return {
   return {
-    Datasource: OpenTsDatasource
+    Datasource: OpenTsDatasource,
+    metricsQueryEditor: metricsQueryEditor,
+    configView: configView,
   };
   };
 });
 });

+ 8 - 9
public/app/plugins/datasource/prometheus/module.js

@@ -1,21 +1,20 @@
 define([
 define([
-  'angular',
   './datasource',
   './datasource',
 ],
 ],
-function (angular, PromDatasource) {
+function (PromDatasource) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.directives');
-
-  module.directive('metricQueryEditorPrometheus', function() {
+  function metricsQueryEditor() {
     return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
     return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
-  });
+  }
 
 
-  module.directive('datasourceCustomSettingsViewPrometheus', function() {
+  function configView() {
     return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'};
     return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'};
-  });
+  }
 
 
   return {
   return {
-    Datasource: PromDatasource
+    Datasource: PromDatasource,
+    metricsQueryEditor: metricsQueryEditor,
+    configView: configView,
   };
   };
 });
 });

+ 14 - 9
public/app/plugins/panel/dashlist/module.js

@@ -11,14 +11,8 @@ function (angular, app, _, config, PanelMeta) {
   var module = angular.module('grafana.panels.dashlist', []);
   var module = angular.module('grafana.panels.dashlist', []);
   app.useModule(module);
   app.useModule(module);
 
 
-  module.directive('grafanaPanelDashlist', function() {
-    return {
-      controller: 'DashListPanelCtrl',
-      templateUrl: 'app/plugins/panel/dashlist/module.html',
-    };
-  });
-
-  module.controller('DashListPanelCtrl', function($scope, panelSrv, backendSrv) {
+  /** @ngInject */
+  function DashListPanelCtrl($scope, panelSrv, backendSrv) {
 
 
     $scope.panelMeta = new PanelMeta({
     $scope.panelMeta = new PanelMeta({
       panelName: 'Dashboard list',
       panelName: 'Dashboard list',
@@ -73,5 +67,16 @@ function (angular, app, _, config, PanelMeta) {
     };
     };
 
 
     $scope.init();
     $scope.init();
-  });
+  }
+
+  function dashListPanelDirective() {
+    return {
+      controller: DashListPanelCtrl,
+      templateUrl: 'app/plugins/panel/dashlist/module.html',
+    };
+  }
+
+  return {
+    panel: dashListPanelDirective
+  };
 });
 });

+ 1 - 1
public/app/plugins/panel/graph/graph.js

@@ -4,7 +4,7 @@ define([
   'moment',
   'moment',
   'lodash',
   'lodash',
   'app/core/utils/kbn',
   'app/core/utils/kbn',
-  './graph.tooltip',
+  './graph_tooltip',
   'jquery.flot',
   'jquery.flot',
   'jquery.flot.events',
   'jquery.flot.events',
   'jquery.flot.selection',
   'jquery.flot.selection',

+ 2 - 0
public/app/plugins/panel/graph/graph_tooltip.d.ts

@@ -0,0 +1,2 @@
+declare var GraphTooltip: any;
+export default GraphTooltip;

+ 0 - 0
public/app/plugins/panel/graph/graph.tooltip.js → public/app/plugins/panel/graph/graph_tooltip.js


+ 3 - 0
public/app/plugins/panel/graph/module.d.ts

@@ -0,0 +1,3 @@
+declare var panel: any;
+declare var GraphCtrl: any;
+export {panel, GraphCtrl};

+ 13 - 11
public/app/plugins/panel/graph/module.js

@@ -12,16 +12,8 @@ define([
 function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
 function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.panels.graph');
-
-  module.directive('grafanaPanelGraph', function() {
-    return {
-      controller: 'GraphCtrl',
-      templateUrl: 'app/plugins/panel/graph/module.html',
-    };
-  });
-
-  module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) {
+  /** @ngInject */
+  function GraphCtrl($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) {
 
 
     $scope.panelMeta = new PanelMeta({
     $scope.panelMeta = new PanelMeta({
       panelName: 'Graph',
       panelName: 'Graph',
@@ -294,7 +286,17 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
     };
     };
 
 
     panelSrv.init($scope);
     panelSrv.init($scope);
+  }
 
 
-  });
+  function graphPanelDirective() {
+    return {
+      controller: GraphCtrl,
+      templateUrl: 'app/plugins/panel/graph/module.html',
+    };
+  }
 
 
+  return {
+    GraphCtrl: GraphCtrl,
+    panel: graphPanelDirective,
+  };
 });
 });

+ 53 - 0
public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts

@@ -0,0 +1,53 @@
+///<reference path="../../../../headers/common.d.ts" />
+
+import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
+
+import 'app/features/panel/panel_srv';
+import 'app/features/panel/panel_helper';
+
+import angular from 'angular';
+import {GraphCtrl} from '../module';
+import helpers from '../../../../../test/specs/helpers';
+
+angular.module('grafana.controllers').controller('GraphCtrl', GraphCtrl);
+
+describe('GraphCtrl', function() {
+  var ctx = new helpers.ControllerTestContext();
+
+  beforeEach(angularMocks.module('grafana.services'));
+  beforeEach(angularMocks.module('grafana.controllers'));
+
+  beforeEach(ctx.providePhase());
+  beforeEach(ctx.createControllerPhase('GraphCtrl'));
+
+  describe('get_data with 2 series', function() {
+    beforeEach(function() {
+      ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([]));
+      ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
+        data: [
+          { target: 'test.cpu1', datapoints: [[1, 10]]},
+          { target: 'test.cpu2', datapoints: [[1, 10]]}
+        ]
+      }));
+      ctx.scope.render = sinon.spy();
+      ctx.scope.refreshData(ctx.datasource);
+      ctx.scope.$digest();
+    });
+
+    it('should send time series to render', function() {
+      var data = ctx.scope.render.getCall(0).args[0];
+      expect(data.length).to.be(2);
+    });
+
+    describe('get_data failure following success', function() {
+      beforeEach(function() {
+        ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
+        ctx.scope.refreshData(ctx.datasource);
+        ctx.scope.$digest();
+      });
+
+    });
+
+  });
+
+});

+ 230 - 0
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -0,0 +1,230 @@
+///<reference path="../../../../headers/common.d.ts" />
+
+import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
+
+import '../module';
+import angular from 'angular';
+import $ from 'jquery';
+import helpers from '../../../../../test/specs/helpers';
+import TimeSeries from '../../../../core/time_series2';
+
+describe('grafanaGraph', function() {
+
+  beforeEach(angularMocks.module('grafana.directives'));
+
+  function graphScenario(desc, func)  {
+    describe(desc, function() {
+      var ctx: any = {};
+
+      ctx.setup = function(setupFunc) {
+
+        beforeEach(angularMocks.module(function($provide) {
+          $provide.value("timeSrv", new helpers.TimeSrvStub());
+        }));
+
+        beforeEach(angularMocks.inject(function($rootScope, $compile) {
+          var scope = $rootScope.$new();
+          var element = angular.element("<div style='width:500px' grafana-graph><div>");
+
+          scope.height = '200px';
+          scope.panel = {
+            legend: {},
+            grid: { },
+            y_formats: [],
+            seriesOverrides: [],
+            tooltip: {
+              shared: true
+            }
+          };
+
+          scope.panelRenderingComplete = sinon.spy();
+          scope.appEvent = sinon.spy();
+          scope.onAppEvent = sinon.spy();
+          scope.hiddenSeries = {};
+          scope.dashboard = { timezone: 'browser' };
+          scope.range = {
+            from: new Date('2014-08-09 10:00:00'),
+            to: new Date('2014-09-09 13:00:00')
+          };
+          ctx.data = [];
+          ctx.data.push(new TimeSeries({
+            datapoints: [[1,1],[2,2]],
+            alias: 'series1'
+          }));
+          ctx.data.push(new TimeSeries({
+            datapoints: [[1,1],[2,2]],
+            alias: 'series2'
+          }));
+
+          setupFunc(scope, ctx.data);
+
+          $compile(element)(scope);
+          scope.$digest();
+          $.plot = ctx.plotSpy = sinon.spy();
+
+          scope.$emit('render', ctx.data);
+          ctx.plotData = ctx.plotSpy.getCall(0).args[1];
+          ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
+        }));
+      };
+
+      func(ctx);
+    });
+  }
+
+  graphScenario('simple lines options', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.lines = true;
+      scope.panel.fill = 5;
+      scope.panel.linewidth = 3;
+      scope.panel.steppedLine = true;
+    });
+
+    it('should configure plot with correct options', function() {
+      expect(ctx.plotOptions.series.lines.show).to.be(true);
+      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+      expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
+      expect(ctx.plotOptions.series.lines.steps).to.be(true);
+    });
+  });
+
+  graphScenario('grid thresholds 100, 200', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.grid = {
+        threshold1: 100,
+        threshold1Color: "#111",
+        threshold2: 200,
+        threshold2Color: "#222",
+      };
+    });
+
+    it('should add grid markings', function() {
+      var markings = ctx.plotOptions.grid.markings;
+      expect(markings[0].yaxis.from).to.be(100);
+      expect(markings[0].yaxis.to).to.be(200);
+      expect(markings[0].color).to.be('#111');
+      expect(markings[1].yaxis.from).to.be(200);
+      expect(markings[1].yaxis.to).to.be(Infinity);
+    });
+  });
+
+  graphScenario('inverted grid thresholds 200, 100', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.grid = {
+        threshold1: 200,
+        threshold1Color: "#111",
+        threshold2: 100,
+        threshold2Color: "#222",
+      };
+    });
+
+    it('should add grid markings', function() {
+      var markings = ctx.plotOptions.grid.markings;
+      expect(markings[0].yaxis.from).to.be(200);
+      expect(markings[0].yaxis.to).to.be(100);
+      expect(markings[0].color).to.be('#111');
+      expect(markings[1].yaxis.from).to.be(100);
+      expect(markings[1].yaxis.to).to.be(-Infinity);
+    });
+  });
+
+  graphScenario('grid thresholds from zero', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.grid = {
+        threshold1: 0,
+        threshold1Color: "#111",
+      };
+    });
+
+    it('should add grid markings', function() {
+      var markings = ctx.plotOptions.grid.markings;
+      expect(markings[0].yaxis.from).to.be(0);
+    });
+  });
+
+  graphScenario('when logBase is log 10', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.grid = {
+        leftMax: null,
+        rightMax: null,
+        leftMin: null,
+        rightMin: null,
+        leftLogBase: 10,
+      };
+    });
+
+    it('should apply axis transform and ticks', function() {
+      var axis = ctx.plotOptions.yaxes[0];
+      expect(axis.transform(100)).to.be(Math.log(100+0.1));
+      expect(axis.ticks[0]).to.be(0);
+      expect(axis.ticks[1]).to.be(1);
+    });
+  });
+
+  graphScenario('should use timeStep for barWidth', function(ctx) {
+    ctx.setup(function(scope, data) {
+      scope.panel.bars = true;
+      data[0] = new TimeSeries({
+        datapoints: [[1,10],[2,20]],
+        alias: 'series1',
+      });
+    });
+
+    it('should set barWidth', function() {
+      expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
+    });
+  });
+
+  graphScenario('series option overrides, fill & points', function(ctx) {
+    ctx.setup(function(scope, data) {
+      scope.panel.lines = true;
+      scope.panel.fill = 5;
+      scope.panel.seriesOverrides = [
+        { alias: 'test', fill: 0, points: true }
+      ];
+
+      data[1].alias = 'test';
+    });
+
+    it('should match second series and fill zero, and enable points', function() {
+      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
+      expect(ctx.plotData[1].lines.fill).to.be(0.001);
+      expect(ctx.plotData[1].points.show).to.be(true);
+    });
+  });
+
+  graphScenario('should order series order according to zindex', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
+    });
+
+    it('should move zindex 2 last', function() {
+      expect(ctx.plotData[0].alias).to.be('series2');
+      expect(ctx.plotData[1].alias).to.be('series1');
+    });
+  });
+
+  graphScenario('when series is hidden', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.hiddenSeries = {'series2': true};
+    });
+
+    it('should remove datapoints and disable stack', function() {
+      expect(ctx.plotData[0].alias).to.be('series1');
+      expect(ctx.plotData[1].data.length).to.be(0);
+      expect(ctx.plotData[1].stack).to.be(false);
+    });
+  });
+
+  graphScenario('when stack and percent', function(ctx) {
+    ctx.setup(function(scope) {
+      scope.panel.percentage = true;
+      scope.panel.stack = true;
+    });
+
+    it('should show percentage', function() {
+      var axis = ctx.plotOptions.yaxes[0];
+      expect(axis.tickFormatter(100, axis)).to.be("100%");
+    });
+  });
+});

+ 171 - 0
public/app/plugins/panel/graph/specs/tooltip_specs.ts

@@ -0,0 +1,171 @@
+///<reference path="../../../../headers/common.d.ts" />
+
+import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
+
+import $ from 'jquery';
+import GraphTooltip from '../graph_tooltip';
+
+var scope =  {
+  appEvent: sinon.spy(),
+  onAppEvent: sinon.spy(),
+};
+
+var elem = $('<div></div>');
+var dashboard = { };
+
+function describeSharedTooltip(desc, fn) {
+  var ctx: any = {};
+  ctx.scope = scope;
+  ctx.scope.panel =  {
+    tooltip:  {
+      shared: true
+    },
+    legend: { },
+    stack: false
+  };
+
+  ctx.setup = function(setupFn) {
+    ctx.setupFn = setupFn;
+  };
+
+  describe(desc, function() {
+    beforeEach(function() {
+      ctx.setupFn();
+      var tooltip = new GraphTooltip(elem, dashboard, scope);
+      ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
+    });
+
+    fn(ctx);
+  });
+}
+
+describeSharedTooltip("steppedLine false, stack false", function(ctx) {
+  ctx.setup(function() {
+    ctx.data = [
+      { data: [[10, 15], [12, 20]], lines: {} },
+      { data: [[10, 2], [12, 3]], lines: {} }
+    ];
+    ctx.pos = { x: 11 };
+  });
+
+  it('should return 2 series', function() {
+    expect(ctx.results.length).to.be(2);
+  });
+  it('should add time to results array', function() {
+    expect(ctx.results.time).to.be(10);
+  });
+  it('should set value and hoverIndex', function() {
+    expect(ctx.results[0].value).to.be(15);
+    expect(ctx.results[1].value).to.be(2);
+    expect(ctx.results[0].hoverIndex).to.be(0);
+  });
+});
+
+describeSharedTooltip("one series is hidden", function(ctx) {
+  ctx.setup(function() {
+    ctx.data = [
+      { data: [[10, 15], [12, 20]], },
+      { data: [] }
+    ];
+    ctx.pos = { x: 11 };
+  });
+});
+
+describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
+  ctx.setup(function() {
+    ctx.data = [
+      {
+        data: [[10, 15], [12, 20]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10,15], [12,20]],
+        },
+        stack: true,
+      },
+      {
+        data: [[10, 2], [12, 3]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10, 2], [12, 3]],
+        },
+        stack: true
+      }
+    ];
+    ctx.scope.panel.stack = true;
+    ctx.pos = { x: 11 };
+  });
+
+  it('should show stacked value', function() {
+    expect(ctx.results[1].value).to.be(17);
+  });
+});
+
+describeSharedTooltip("steppedLine false, stack true, individual false, series stack false", function(ctx) {
+  ctx.setup(function() {
+    ctx.data = [
+      {
+        data: [[10, 15], [12, 20]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10, 15], [12, 20]],
+        },
+        stack: true
+      },
+      {
+        data: [[10, 2], [12, 3]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10, 2], [12, 3]],
+        },
+        stack: false
+      }
+    ];
+    ctx.scope.panel.stack = true;
+    ctx.pos = { x: 11 };
+  });
+
+  it('should not show stacked value', function() {
+    expect(ctx.results[1].value).to.be(2);
+  });
+
+});
+
+describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
+  ctx.setup(function() {
+    ctx.data = [
+      {
+        data: [[10, 15], [12, 20]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10, 15], [12, 20]],
+        },
+        stack: true
+      },
+      {
+        data: [[10, 2], [12, 3]],
+        lines: {},
+        datapoints: {
+          pointsize: 2,
+          points: [[10, 2], [12, 3]],
+        },
+        stack: false
+      }
+    ];
+    ctx.scope.panel.stack = true;
+    ctx.scope.panel.tooltip.value_type = 'individual';
+    ctx.pos = { x: 11 };
+  });
+
+  it('should not show stacked value', function() {
+    expect(ctx.results[1].value).to.be(2);
+  });
+
+});
+
+
+

+ 245 - 0
public/app/plugins/panel/singlestat/controller.js

@@ -0,0 +1,245 @@
+define([
+  'angular',
+  'app/app',
+  'lodash',
+  'app/core/utils/kbn',
+  'app/core/time_series',
+  'app/features/panel/panel_meta',
+],
+function (angular, app, _, kbn, TimeSeries, PanelMeta) {
+  'use strict';
+
+  /** @ngInject */
+  function SingleStatCtrl($scope, panelSrv, panelHelper) {
+
+    $scope.panelMeta = new PanelMeta({
+      panelName: 'Singlestat',
+      editIcon:  "fa fa-dashboard",
+      fullscreen: true,
+      metricsEditor: true
+    });
+
+    $scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
+
+    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html');
+    $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
+
+    // Set and populate defaults
+    var _d = {
+      links: [],
+      datasource: null,
+      maxDataPoints: 100,
+      interval: null,
+      targets: [{}],
+      cacheTimeout: null,
+      format: 'none',
+      prefix: '',
+      postfix: '',
+      nullText: null,
+      valueMaps: [
+        { value: 'null', op: '=', text: 'N/A' }
+      ],
+      nullPointMode: 'connected',
+      valueName: 'avg',
+      prefixFontSize: '50%',
+      valueFontSize: '80%',
+      postfixFontSize: '50%',
+      thresholds: '',
+      colorBackground: false,
+      colorValue: false,
+      colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+      sparkline: {
+        show: false,
+        full: false,
+        lineColor: 'rgb(31, 120, 193)',
+        fillColor: 'rgba(31, 118, 189, 0.18)',
+      }
+    };
+
+    _.defaults($scope.panel, _d);
+    $scope.unitFormats = kbn.getUnitFormats();
+
+    $scope.setUnitFormat = function(subItem) {
+      $scope.panel.format = subItem.value;
+      $scope.render();
+    };
+
+    $scope.init = function() {
+      panelSrv.init($scope);
+    };
+
+    $scope.refreshData = function(datasource) {
+      panelHelper.updateTimeRange($scope);
+
+      return panelHelper.issueMetricQuery($scope, datasource)
+        .then($scope.dataHandler, function(err) {
+          $scope.series = [];
+          $scope.render();
+          throw err;
+        });
+    };
+
+    $scope.loadSnapshot = function(snapshotData) {
+      panelHelper.updateTimeRange($scope);
+      $scope.dataHandler(snapshotData);
+    };
+
+    $scope.dataHandler = function(results) {
+      $scope.series = _.map(results.data, $scope.seriesHandler);
+      $scope.render();
+    };
+
+    $scope.seriesHandler = function(seriesData) {
+      var series = new TimeSeries({
+        datapoints: seriesData.datapoints,
+        alias: seriesData.target,
+      });
+
+      series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
+
+      return series;
+    };
+
+    $scope.setColoring = function(options) {
+      if (options.background) {
+        $scope.panel.colorValue = false;
+        $scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
+      }
+      else {
+        $scope.panel.colorBackground = false;
+        $scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
+      }
+      $scope.render();
+    };
+
+    $scope.invertColorOrder = function() {
+      var tmp = $scope.panel.colors[0];
+      $scope.panel.colors[0] = $scope.panel.colors[2];
+      $scope.panel.colors[2] = tmp;
+      $scope.render();
+    };
+
+    $scope.getDecimalsForValue = function(value) {
+      if (_.isNumber($scope.panel.decimals)) {
+        return { decimals: $scope.panel.decimals, scaledDecimals: null };
+      }
+
+      var delta = value / 2;
+      var dec = -Math.floor(Math.log(delta) / Math.LN10);
+
+      var magn = Math.pow(10, -dec),
+          norm = delta / magn, // norm is between 1.0 and 10.0
+          size;
+
+      if (norm < 1.5) {
+        size = 1;
+      } else if (norm < 3) {
+        size = 2;
+        // special case for 2.5, requires an extra decimal
+        if (norm > 2.25) {
+          size = 2.5;
+          ++dec;
+        }
+      } else if (norm < 7.5) {
+        size = 5;
+      } else {
+        size = 10;
+      }
+
+      size *= magn;
+
+      // reduce starting decimals if not needed
+      if (Math.floor(value) === value) { dec = 0; }
+
+      var result = {};
+      result.decimals = Math.max(0, dec);
+      result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
+
+      return result;
+    };
+
+    $scope.render = function() {
+      var data = {};
+
+      $scope.setValues(data);
+
+      data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
+        return Number(strVale.trim());
+      });
+
+      data.colorMap = $scope.panel.colors;
+
+      $scope.data = data;
+      $scope.$broadcast('render');
+    };
+
+    $scope.setValues = function(data) {
+      data.flotpairs = [];
+
+      if($scope.series.length > 1) {
+        $scope.inspector.error = new Error();
+        $scope.inspector.error.message = 'Multiple Series Error';
+        $scope.inspector.error.data = 'Metric query returns ' + $scope.series.length +
+        ' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series);
+        throw $scope.inspector.error;
+      }
+
+      if ($scope.series && $scope.series.length > 0) {
+        var lastPoint = _.last($scope.series[0].datapoints);
+        var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
+
+        if (_.isString(lastValue)) {
+          data.value = 0;
+          data.valueFormated = lastValue;
+          data.valueRounded = 0;
+        } else {
+          data.value = $scope.series[0].stats[$scope.panel.valueName];
+          data.flotpairs = $scope.series[0].flotpairs;
+
+          var decimalInfo = $scope.getDecimalsForValue(data.value);
+          var formatFunc = kbn.valueFormats[$scope.panel.format];
+          data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
+          data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
+        }
+      }
+
+      // check value to text mappings
+      for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
+        var map = $scope.panel.valueMaps[i];
+        // special null case
+        if (map.value === 'null') {
+          if (data.value === null || data.value === void 0) {
+            data.valueFormated = map.text;
+            return;
+          }
+          continue;
+        }
+
+        // value/number to text mapping
+        var value = parseFloat(map.value);
+        if (value === data.value) {
+          data.valueFormated = map.text;
+          return;
+        }
+      }
+
+      if (data.value === null || data.value === void 0) {
+        data.valueFormated = "no value";
+      }
+    };
+
+    $scope.removeValueMap = function(map) {
+      var index = _.indexOf($scope.panel.valueMaps, map);
+      $scope.panel.valueMaps.splice(index, 1);
+      $scope.render();
+    };
+
+    $scope.addValueMap = function() {
+      $scope.panel.valueMaps.push({value: '', op: '=', text: '' });
+    };
+
+    $scope.init();
+  }
+
+  return SingleStatCtrl;
+});

+ 1 - 1
public/app/plugins/panel/singlestat/module.html

@@ -1,4 +1,4 @@
 <grafana-panel>
 <grafana-panel>
-	<div class="singlestat-panel" singlestat-panel></div>
+	<div class="singlestat-panel"></div>
 	<div class="clearfix"></div>
 	<div class="clearfix"></div>
 </grafana-panel>
 </grafana-panel>

+ 188 - 210
public/app/plugins/panel/singlestat/module.js

@@ -1,253 +1,231 @@
 define([
 define([
-  'angular',
-  'app/app',
+  './controller',
   'lodash',
   'lodash',
-  'app/core/utils/kbn',
-  'app/core/time_series',
-  'app/features/panel/panel_meta',
-  './singleStatPanel',
+  'jquery',
+  'jquery.flot',
 ],
 ],
-function (angular, app, _, kbn, TimeSeries, PanelMeta) {
+function (SingleStatCtrl, _, $) {
   'use strict';
   'use strict';
 
 
-  var module = angular.module('grafana.panels.singlestat');
-  app.useModule(module);
-
-  module.directive('grafanaPanelSinglestat', function() {
+  /** @ngInject */
+  function singleStatPanel($location, linkSrv, $timeout, templateSrv) {
     return {
     return {
-      controller: 'SingleStatCtrl',
+      controller: SingleStatCtrl,
       templateUrl: 'app/plugins/panel/singlestat/module.html',
       templateUrl: 'app/plugins/panel/singlestat/module.html',
-    };
-  });
-
-  module.controller('SingleStatCtrl', function($scope, panelSrv, panelHelper) {
-
-    $scope.panelMeta = new PanelMeta({
-      panelName: 'Singlestat',
-      editIcon:  "fa fa-dashboard",
-      fullscreen: true,
-      metricsEditor: true
-    });
-
-    $scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
-
-    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html');
-    $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
-
-    // Set and populate defaults
-    var _d = {
-      links: [],
-      datasource: null,
-      maxDataPoints: 100,
-      interval: null,
-      targets: [{}],
-      cacheTimeout: null,
-      format: 'none',
-      prefix: '',
-      postfix: '',
-      nullText: null,
-      valueMaps: [
-        { value: 'null', op: '=', text: 'N/A' }
-      ],
-      nullPointMode: 'connected',
-      valueName: 'avg',
-      prefixFontSize: '50%',
-      valueFontSize: '80%',
-      postfixFontSize: '50%',
-      thresholds: '',
-      colorBackground: false,
-      colorValue: false,
-      colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
-      sparkline: {
-        show: false,
-        full: false,
-        lineColor: 'rgb(31, 120, 193)',
-        fillColor: 'rgba(31, 118, 189, 0.18)',
-      }
-    };
+      link: function(scope, elem) {
+        var data, panel, linkInfo, $panelContainer;
+        var firstRender = true;
+
+        scope.$on('render', function() {
+          if (firstRender) {
+            var inner = elem.find('.singlestat-panel');
+            if (inner.length) {
+              elem = inner;
+              $panelContainer = elem.parents('.panel-container');
+              firstRender = false;
+            }
+          }
 
 
-    _.defaults($scope.panel, _d);
-    $scope.unitFormats = kbn.getUnitFormats();
+          render();
+          scope.panelRenderingComplete();
+        });
 
 
-    $scope.setUnitFormat = function(subItem) {
-      $scope.panel.format = subItem.value;
-      $scope.render();
-    };
+        function setElementHeight() {
+          try {
+            var height = scope.height || panel.height || scope.row.height;
+            if (_.isString(height)) {
+              height = parseInt(height.replace('px', ''), 10);
+            }
 
 
-    $scope.init = function() {
-      panelSrv.init($scope);
-    };
+            height -= 5; // padding
+            height -= panel.title ? 24 : 9; // subtract panel title bar
 
 
-    $scope.refreshData = function(datasource) {
-      panelHelper.updateTimeRange($scope);
+            elem.css('height', height + 'px');
 
 
-      return panelHelper.issueMetricQuery($scope, datasource)
-        .then($scope.dataHandler, function(err) {
-          $scope.series = [];
-          $scope.render();
-          throw err;
-        });
-    };
+            return true;
+          } catch(e) { // IE throws errors sometimes
+            return false;
+          }
+        }
 
 
-    $scope.loadSnapshot = function(snapshotData) {
-      panelHelper.updateTimeRange($scope);
-      $scope.dataHandler(snapshotData);
-    };
+        function applyColoringThresholds(value, valueString) {
+          if (!panel.colorValue) {
+            return valueString;
+          }
 
 
-    $scope.dataHandler = function(results) {
-      $scope.series = _.map(results.data, $scope.seriesHandler);
-      $scope.render();
-    };
+          var color = getColorForValue(value);
+          if (color) {
+            return '<span style="color:' + color + '">'+ valueString + '</span>';
+          }
 
 
-    $scope.seriesHandler = function(seriesData) {
-      var series = new TimeSeries({
-        datapoints: seriesData.datapoints,
-        alias: seriesData.target,
-      });
+          return valueString;
+        }
 
 
-      series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
+        function getColorForValue(value) {
+          for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
+            if (value >= data.thresholds[i]) {
+              return data.colorMap[i];
+            }
+          }
+          return null;
+        }
 
 
-      return series;
-    };
+        function getSpan(className, fontSize, value)  {
+          value = templateSrv.replace(value);
+          return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
+            value + '</span>';
+        }
 
 
-    $scope.setColoring = function(options) {
-      if (options.background) {
-        $scope.panel.colorValue = false;
-        $scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
-      }
-      else {
-        $scope.panel.colorBackground = false;
-        $scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
-      }
-      $scope.render();
-    };
+        function getBigValueHtml() {
+          var body = '<div class="singlestat-panel-value-container">';
 
 
-    $scope.invertColorOrder = function() {
-      var tmp = $scope.panel.colors[0];
-      $scope.panel.colors[0] = $scope.panel.colors[2];
-      $scope.panel.colors[2] = tmp;
-      $scope.render();
-    };
+          if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
 
 
-    $scope.getDecimalsForValue = function(value) {
-      if (_.isNumber($scope.panel.decimals)) {
-        return { decimals: $scope.panel.decimals, scaledDecimals: null };
-      }
+          var value = applyColoringThresholds(data.valueRounded, data.valueFormated);
+          body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
 
 
-      var delta = value / 2;
-      var dec = -Math.floor(Math.log(delta) / Math.LN10);
-
-      var magn = Math.pow(10, -dec),
-          norm = delta / magn, // norm is between 1.0 and 10.0
-          size;
-
-      if (norm < 1.5) {
-        size = 1;
-      } else if (norm < 3) {
-        size = 2;
-        // special case for 2.5, requires an extra decimal
-        if (norm > 2.25) {
-          size = 2.5;
-          ++dec;
-        }
-      } else if (norm < 7.5) {
-        size = 5;
-      } else {
-        size = 10;
-      }
+          if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
 
 
-      size *= magn;
+          body += '</div>';
 
 
-      // reduce starting decimals if not needed
-      if (Math.floor(value) === value) { dec = 0; }
+          return body;
+        }
 
 
-      var result = {};
-      result.decimals = Math.max(0, dec);
-      result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
+        function addSparkline() {
+          var panel = scope.panel;
+          var width = elem.width() + 20;
+          var height = elem.height() || 100;
+
+          var plotCanvas = $('<div></div>');
+          var plotCss = {};
+          plotCss.position = 'absolute';
+
+          if (panel.sparkline.full) {
+            plotCss.bottom = '5px';
+            plotCss.left = '-5px';
+            plotCss.width = (width - 10) + 'px';
+            var dynamicHeightMargin = height <= 100 ? 5 : (Math.round((height/100)) * 15) + 5;
+            plotCss.height = (height - dynamicHeightMargin) + 'px';
+          }
+          else {
+            plotCss.bottom = "0px";
+            plotCss.left = "-5px";
+            plotCss.width = (width - 10) + 'px';
+            plotCss.height = Math.floor(height * 0.25) + "px";
+          }
 
 
-      return result;
-    };
+          plotCanvas.css(plotCss);
+
+          var options = {
+            legend: { show: false },
+            series: {
+              lines:  {
+                show: true,
+                fill: 1,
+                lineWidth: 1,
+                fillColor: panel.sparkline.fillColor,
+              },
+            },
+            yaxes: { show: false },
+            xaxis: {
+              show: false,
+              mode: "time",
+              min: scope.range.from.valueOf(),
+              max: scope.range.to.valueOf(),
+            },
+            grid: { hoverable: false, show: false },
+          };
+
+          elem.append(plotCanvas);
+
+          var plotSeries = {
+            data: data.flotpairs,
+            color: panel.sparkline.lineColor
+          };
+
+          $.plot(plotCanvas, [plotSeries], options);
+        }
 
 
-    $scope.render = function() {
-      var data = {};
+        function render() {
+          if (!scope.data) { return; }
+
+          data = scope.data;
+          panel = scope.panel;
+
+          setElementHeight();
+
+          var body = getBigValueHtml();
+
+          if (panel.colorBackground && !isNaN(data.valueRounded)) {
+            var color = getColorForValue(data.valueRounded);
+            if (color) {
+              $panelContainer.css('background-color', color);
+              if (scope.fullscreen) {
+                elem.css('background-color', color);
+              } else {
+                elem.css('background-color', '');
+              }
+            }
+          } else {
+            $panelContainer.css('background-color', '');
+            elem.css('background-color', '');
+          }
 
 
-      $scope.setValues(data);
+          elem.html(body);
 
 
-      data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
-        return Number(strVale.trim());
-      });
+          if (panel.sparkline.show) {
+            addSparkline();
+          }
 
 
-      data.colorMap = $scope.panel.colors;
+          elem.toggleClass('pointer', panel.links.length > 0);
 
 
-      $scope.data = data;
-      $scope.$broadcast('render');
-    };
+          if (panel.links.length > 0) {
+            linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], scope.panel.scopedVars);
+          } else {
+            linkInfo = null;
+          }
+        }
 
 
-    $scope.setValues = function(data) {
-      data.flotpairs = [];
+        // drilldown link tooltip
+        var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"');
 
 
-      if($scope.series.length > 1) {
-        $scope.inspector.error = new Error();
-        $scope.inspector.error.message = 'Multiple Series Error';
-        $scope.inspector.error.data = 'Metric query returns ' + $scope.series.length +
-        ' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series);
-        throw $scope.inspector.error;
-      }
+        elem.mouseleave(function() {
+          if (panel.links.length === 0) { return;}
+          drilldownTooltip.detach();
+        });
 
 
-      if ($scope.series && $scope.series.length > 0) {
-        var lastPoint = _.last($scope.series[0].datapoints);
-        var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
-
-        if (_.isString(lastValue)) {
-          data.value = 0;
-          data.valueFormated = lastValue;
-          data.valueRounded = 0;
-        } else {
-          data.value = $scope.series[0].stats[$scope.panel.valueName];
-          data.flotpairs = $scope.series[0].flotpairs;
-
-          var decimalInfo = $scope.getDecimalsForValue(data.value);
-          var formatFunc = kbn.valueFormats[$scope.panel.format];
-          data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
-          data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
-        }
-      }
+        elem.click(function() {
+          if (!linkInfo) { return; }
 
 
-      // check value to text mappings
-      for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
-        var map = $scope.panel.valueMaps[i];
-        // special null case
-        if (map.value === 'null') {
-          if (data.value === null || data.value === void 0) {
-            data.valueFormated = map.text;
+          if (linkInfo.target === '_blank') {
+            var redirectWindow = window.open(linkInfo.href, '_blank');
+            redirectWindow.location;
             return;
             return;
           }
           }
-          continue;
-        }
 
 
-        // value/number to text mapping
-        var value = parseFloat(map.value);
-        if (value === data.value) {
-          data.valueFormated = map.text;
-          return;
-        }
-      }
+          if (linkInfo.href.indexOf('http') === 0) {
+            window.location.href = linkInfo.href;
+          } else {
+            $timeout(function() {
+              $location.url(linkInfo.href);
+            });
+          }
 
 
-      if (data.value === null || data.value === void 0) {
-        data.valueFormated = "no value";
-      }
-    };
+          drilldownTooltip.detach();
+        });
 
 
-    $scope.removeValueMap = function(map) {
-      var index = _.indexOf($scope.panel.valueMaps, map);
-      $scope.panel.valueMaps.splice(index, 1);
-      $scope.render();
-    };
+        elem.mousemove(function(e) {
+          if (!linkInfo) { return;}
 
 
-    $scope.addValueMap = function() {
-      $scope.panel.valueMaps.push({value: '', op: '=', text: '' });
+          drilldownTooltip.text('click to go to: ' + linkInfo.title);
+
+          drilldownTooltip.place_tt(e.pageX+20, e.pageY-15);
+        });
+      }
     };
     };
+  }
 
 
-    $scope.init();
-  });
+  return {
+    panel: singleStatPanel
+  };
 });
 });

+ 1 - 1
public/app/plugins/panel/table/controller.ts

@@ -20,7 +20,7 @@ export class TablePanelCtrl {
       metricsEditor: true,
       metricsEditor: true,
     });
     });
 
 
-    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/table/options.html');
+    $scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/table/options.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
     $scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
 
 
     var panelDefaults = {
     var panelDefaults = {

+ 1 - 1
public/app/plugins/panel/table/editor.ts

@@ -118,7 +118,7 @@ export function tablePanelEditor($q, uiSegmentSrv) {
   return {
   return {
     restrict: 'E',
     restrict: 'E',
     scope: true,
     scope: true,
-    templateUrl: 'app/plugins/panels/table/editor.html',
+    templateUrl: 'app/plugins/panel/table/editor.html',
     controller: TablePanelEditorCtrl,
     controller: TablePanelEditorCtrl,
   };
   };
 }
 }

+ 4 - 3
public/app/plugins/panel/table/module.ts

@@ -10,7 +10,9 @@ import {TablePanelCtrl} from './controller';
 import {TableRenderer} from './renderer';
 import {TableRenderer} from './renderer';
 import {tablePanelEditor} from './editor';
 import {tablePanelEditor} from './editor';
 
 
-export function tablePanel() {
+angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);
+
+function tablePanel() {
   'use strict';
   'use strict';
   return {
   return {
     restrict: 'E',
     restrict: 'E',
@@ -102,5 +104,4 @@ export function tablePanel() {
   };
   };
 }
 }
 
 
-angular.module('grafana.directives').directive('grafanaPanelTable', tablePanel);
-angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);
+export {tablePanel as panel};

+ 2 - 3
public/app/plugins/panel/table/renderer.ts

@@ -96,13 +96,12 @@ export class TableRenderer {
   }
   }
 
 
   renderCell(columnIndex, value, addWidthHack = false) {
   renderCell(columnIndex, value, addWidthHack = false) {
-    var value = this.formatColumnValue(columnIndex, value);
+    value = this.formatColumnValue(columnIndex, value);
     var style = '';
     var style = '';
     if (this.colorState.cell) {
     if (this.colorState.cell) {
       style = ' style="background-color:' + this.colorState.cell + ';color: white"';
       style = ' style="background-color:' + this.colorState.cell + ';color: white"';
       this.colorState.cell = null;
       this.colorState.cell = null;
-    }
-    else if (this.colorState.value) {
+    } else if (this.colorState.value) {
       style = ' style="color:' + this.colorState.value + '"';
       style = ' style="color:' + this.colorState.value + '"';
       this.colorState.value = null;
       this.colorState.value = null;
     }
     }

+ 1 - 2
public/app/plugins/panel/table/transformers.ts

@@ -52,8 +52,7 @@ transformers['timeseries_to_columns'] = {
         if (!points[timeKey]) {
         if (!points[timeKey]) {
           points[timeKey] = {time: dp[1]};
           points[timeKey] = {time: dp[1]};
           points[timeKey][i] = dp[0];
           points[timeKey][i] = dp[0];
-        }
-        else {
+        } else {
           points[timeKey][i] = dp[0];
           points[timeKey][i] = dp[0];
         }
         }
       }
       }

+ 13 - 12
public/app/plugins/panel/text/module.js

@@ -10,17 +10,7 @@ function (angular, app, _, require, PanelMeta) {
 
 
   var converter;
   var converter;
 
 
-  var module = angular.module('grafana.panels.text', []);
-  app.useModule(module);
-
-  module.directive('grafanaPanelText', function() {
-    return {
-      controller: 'TextPanelCtrl',
-      templateUrl: 'app/plugins/panel/text/module.html',
-    };
-  });
-
-  module.controller('TextPanelCtrl', function($scope, templateSrv, $sce, panelSrv) {
+  function TextPanelCtrl($scope, templateSrv, $sce, panelSrv) {
 
 
     $scope.panelMeta = new PanelMeta({
     $scope.panelMeta = new PanelMeta({
       panelName: 'Text',
       panelName: 'Text',
@@ -107,5 +97,16 @@ function (angular, app, _, require, PanelMeta) {
     };
     };
 
 
     $scope.init();
     $scope.init();
-  });
+  }
+
+  function textPanel() {
+    return {
+      controller: TextPanelCtrl,
+      templateUrl: 'app/plugins/panel/text/module.html',
+    };
+  }
+
+  return {
+    panel: textPanel,
+  };
 });
 });

+ 46 - 2
public/app/plugins/plugin_api.md

@@ -1,8 +1,52 @@
 # Plugin API
 # Plugin API
 
 
-## Changelog
+### 3.0 changes to plugin api changes
 
 
-2.5.1
+There has been big changes to both data source and plugin schema (plugin.json) and how
+you write the plugin main module.
+
+#### Datasource plugin
+
+Now data source plugins AMD/SystemJS module should return:
+
+```javascript
+return {
+  Datasource: ElasticDatasource,
+  configView: editView.default,
+  annotationsQueryEditor: annotationsQueryEditor,
+  metricsQueryEditor: metricsQueryEditor,
+  metricsQueryOptions: metricsQueryOptions,
+};
+```
+
+Where ElasticDatasource is a constructor function to a javascript. The constructor
+function can take angular services and `instanceSettings` as parameters.
+
+Example:
+
+```javascript
+function ElasticDatasource(instanceSettings, templateSrv) {
+  this.instanceSettings = this.instanceSettings;
+  ///...
+};
+```
+
+A datasource module can optionally return a configView directive function, metricsQueryEditor directive function, etc.
+
+Example:
+
+```javascript
+function metricsQueryEditor() {
+  return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
+}
+```
+
+#### Panel plugin
+
+The panel plugin AMD/SystemJS module should return an object with a property named `panel`. This needs to be
+a directive function.
+
+### 2.5.1 changes
 datasource annotationQuery changed. now single options parameter with:
 datasource annotationQuery changed. now single options parameter with:
 - range
 - range
 - rangeRaw
 - rangeRaw

+ 0 - 51
public/test/specs/graph-ctrl-specs.js

@@ -1,51 +0,0 @@
-define([
-  './helpers',
-  'app/features/panel/panel_srv',
-  'app/features/panel/panel_helper',
-  'app/plugins/panel/graph/module'
-], function(helpers) {
-  'use strict';
-
-  describe('GraphCtrl', function() {
-    var ctx = new helpers.ControllerTestContext();
-
-    beforeEach(module('grafana.services'));
-    beforeEach(module('grafana.panels.graph'));
-
-    beforeEach(ctx.providePhase());
-    beforeEach(ctx.createControllerPhase('GraphCtrl'));
-
-    describe('get_data with 2 series', function() {
-      beforeEach(function() {
-        ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([]));
-        ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
-          data: [
-            { target: 'test.cpu1', datapoints: [[1, 10]]},
-            { target: 'test.cpu2', datapoints: [[1, 10]]}
-          ]
-        }));
-        ctx.scope.render = sinon.spy();
-        ctx.scope.refreshData(ctx.datasource);
-        ctx.scope.$digest();
-      });
-
-      it('should send time series to render', function() {
-        var data = ctx.scope.render.getCall(0).args[0];
-        expect(data.length).to.be(2);
-      });
-
-      describe('get_data failure following success', function() {
-        beforeEach(function() {
-          ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
-          ctx.scope.refreshData(ctx.datasource);
-          ctx.scope.$digest();
-        });
-
-      });
-
-    });
-
-  });
-
-});
-

+ 0 - 231
public/test/specs/graph-specs.js

@@ -1,231 +0,0 @@
-define([
-  './helpers',
-  'angular',
-  'jquery',
-  'app/core/time_series',
-  'app/plugins/panel/graph/graph'
-], function(helpers, angular, $, TimeSeries) {
-  'use strict';
-
-  describe('grafanaGraph', function() {
-
-    beforeEach(module('grafana.directives'));
-
-    function graphScenario(desc, func)  {
-      describe(desc, function() {
-        var ctx = {};
-
-        ctx.setup = function (setupFunc) {
-
-          beforeEach(module(function($provide) {
-            $provide.value("timeSrv", new helpers.TimeSrvStub());
-          }));
-
-          beforeEach(inject(function($rootScope, $compile) {
-            var scope = $rootScope.$new();
-            var element = angular.element("<div style='width:500px' grafana-graph><div>");
-
-            scope.height = '200px';
-            scope.panel = {
-              legend: {},
-              grid: { },
-              y_formats: [],
-              seriesOverrides: [],
-              tooltip: {
-                shared: true
-              }
-            };
-
-            scope.panelRenderingComplete = sinon.spy();
-            scope.appEvent = sinon.spy();
-            scope.onAppEvent = sinon.spy();
-            scope.hiddenSeries = {};
-            scope.dashboard = { timezone: 'browser' };
-            scope.range = {
-              from: new Date('2014-08-09 10:00:00'),
-              to: new Date('2014-09-09 13:00:00')
-            };
-            ctx.data = [];
-            ctx.data.push(new TimeSeries({
-              datapoints: [[1,1],[2,2]],
-              alias: 'series1'
-            }));
-            ctx.data.push(new TimeSeries({
-              datapoints: [[1,1],[2,2]],
-              alias: 'series2'
-            }));
-
-            setupFunc(scope, ctx.data);
-
-            $compile(element)(scope);
-            scope.$digest();
-            $.plot = ctx.plotSpy = sinon.spy();
-
-            scope.$emit('render', ctx.data);
-            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
-            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
-          }));
-        };
-
-        func(ctx);
-      });
-    }
-
-    graphScenario('simple lines options', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.lines = true;
-        scope.panel.fill = 5;
-        scope.panel.linewidth = 3;
-        scope.panel.steppedLine = true;
-      });
-
-      it('should configure plot with correct options', function() {
-        expect(ctx.plotOptions.series.lines.show).to.be(true);
-        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-        expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
-        expect(ctx.plotOptions.series.lines.steps).to.be(true);
-      });
-    });
-
-    graphScenario('grid thresholds 100, 200', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.grid = {
-          threshold1: 100,
-          threshold1Color: "#111",
-          threshold2: 200,
-          threshold2Color: "#222",
-        };
-      });
-
-      it('should add grid markings', function() {
-        var markings = ctx.plotOptions.grid.markings;
-        expect(markings[0].yaxis.from).to.be(100);
-        expect(markings[0].yaxis.to).to.be(200);
-        expect(markings[0].color).to.be('#111');
-        expect(markings[1].yaxis.from).to.be(200);
-        expect(markings[1].yaxis.to).to.be(Infinity);
-      });
-    });
-
-    graphScenario('inverted grid thresholds 200, 100', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.grid = {
-          threshold1: 200,
-          threshold1Color: "#111",
-          threshold2: 100,
-          threshold2Color: "#222",
-        };
-      });
-
-      it('should add grid markings', function() {
-        var markings = ctx.plotOptions.grid.markings;
-        expect(markings[0].yaxis.from).to.be(200);
-        expect(markings[0].yaxis.to).to.be(100);
-        expect(markings[0].color).to.be('#111');
-        expect(markings[1].yaxis.from).to.be(100);
-        expect(markings[1].yaxis.to).to.be(-Infinity);
-      });
-    });
-
-    graphScenario('grid thresholds from zero', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.grid = {
-          threshold1: 0,
-          threshold1Color: "#111",
-        };
-      });
-
-      it('should add grid markings', function() {
-        var markings = ctx.plotOptions.grid.markings;
-        expect(markings[0].yaxis.from).to.be(0);
-      });
-    });
-
-    graphScenario('when logBase is log 10', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.grid = {
-          leftMax: null,
-          rightMax: null,
-          leftMin: null,
-          rightMin: null,
-          leftLogBase: 10,
-        };
-      });
-
-      it('should apply axis transform and ticks', function() {
-        var axis = ctx.plotOptions.yaxes[0];
-        expect(axis.transform(100)).to.be(Math.log(100+0.1));
-        expect(axis.ticks[0]).to.be(0);
-        expect(axis.ticks[1]).to.be(1);
-      });
-    });
-
-    graphScenario('should use timeStep for barWidth', function(ctx) {
-      ctx.setup(function(scope, data) {
-        scope.panel.bars = true;
-        data[0] = new TimeSeries({
-          datapoints: [[1,10],[2,20]],
-          alias: 'series1',
-        });
-      });
-
-      it('should set barWidth', function() {
-        expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
-      });
-    });
-
-    graphScenario('series option overrides, fill & points', function(ctx) {
-      ctx.setup(function(scope, data) {
-        scope.panel.lines = true;
-        scope.panel.fill = 5;
-        scope.panel.seriesOverrides = [
-          { alias: 'test', fill: 0, points: true }
-        ];
-
-        data[1].alias = 'test';
-      });
-
-      it('should match second series and fill zero, and enable points', function() {
-        expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-        expect(ctx.plotData[1].lines.fill).to.be(0.001);
-        expect(ctx.plotData[1].points.show).to.be(true);
-      });
-    });
-
-    graphScenario('should order series order according to zindex', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
-      });
-
-      it('should move zindex 2 last', function() {
-        expect(ctx.plotData[0].alias).to.be('series2');
-        expect(ctx.plotData[1].alias).to.be('series1');
-      });
-    });
-
-    graphScenario('when series is hidden', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.hiddenSeries = {'series2': true};
-      });
-
-      it('should remove datapoints and disable stack', function() {
-        expect(ctx.plotData[0].alias).to.be('series1');
-        expect(ctx.plotData[1].data.length).to.be(0);
-        expect(ctx.plotData[1].stack).to.be(false);
-      });
-    });
-
-    graphScenario('when stack and percent', function(ctx) {
-      ctx.setup(function(scope) {
-        scope.panel.percentage = true;
-        scope.panel.stack = true;
-      });
-
-      it('should show percentage', function() {
-        var axis = ctx.plotOptions.yaxes[0];
-        expect(axis.tickFormatter(100, axis)).to.be("100%");
-      });
-    });
-  });
-});
-

+ 0 - 171
public/test/specs/graph-tooltip-specs.js

@@ -1,171 +0,0 @@
-define([
-  'jquery',
-  'app/plugins/panel/graph/graph.tooltip'
-], function($, GraphTooltip) {
-  'use strict';
-
-  var scope =  {
-    appEvent: sinon.spy(),
-    onAppEvent: sinon.spy(),
-  };
-
-  var elem = $('<div></div>');
-  var dashboard = { };
-
-  function describeSharedTooltip(desc, fn) {
-    var ctx = {};
-    ctx.scope = scope;
-    ctx.scope.panel =  {
-      tooltip:  {
-        shared: true
-      },
-      legend: { },
-      stack: false
-    };
-
-    ctx.setup = function(setupFn) {
-      ctx.setupFn = setupFn;
-    };
-
-    describe(desc, function() {
-      beforeEach(function() {
-        ctx.setupFn();
-        var tooltip = new GraphTooltip(elem, dashboard, scope);
-        ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
-      });
-
-      fn(ctx);
-    });
-  }
-
-  describeSharedTooltip("steppedLine false, stack false", function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [
-        { data: [[10, 15], [12, 20]], lines: {} },
-        { data: [[10, 2], [12, 3]], lines: {} }
-      ];
-      ctx.pos = { x: 11 };
-    });
-
-    it('should return 2 series', function() {
-      expect(ctx.results.length).to.be(2);
-    });
-    it('should add time to results array', function() {
-      expect(ctx.results.time).to.be(10);
-    });
-    it('should set value and hoverIndex', function() {
-      expect(ctx.results[0].value).to.be(15);
-      expect(ctx.results[1].value).to.be(2);
-      expect(ctx.results[0].hoverIndex).to.be(0);
-    });
-  });
-
-  describeSharedTooltip("one series is hidden", function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [
-        { data: [[10, 15], [12, 20]], },
-        { data: [] }
-      ];
-      ctx.pos = { x: 11 };
-    });
-  });
-
-  describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [
-        {
-          data: [[10, 15], [12, 20]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10,15], [12,20]],
-          },
-          stack: true,
-        },
-        {
-          data: [[10, 2], [12, 3]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10, 2], [12, 3]],
-          },
-          stack: true
-        }
-      ];
-      ctx.scope.panel.stack = true;
-      ctx.pos = { x: 11 };
-    });
-
-    it('should show stacked value', function() {
-      expect(ctx.results[1].value).to.be(17);
-    });
-  });
-
-  describeSharedTooltip("steppedLine false, stack true, individual false, series stack false", function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [
-        {
-          data: [[10, 15], [12, 20]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10, 15], [12, 20]],
-          },
-          stack: true
-        },
-        {
-          data: [[10, 2], [12, 3]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10, 2], [12, 3]],
-          },
-          stack: false
-        }
-      ];
-      ctx.scope.panel.stack = true;
-      ctx.pos = { x: 11 };
-    });
-
-    it('should not show stacked value', function() {
-      expect(ctx.results[1].value).to.be(2);
-    });
-
-  });
-
-  describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
-    ctx.setup(function() {
-      ctx.data = [
-        {
-          data: [[10, 15], [12, 20]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10, 15], [12, 20]],
-          },
-          stack: true
-        },
-        {
-          data: [[10, 2], [12, 3]],
-          lines: {},
-          datapoints: {
-            pointsize: 2,
-            points: [[10, 2], [12, 3]],
-          },
-          stack: false
-        }
-      ];
-      ctx.scope.panel.stack = true;
-      ctx.scope.panel.tooltip.value_type = 'individual';
-      ctx.pos = { x: 11 };
-    });
-
-    it('should not show stacked value', function() {
-      expect(ctx.results[1].value).to.be(2);
-    });
-
-  });
-
-});
-
-

+ 2 - 4
public/test/specs/helpers.d.ts

@@ -1,6 +1,4 @@
-declare module "test/specs/helpers" {
-  let helpers: any;
-  export default helpers;
-}
+declare let helpers: any;
+export default helpers;
 
 
 
 

+ 7 - 4
public/test/specs/singlestat-specs.js

@@ -1,11 +1,14 @@
 define([
 define([
+  'angular',
   './helpers',
   './helpers',
+  'app/plugins/panel/singlestat/controller',
   'app/features/panel/panel_srv',
   'app/features/panel/panel_srv',
   'app/features/panel/panel_helper',
   'app/features/panel/panel_helper',
-  'app/plugins/panel/singlestat/module'
-], function(helpers) {
+], function(angular, helpers, SingleStatCtrl) {
   'use strict';
   'use strict';
 
 
+  angular.module('grafana.controllers').controller('SingleStatCtrl', SingleStatCtrl);
+
   describe('SingleStatCtrl', function() {
   describe('SingleStatCtrl', function() {
     var ctx = new helpers.ControllerTestContext();
     var ctx = new helpers.ControllerTestContext();
 
 
@@ -16,7 +19,7 @@ define([
         ctx.setup = function (setupFunc) {
         ctx.setup = function (setupFunc) {
 
 
           beforeEach(module('grafana.services'));
           beforeEach(module('grafana.services'));
-          beforeEach(module('grafana.panels.singlestat'));
+          beforeEach(module('grafana.controllers'));
 
 
           beforeEach(ctx.providePhase());
           beforeEach(ctx.providePhase());
           beforeEach(ctx.createControllerPhase('SingleStatCtrl'));
           beforeEach(ctx.createControllerPhase('SingleStatCtrl'));
@@ -24,7 +27,7 @@ define([
           beforeEach(function() {
           beforeEach(function() {
             setupFunc();
             setupFunc();
             ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
             ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
-              data: [ { target: 'test.cpu1', datapoints: ctx.datapoints } ]
+              data: [{target: 'test.cpu1', datapoints: ctx.datapoints}]
             }));
             }));
 
 
             ctx.scope.refreshData(ctx.datasource);
             ctx.scope.refreshData(ctx.datasource);

+ 13 - 12
tasks/options/tslint.js

@@ -6,18 +6,19 @@ module.exports = function(config) {
       }
       }
     },
     },
     options: {
     options: {
-      configuration: {
-        rules: {
-          curly: true,
-          align: [true, "parameters", "statements"],
-          indent: [true, "spaces"],
-          "class-name": true,
-          "interface-name": true,
-          "semicolon": true,
-          "use-strict": [false, "check-module", "check-function"],
-          "whitespace": [true, "check-branch", "check-decl", "check-type"],
-        }
-      }
+      configuration: 'tslint.json'
+      //   {
+      //   rules: {
+      //     curly: true,
+      //     align: [true, "parameters", "statements"],
+      //     indent: [true, "spaces"],
+      //     "class-name": true,
+      //     "interface-name": true,
+      //     "semicolon": true,
+      //     "use-strict": [false, "check-module", "check-function"],
+      //     "whitespace": [true, "check-branch", "check-decl", "check-type"],
+      //   }
+      // }
     }
     }
   };
   };
 };
 };

+ 5 - 3
tasks/options/watch.js

@@ -27,15 +27,17 @@ module.exports = function(config, grunt) {
     }
     }
 
 
     if (/(\.ts)$/.test(filepath)) {
     if (/(\.ts)$/.test(filepath)) {
+      newPath = filepath.replace(/^public/, 'public_gen');
+      grunt.log.writeln('Copying to ' + newPath);
+      grunt.file.copy(filepath, newPath);
+
+      // copy ts file also used by source maps
       //changes changed file source to that of the changed file
       //changes changed file source to that of the changed file
       var option = 'typescript.build.src';
       var option = 'typescript.build.src';
       var result = filepath;
       var result = filepath;
       grunt.config(option, result);
       grunt.config(option, result);
       grunt.task.run('typescript:build');
       grunt.task.run('typescript:build');
       grunt.task.run('tslint');
       grunt.task.run('tslint');
-      // copy ts file also used by source maps
-      newPath = filepath.replace(/^public/, 'public_gen');
-      grunt.file.copy(filepath, newPath);
     }
     }
   });
   });
 
 

+ 62 - 0
tslint.json

@@ -0,0 +1,62 @@
+{
+  "rules": {
+    "class-name": true,
+    "comment-format": [true, "check-space"],
+    "curly": true,
+    "eofline": true,
+    "forin": false,
+    "indent": [true, "spaces"],
+    "label-position": true,
+    "label-undefined": true,
+    "max-line-length": [true, 140],
+    "member-access": false,
+    "no-arg": true,
+    "no-bitwise": true,
+    "no-console": [true,
+      "debug",
+      "info",
+      "time",
+      "timeEnd",
+      "trace"
+    ],
+    "no-construct": true,
+    "no-debugger": true,
+    "no-duplicate-key": true,
+    "no-duplicate-variable": true,
+    "no-empty": true,
+    "no-eval": true,
+    "no-inferrable-types": true,
+    "no-shadowed-variable": false,
+    "no-string-literal": false,
+    "no-switch-case-fall-through": true,
+    "no-trailing-comma": true,
+    "no-trailing-whitespace": true,
+    "no-unused-expression": false,
+    "no-unused-variable": false,
+    "no-unreachable": true,
+    "no-use-before-declare": true,
+    "no-var-keyword": false,
+    "object-literal-sort-keys": false,
+    "one-line": [true,
+      "check-open-brace",
+      "check-catch",
+      "check-else"
+    ],
+    "radix": false,
+    "semicolon": true,
+    "triple-equals": [true, "allow-null-check"],
+    "typedef-whitespace": [true, {
+      "call-signature": "nospace",
+      "index-signature": "nospace",
+      "parameter": "nospace",
+      "property-declaration": "nospace",
+      "variable-declaration": "nospace"
+    }],
+    "variable-name": [true, "ban-keywords"],
+    "whitespace": [true,
+      "check-branch",
+      "check-decl",
+      "check-type"
+    ]
+  }
+}