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

Merge branch 'develop' of github.com:grafana/grafana into develop

Torkel Ödegaard 8 лет назад
Родитель
Сommit
9358d0c6f7
53 измененных файлов с 981 добавлено и 1566 удалено
  1. 5 2
      package.json
  2. 8 6
      pkg/api/dashboard.go
  3. 58 34
      pkg/services/sqlstore/dashboard.go
  4. 3 1
      pkg/services/sqlstore/dashboard_test.go
  5. 3 2
      pkg/services/sqlstore/datasource_test.go
  6. 2 2
      public/app/core/directives/dash_class.js
  7. 1 1
      public/app/core/directives/dash_edit_link.js
  8. 46 34
      public/app/core/services/backend_srv.ts
  9. 57 54
      public/app/features/dashboard/DashboardModel.ts
  10. 74 0
      public/app/features/dashboard/PanelModel.ts
  11. 3 2
      public/app/features/dashboard/all.js
  12. 127 127
      public/app/features/dashboard/dashboard_ctrl.ts
  13. 1 3
      public/app/features/dashboard/dashboard_srv.ts
  14. 131 0
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  15. 40 0
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  16. 8 0
      public/app/features/dashboard/dashgrid/PanelContainer.ts
  17. 34 0
      public/app/features/dashboard/dashgrid/PanelLoader.ts
  18. 0 211
      public/app/features/dashboard/dashgrid/dashgrid.ts
  19. 0 232
      public/app/features/dashboard/dashgrid/ref.txt
  20. 1 1
      public/app/features/dashboard/dashnav/dashnav.html
  21. 15 7
      public/app/features/dashboard/dashnav/dashnav.ts
  22. 0 95
      public/app/features/dashboard/graphiteImportCtrl.js
  23. 1 1
      public/app/features/dashboard/history/history.ts
  24. 1 3
      public/app/features/dashboard/history/history_srv.ts
  25. 13 13
      public/app/features/dashboard/partials/settings.html
  26. 3 5
      public/app/features/dashboard/save_as_modal.ts
  27. 1 1
      public/app/features/dashboard/specs/dashboard_model_specs.ts
  28. 1 1
      public/app/features/dashboard/specs/exporter_specs.ts
  29. 6 12
      public/app/features/dashboard/viewStateSrv.js
  30. 1 3
      public/app/features/panel/metrics_tab.ts
  31. 12 18
      public/app/features/panel/panel_ctrl.ts
  32. 1 0
      public/app/features/panel/panel_directive.ts
  33. 5 5
      public/app/features/panel/panel_header.ts
  34. 1 1
      public/app/features/plugins/plugin_component.ts
  35. 6 4
      public/app/partials/dashboard.html
  36. 2 2
      public/app/plugins/panel/graph/specs/graph_specs.ts
  37. 19 13
      public/dashboards/home.json
  38. 5 4
      public/sass/_grafana.scss
  39. 3 3
      public/sass/_variables.dark.scss
  40. 3 3
      public/sass/_variables.light.scss
  41. 2 2
      public/sass/_variables.scss
  42. 24 0
      public/sass/components/_dashboard_grid.scss
  43. 0 325
      public/sass/components/_gridstack.scss
  44. 1 1
      public/sass/components/_modals.scss
  45. 1 1
      public/sass/components/_navbar.scss
  46. 1 1
      public/sass/components/_submenu.scss
  47. 2 0
      public/sass/pages/_dashboard.scss
  48. 0 4
      public/test/specs/helpers.d.ts
  49. 0 180
      public/test/specs/helpers.js
  50. 195 0
      public/test/specs/helpers.ts
  51. 0 130
      public/test/test-main.js
  52. 1 1
      scripts/webpack/webpack.dev.js
  53. 53 15
      yarn.lock

+ 5 - 2
package.json

@@ -110,18 +110,21 @@
     "angular-sanitize": "^1.6.6",
     "babel-polyfill": "^6.26.0",
     "brace": "^0.10.0",
+    "classnames": "^2.2.5",
     "clipboard": "^1.7.1",
     "eventemitter3": "^2.0.2",
-    "gridstack": "https://github.com/grafana/gridstack.js#grafana",
-    "gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
     "file-saver": "^1.3.3",
+    "gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "ngreact": "^0.4.1",
+    "prop-types": "^15.6.0",
     "react": "^16.0.0",
     "react-dom": "^16.0.0",
+    "react-grid-layout": "^0.16.0",
+    "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",
     "rxjs": "^5.4.3",
     "tether": "^1.4.0",

+ 8 - 6
pkg/api/dashboard.go

@@ -278,12 +278,14 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 	panels := dash.Get("panels").MustArray()
 
 	newpanel := simplejson.NewFromAny(map[string]interface{}{
-		"type":   "gettingstarted",
-		"id":     123123,
-		"x":      0,
-		"y":      3,
-		"width":  12,
-		"height": 4,
+		"type": "gettingstarted",
+		"id":   123123,
+		"gridPos": map[string]interface{}{
+			"x": 0,
+			"y": 3,
+			"w": 12,
+			"h": 4,
+		},
 	})
 
 	panels = append(panels, newpanel)

+ 58 - 34
pkg/services/sqlstore/dashboard.go

@@ -189,14 +189,13 @@ type DashboardSearchProjection struct {
 }
 
 func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
+	var sql bytes.Buffer
+	params := make([]interface{}, 0)
 	limit := query.Limit
 	if limit == 0 {
 		limit = 1000
 	}
 
-	var sql bytes.Buffer
-	params := make([]interface{}, 0)
-
 	sql.WriteString(`
 	SELECT
 		dashboard.id,
@@ -207,36 +206,69 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 		dashboard.folder_id,
 		folder.slug as folder_slug,
 		folder.title as folder_title
-	FROM (
+	FROM `)
+
+	// add tags filter
+	if len(query.Tags) > 0 {
+		sql.WriteString(
+			`(
 		SELECT
 			dashboard.id FROM dashboard
 			LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
-	`)
+		`)
+		if query.IsStarred {
+			sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
+		}
 
-	// add tags filter
-	if len(query.Tags) > 0 {
-		sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `)`)
+		sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `) AND `)
 		for _, tag := range query.Tags {
 			params = append(params, tag)
 		}
-	}
+		params = createSearchWhereClause(query, &sql, params)
+		fmt.Printf("params2 %v", params)
+
+		// this ends the inner select (tag filtered part)
+		sql.WriteString(`
+			GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
+			LIMIT ?) as ids
+			INNER JOIN dashboard on ids.id = dashboard.id
+		`)
+
+		params = append(params, len(query.Tags))
+		params = append(params, limit)
+	} else {
+		sql.WriteString(`( SELECT dashboard.id FROM dashboard `)
+		if query.IsStarred {
+			sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
+		}
+		sql.WriteString(` WHERE `)
+		params = createSearchWhereClause(query, &sql, params)
 
-	// this ends the inner select (tag filtered part)
-	sql.WriteString(`
-		  GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
-		  ORDER BY dashboard.title ASC LIMIT ?) as ids`)
-	params = append(params, len(query.Tags))
-	params = append(params, limit)
+		sql.WriteString(`
+			LIMIT ?) as ids
+		INNER JOIN dashboard on ids.id = dashboard.id
+		`)
+		params = append(params, limit)
+	}
 
 	sql.WriteString(`
-		INNER JOIN dashboard on ids.id = dashboard.id
 		LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
 		LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
-	if query.IsStarred {
-		sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
+
+	sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 5000"))
+
+	var res []DashboardSearchProjection
+
+	err := x.Sql(sql.String(), params...).Find(&res)
+	if err != nil {
+		return nil, err
 	}
 
-	sql.WriteString(` WHERE dashboard.org_id=?`)
+	return res, nil
+}
+
+func createSearchWhereClause(query *search.FindPersistedDashboardsQuery, sql *bytes.Buffer, params []interface{}) []interface{} {
+	sql.WriteString(` dashboard.org_id=?`)
 	params = append(params, query.SignedInUser.OrgId)
 
 	if query.IsStarred {
@@ -253,16 +285,17 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 
 	if query.SignedInUser.OrgRole != m.ROLE_ADMIN {
 		allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
-		SELECT distinct d.id AS DashboardId
+			SELECT distinct d.id AS DashboardId
 			FROM dashboard AS d
-	      LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
-	      LEFT JOIN user_group_member as ugm on ugm.user_group_id =  da.user_group_id
-	      LEFT JOIN org_user ou on ou.role = da.role
+	      		LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
+	      		LEFT JOIN user_group_member as ugm on ugm.user_group_id =  da.user_group_id
+	      		LEFT JOIN org_user ou on ou.role = da.role
 			WHERE
 			  d.has_acl = 1 and
 				(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
 			  and d.org_id = ?
-			  ))`
+			)
+		)`
 
 		sql.WriteString(allowedDashboardsSubQuery)
 		params = append(params, query.SignedInUser.UserId, query.SignedInUser.UserId, query.SignedInUser.OrgId)
@@ -286,16 +319,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
 		params = append(params, query.FolderId)
 	}
 
-	sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
-
-	var res []DashboardSearchProjection
-
-	err := x.Sql(sql.String(), params...).Find(&res)
-	if err != nil {
-		return nil, err
-	}
-
-	return res, nil
+	return params
 }
 
 func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {

+ 3 - 1
pkg/services/sqlstore/dashboard_test.go

@@ -3,6 +3,7 @@ package sqlstore
 import (
 	"testing"
 
+	"github.com/go-xorm/xorm"
 	. "github.com/smartystreets/goconvey/convey"
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -12,9 +13,10 @@ import (
 )
 
 func TestDashboardDataAccess(t *testing.T) {
+	var x *xorm.Engine
 
 	Convey("Testing DB", t, func() {
-		InitTestDB(t)
+		x = InitTestDB(t)
 
 		Convey("Given saved dashboard", func() {
 			savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")

+ 3 - 2
pkg/services/sqlstore/datasource_test.go

@@ -11,11 +11,10 @@ import (
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 )
 
-func InitTestDB(t *testing.T) {
+func InitTestDB(t *testing.T) *xorm.Engine {
 	x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
 	//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
 	//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
-	// x.ShowSQL()
 
 	// x.ShowSQL()
 
@@ -28,6 +27,8 @@ func InitTestDB(t *testing.T) {
 	if err := SetEngine(x); err != nil {
 		t.Fatal(err)
 	}
+
+	return x
 }
 
 type Test struct {

+ 2 - 2
public/app/core/directives/dash_class.js

@@ -19,7 +19,7 @@ function (_, $, coreModule) {
         });
 
         var lastHideControlsVal;
-        $scope.$watch('dashboard.hideControls', function() {
+        $scope.$watch('ctrl.dashboard.hideControls', function() {
           if (!$scope.dashboard) {
             return;
           }
@@ -31,7 +31,7 @@ function (_, $, coreModule) {
           }
         });
 
-        $scope.$watch('playlistSrv', function(newValue) {
+        $scope.$watch('ctrl.playlistSrv', function(newValue) {
           elem.toggleClass('playlist-active', _.isObject(newValue));
         });
       }

+ 1 - 1
public/app/core/directives/dash_edit_link.js

@@ -123,7 +123,7 @@ function ($, angular, coreModule, _) {
           }, 10);
         }
 
-        scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) {
+        scope.$watch("ctrl.dashboardViewState.state.editview", function(newValue, oldValue) {
           if (newValue) {
             showEditorPane(null, {editview: newValue});
           } else if (oldValue) {

+ 46 - 34
public/app/core/services/backend_srv.ts

@@ -235,42 +235,54 @@ export class BackendSrv {
 
   createDashboardFolder(name) {
     const dash = {
+      schemaVersion: 16,
       title: name,
       editable: true,
       hideControls: true,
-      rows: [
+      panels: [
         {
-          panels: [
-            {
-              folderId: 0,
-              headings: false,
-              limit: 1000,
-              links: [],
-              query: '',
-              recent: false,
-              search: true,
-              span: 4,
-              starred: false,
-              tags: [],
-              title: 'Dashboards in this folder',
-              type: 'dashlist'
-            },
-            {
-              onlyAlertsOnDashboard: true,
-              span: 4,
-              title: 'Alerts in this folder',
-              type: 'alertlist'
-            },
-            {
-              span: 4,
-              title: 'Permissions for this folder',
-              type: 'permissionlist',
-              folderId: 0
-            }
-          ],
-          showTitle: true,
-          title: name,
-          titleSize: 'h1'
+          id: 1,
+          folderId: 0,
+          headings: false,
+          limit: 1000,
+          links: [],
+          query: '',
+          recent: false,
+          search: true,
+          starred: false,
+          tags: [],
+          title: 'Dashboards in this folder',
+          type: 'dashlist',
+          gridPos: {
+            x: 0,
+            y: 0,
+            w: 4,
+            h: 10
+          }
+        },
+        {
+          id: 2,
+          onlyAlertsOnDashboard: true,
+          title: 'Alerts in this folder',
+          type: 'alertlist',
+          gridPos: {
+            x: 4,
+            y: 0,
+            w: 4,
+            h: 10
+          }
+        },
+        {
+          id: 3,
+          title: 'Permissions for this folder',
+          type: 'permissionlist',
+          folderId: 0,
+          gridPos: {
+            x: 8,
+            y: 0,
+            w: 4,
+            h: 10
+          }
         }
       ]
     };
@@ -280,8 +292,8 @@ export class BackendSrv {
       return this.getDashboard('db', res.slug);
     })
     .then(res => {
-      res.dashboard.rows[0].panels[0].folderId = res.dashboard.id;
-      res.dashboard.rows[0].panels[2].folderId = res.dashboard.id;
+      res.dashboard.panels[0].folderId = res.dashboard.id;
+      res.dashboard.panels[2].folderId = res.dashboard.id;
       return this.saveDashboard(res.dashboard, {overwrite: false});
     });
   }

+ 57 - 54
public/app/features/dashboard/model.ts → public/app/features/dashboard/DashboardModel.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 import angular from 'angular';
 import moment from 'moment';
 import _ from 'lodash';
@@ -8,20 +6,11 @@ import $ from 'jquery';
 import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
 import {Emitter, contextSrv, appEvents} from 'app/core/core';
 import {DashboardRow} from './row/row_model';
+import {PanelModel} from './PanelModel';
 import sortByKeys from 'app/core/utils/sort_by_keys';
 
-export interface Panel {
-  id: number;
-  x: number;
-  y: number;
-  width: number;
-  height: number;
-  type: string;
-  title: string;
-}
-
 export const CELL_HEIGHT = 30;
-export const CELL_VMARGIN = 15;
+export const CELL_VMARGIN = 10;
 
 export class DashboardModel {
   id: any;
@@ -50,7 +39,7 @@ export class DashboardModel {
   events: any;
   editMode: boolean;
   folderId: number;
-  panels: Panel[];
+  panels: PanelModel[];
 
   constructor(data, meta?) {
     if (!data) {
@@ -80,8 +69,7 @@ export class DashboardModel {
     this.links = data.links || [];
     this.gnetId = data.gnetId || null;
     this.folderId = data.folderId || null;
-    this.panels = data.panels || [];
-    this.rows = [];
+    this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
 
     this.addBuiltInAnnotationQuery();
     this.initMeta(meta);
@@ -134,32 +122,39 @@ export class DashboardModel {
     // temp remove stuff
     var events = this.events;
     var meta = this.meta;
-    var rows = this.rows;
     var variables = this.templating.list;
+    var panels = this.panels;
 
     delete this.events;
     delete this.meta;
+    delete this.panels;
 
     // prepare save model
-    this.rows = _.map(rows, row => row.getSaveModel());
     this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
+    this.panels = _.map(panels, panel => panel.getSaveModel());
 
     // make clone
     var copy = $.extend(true, {}, this);
     //  sort clone
     copy = sortByKeys(copy);
+    console.log(copy.panels);
 
     // restore properties
     this.events = events;
     this.meta = meta;
-    this.rows = rows;
     this.templating.list = variables;
+    this.panels = panels;
 
     return copy;
   }
 
-  addEmptyRow() {
-    this.rows.push(new DashboardRow({isNew: true}));
+  setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
+    this.meta.fullscreen = fullscreen;
+    this.meta.isEditing = isEditing && this.meta.canEdit;
+
+    panel.setViewMode(fullscreen, this.meta.isEditing);
+
+    this.events.emit('view-mode-changed', panel);
   }
 
   private ensureListExist(data) {
@@ -194,7 +189,8 @@ export class DashboardModel {
 
   addPanel(panel) {
     panel.id = this.getNextPanelId();
-    this.panels.push(panel);
+    this.panels.unshift(new PanelModel(panel));
+    this.events.emit('panel-added', panel);
   }
 
   removePanel(panel, ask?) {
@@ -296,6 +292,14 @@ export class DashboardModel {
     }
   }
 
+  on(eventName, callback) {
+    this.events.on(eventName, callback);
+  }
+
+  off(eventName, callback?) {
+    this.events.off(eventName, callback);
+  }
+
   cycleGraphTooltip() {
     this.graphTooltip = (this.graphTooltip + 1) % 3;
   }
@@ -338,7 +342,7 @@ export class DashboardModel {
     var i, j, k;
     var oldVersion = this.schemaVersion;
     var panelUpgrades = [];
-    this.schemaVersion = 15;
+    this.schemaVersion = 16;
 
     if (oldVersion === this.schemaVersion) {
       return;
@@ -647,7 +651,7 @@ export class DashboardModel {
         this.graphTooltip = old.sharedCrosshair ? 1 : 0;
       }
 
-      if (oldVersion < 15) {
+      if (oldVersion < 16) {
         this.upgradeToGridLayout(old);
       }
 
@@ -655,60 +659,59 @@ export class DashboardModel {
         return;
       }
 
-      for (i = 0; i < this.rows.length; i++) {
-        var row = this.rows[i];
-        for (j = 0; j < row.panels.length; j++) {
-          for (k = 0; k < panelUpgrades.length; k++) {
-            panelUpgrades[k].call(this, row.panels[j]);
-          }
+      for (j = 0; j < this.panels.length; j++) {
+        for (k = 0; k < panelUpgrades.length; k++) {
+          panelUpgrades[k].call(this, this.panels[j]);
         }
       }
     }
 
     upgradeToGridLayout(old) {
       let yPos = 0;
-      let rowIds = 1000;
+      //let rowIds = 1000;
+      //
+
+      if (!old.rows) {
+        return;
+      }
 
       for (let row of old.rows) {
         let xPos = 0;
-        let height: any = row.height;
-
-        if (this.meta.keepRows) {
-          this.panels.push({
-            id: rowIds++,
-            type: 'row',
-            title: row.title,
-            x: 0,
-            y: yPos,
-            height: 1,
-            width: 12
-          });
-
-          yPos += 1;
-        }
+        let height: any = row.height || 250;
+
+        // if (this.meta.keepRows) {
+        //   this.panels.push({
+        //     id: rowIds++,
+        //     type: 'row',
+        //     title: row.title,
+        //     x: 0,
+        //     y: yPos,
+        //     height: 1,
+        //     width: 12
+        //   });
+        //
+        //   yPos += 1;
+        // }
 
         if (_.isString(height)) {
           height = parseInt(height.replace('px', ''), 10);
         }
 
-        height = Math.ceil(height / CELL_HEIGHT);
+        const rowGridHeight = Math.ceil(height / CELL_HEIGHT);
 
         for (let panel of row.panels) {
           // should wrap to next row?
           if (xPos + panel.span >= 12) {
-            yPos += height;
+            yPos += rowGridHeight;
           }
 
-          panel.x = xPos;
-          panel.y = yPos;
-          panel.width = panel.span;
-          panel.height = height;
+          panel.gridPos = { x: xPos, y: yPos, w: panel.span, h: rowGridHeight };
 
           delete panel.span;
 
-          xPos += panel.width;
+          xPos += rowGridHeight;
 
-          this.panels.push(panel);
+          this.panels.push(new PanelModel(panel));
         }
 
         yPos += height;

+ 74 - 0
public/app/features/dashboard/PanelModel.ts

@@ -0,0 +1,74 @@
+import {Emitter} from 'app/core/core';
+
+export interface GridPos {
+  x: number;
+  y: number;
+  w: number;
+  h: number;
+}
+
+const notPersistedProperties: {[str: string]: boolean} = {
+  "events": true,
+  "fullscreen": true,
+  "isEditing": true,
+};
+
+export class PanelModel {
+  id: number;
+  gridPos:  GridPos;
+  type: string;
+  title: string;
+  alert?: any;
+
+  // non persisted
+  fullscreen: boolean;
+  isEditing: boolean;
+  events: Emitter;
+
+  constructor(model) {
+    this.events = new Emitter();
+
+    // copy properties from persisted model
+    for (var property in model) {
+      this[property] = model[property];
+    }
+  }
+
+  getSaveModel() {
+    const model: any = {};
+    for (var property in this) {
+      if (notPersistedProperties[property] || !this.hasOwnProperty(property)) {
+        continue;
+      }
+
+      model[property] = this[property];
+    }
+
+    return model;
+  }
+
+  setViewMode(fullscreen: boolean, isEditing: boolean) {
+    this.fullscreen = fullscreen;
+    this.isEditing = isEditing;
+    this.events.emit('panel-size-changed');
+  }
+
+  updateGridPos(newPos: GridPos) {
+    let sizeChanged = false;
+
+    if (this.gridPos.w !== newPos.w || this.gridPos.h !== newPos.h) {
+      sizeChanged = true;
+    }
+
+    this.gridPos.x = newPos.x;
+    this.gridPos.y = newPos.y;
+    this.gridPos.w = newPos.w;
+    this.gridPos.h = newPos.h;
+
+    if (sizeChanged) {
+      console.log('PanelModel sizeChanged event and render events fired');
+      this.events.emit('panel-size-changed');
+    }
+  }
+}
+

+ 3 - 2
public/app/features/dashboard/all.js

@@ -15,7 +15,6 @@ define([
   './unsavedChangesSrv',
   './unsaved_changes_modal',
   './timepicker/timepicker',
-  './graphiteImportCtrl',
   './impression_store',
   './upload',
   './import/dash_import',
@@ -23,7 +22,9 @@ define([
   './export_data/export_data_modal',
   './ad_hoc_filters',
   './repeat_option/repeat_option',
-  './dashgrid/dashgrid',
+  './dashgrid/DashboardGrid',
+  './dashgrid/PanelLoader',
+  './row/add_panel',
   './acl/acl',
   './folder_picker/picker',
   './folder_modal/folder'

+ 127 - 127
public/app/features/dashboard/dashboard_ctrl.ts

@@ -1,143 +1,143 @@
-///<reference path="../../headers/common.d.ts" />
-
 import config from 'app/core/config';
-import angular from 'angular';
 
 import coreModule from 'app/core/core_module';
+import {PanelContainer} from './dashgrid/PanelContainer';
+import {DashboardModel} from './DashboardModel';
 
-export class DashboardCtrl {
+export class DashboardCtrl implements PanelContainer {
+  dashboard: DashboardModel;
+  dashboardViewState: any;
+  loadedFallbackDashboard: boolean;
+  editTab: number;
 
   /** @ngInject */
   constructor(
     private $scope,
-    $rootScope,
-    keybindingSrv,
-    timeSrv,
-    variableSrv,
-    alertingSrv,
-    dashboardSrv,
-    unsavedChangesSrv,
-    dynamicDashboardSrv,
-    dashboardViewStateSrv,
-    contextSrv,
-    alertSrv,
-    $timeout) {
-
-      $scope.editor = { index: 0 };
-
-      var resizeEventTimeout;
-
-      $scope.setupDashboard = function(data) {
-        try {
-          $scope.setupDashboardInternal(data);
-        } catch (err) {
-          $scope.onInitFailed(err, 'Dashboard init failed', true);
-        }
-      };
-
-      $scope.setupDashboardInternal = function(data) {
-        var dashboard = dashboardSrv.create(data.dashboard, data.meta);
-        dashboardSrv.setCurrent(dashboard);
-
-        // init services
-        timeSrv.init(dashboard);
-        alertingSrv.init(dashboard, data.alerts);
-
-        // template values service needs to initialize completely before
-        // the rest of the dashboard can load
-        variableSrv.init(dashboard)
-        // template values failes are non fatal
-        .catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
-        // continue
-        .finally(function() {
-          dynamicDashboardSrv.init(dashboard);
-          dynamicDashboardSrv.process();
-
-          unsavedChangesSrv.init(dashboard, $scope);
-
-          $scope.dashboard = dashboard;
-          $scope.dashboardMeta = dashboard.meta;
-          $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
-
-          keybindingSrv.setupDashboardBindings($scope, dashboard);
-
-          $scope.dashboard.updateSubmenuVisibility();
-          $scope.setWindowTitleAndTheme();
-
-          $scope.appEvent("dashboard-initialized", $scope.dashboard);
-        })
-        .catch($scope.onInitFailed.bind(this, 'Dashboard init failed', true));
-      };
-
-      $scope.onInitFailed = function(msg, fatal, err) {
-        console.log(msg, err);
-
-        if (err.data && err.data.message) {
-          err.message = err.data.message;
-        } else if (!err.message) {
-          err = {message: err.toString()};
-        }
-
-        $scope.appEvent("alert-error", [msg, err.message]);
-
-        // protect against  recursive fallbacks
-        if (fatal && !$scope.loadedFallbackDashboard) {
-          $scope.loadedFallbackDashboard = true;
-          $scope.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
-        }
-      };
-
-      $scope.templateVariableUpdated = function() {
-        dynamicDashboardSrv.process();
-      };
-
-      $scope.setWindowTitleAndTheme = function() {
-        window.document.title = config.window_title_prefix + $scope.dashboard.title;
-      };
-
-      $scope.broadcastRefresh = function() {
-        $rootScope.$broadcast('refresh');
-      };
-
-      $scope.addRowDefault = function() {
-        $scope.dashboard.addEmptyRow();
-      };
-
-      $scope.showJsonEditor = function(evt, options) {
-        var editScope = $rootScope.$new();
-        editScope.object = options.object;
-        editScope.updateHandler = options.updateHandler;
-        $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
-      };
-
-      $scope.registerWindowResizeEvent = function() {
-        angular.element(window).bind('resize', function() {
-          $timeout.cancel(resizeEventTimeout);
-          resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
-        });
-
-        $scope.$on('$destroy', function() {
-          angular.element(window).unbind('resize');
-          $scope.dashboard.destroy();
-        });
-      };
-
-      $scope.timezoneChanged = function() {
-        $rootScope.$broadcast("refresh");
-      };
-
-      $scope.onFolderChange = function(folder) {
-        $scope.dashboard.folderId = folder.id;
-        $scope.dashboard.meta.folderId = folder.id;
-        $scope.dashboard.meta.folderTitle= folder.title;
-      };
+    private $rootScope,
+    private keybindingSrv,
+    private timeSrv,
+    private variableSrv,
+    private alertingSrv,
+    private dashboardSrv,
+    private unsavedChangesSrv,
+    private dynamicDashboardSrv,
+    private dashboardViewStateSrv,
+    private panelLoader) {
+      // temp hack due to way dashboards are loaded
+      // can't use controllerAs on route yet
+      $scope.ctrl = this;
+
+      // TODO: break out settings view to separate view & controller
+      this.editTab = 0;
+
+      // funcs called from React component bindings and needs this binding
+      this.getPanelContainer = this.getPanelContainer.bind(this);
+    }
+
+    setupDashboard(data) {
+      try {
+        this.setupDashboardInternal(data);
+      } catch (err) {
+        this.onInitFailed(err, 'Dashboard init failed', true);
+      }
+    }
+
+    setupDashboardInternal(data) {
+      const dashboard = this.dashboardSrv.create(data.dashboard, data.meta);
+      this.dashboardSrv.setCurrent(dashboard);
+
+      // init services
+      this.timeSrv.init(dashboard);
+      this.alertingSrv.init(dashboard, data.alerts);
+
+      // template values service needs to initialize completely before
+      // the rest of the dashboard can load
+      this.variableSrv.init(dashboard)
+      // template values failes are non fatal
+      .catch(this.onInitFailed.bind(this, 'Templating init failed', false))
+      // continue
+      .finally(() => {
+        this.dashboard = dashboard;
+
+        this.dynamicDashboardSrv.init(dashboard);
+        this.dynamicDashboardSrv.process();
+
+        this.unsavedChangesSrv.init(dashboard, this.$scope);
+
+        // TODO refactor ViewStateSrv
+        this.$scope.dashboard = dashboard;
+        this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope);
+
+        this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
+
+        this.dashboard.updateSubmenuVisibility();
+        this.setWindowTitleAndTheme();
+
+        this.$scope.appEvent("dashboard-initialized", dashboard);
+      })
+      .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
+    }
+
+    onInitFailed(msg, fatal, err) {
+      console.log(msg, err);
+
+      if (err.data && err.data.message) {
+        err.message = err.data.message;
+      } else if (!err.message) {
+        err = {message: err.toString()};
+      }
+
+      this.$scope.appEvent("alert-error", [msg, err.message]);
+
+      // protect against  recursive fallbacks
+      if (fatal && !this.loadedFallbackDashboard) {
+        this.loadedFallbackDashboard = true;
+        this.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
+      }
+    }
+
+    templateVariableUpdated() {
+      this.dynamicDashboardSrv.process();
+    }
+
+    setWindowTitleAndTheme() {
+      window.document.title = config.window_title_prefix + this.dashboard.title;
+    }
+
+    showJsonEditor(evt, options) {
+      var editScope = this.$rootScope.$new();
+      editScope.object = options.object;
+      editScope.updateHandler = options.updateHandler;
+      this.$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
+    }
+
+    getDashboard() {
+      return this.dashboard;
+    }
+
+    getPanelLoader() {
+      return this.panelLoader;
+    }
+
+    timezoneChanged() {
+      this.$rootScope.$broadcast("refresh");
+    }
+
+    onFolderChange(folder) {
+      this.dashboard.folderId = folder.id;
+      this.dashboard.meta.folderId = folder.id;
+      this.dashboard.meta.folderTitle= folder.title;
+    }
+
+    getPanelContainer() {
+      console.log('DashboardCtrl:getPanelContainer()');
+      return this;
     }
 
     init(dashboard) {
       this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
       this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
-      this.$scope.setupDashboard(dashboard);
-      this.$scope.registerWindowResizeEvent();
+      this.setupDashboard(dashboard);
     }
 }
 

+ 1 - 3
public/app/features/dashboard/dashboard_srv.ts

@@ -1,7 +1,5 @@
-///<reference path="../../headers/common.d.ts" />
-
 import coreModule from 'app/core/core_module';
-import {DashboardModel} from './model';
+import {DashboardModel} from './DashboardModel';
 
 export class DashboardSrv {
   dash: any;

+ 131 - 0
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -0,0 +1,131 @@
+import React from 'react';
+import coreModule from 'app/core/core_module';
+import ReactGridLayout from 'react-grid-layout';
+import {CELL_HEIGHT, CELL_VMARGIN} from '../DashboardModel';
+import {DashboardPanel} from './DashboardPanel';
+import {DashboardModel} from '../DashboardModel';
+import {PanelContainer} from './PanelContainer';
+import {PanelModel} from '../PanelModel';
+import classNames from 'classnames';
+import sizeMe from 'react-sizeme';
+
+const COLUMN_COUNT = 12;
+
+function GridWrapper({size, layout, onLayoutChange, children, onResize}) {
+  if (size.width === 0) {
+    console.log('size is zero!');
+  }
+
+  const gridWidth = size.width > 0 ? size.width : 1200;
+
+  return (
+    <ReactGridLayout
+      width={gridWidth}
+      className="layout"
+      isDraggable={true}
+      isResizable={true}
+      measureBeforeMount={false}
+      containerPadding={[0, 0]}
+      useCSSTransforms={true}
+      margin={[CELL_VMARGIN, CELL_VMARGIN]}
+      cols={COLUMN_COUNT}
+      rowHeight={CELL_HEIGHT}
+      draggableHandle=".grid-drag-handle"
+      layout={layout}
+      onResize={onResize}
+      onLayoutChange={onLayoutChange}>
+      {children}
+    </ReactGridLayout>
+  );
+}
+
+const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper);
+
+export interface DashboardGridProps {
+  getPanelContainer: () => PanelContainer;
+}
+
+export class DashboardGrid extends React.Component<DashboardGridProps, any> {
+  gridToPanelMap: any;
+  panelContainer: PanelContainer;
+  dashboard: DashboardModel;
+  panelMap: {[id: string]: PanelModel};
+
+  constructor(props) {
+    super(props);
+    this.panelContainer = this.props.getPanelContainer();
+    this.onLayoutChange = this.onLayoutChange.bind(this);
+    this.onResize = this.onResize.bind(this);
+
+    // subscribe to dashboard events
+    this.dashboard = this.panelContainer.getDashboard();
+    this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
+    this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
+  }
+
+  buildLayout() {
+    const layout = [];
+    this.panelMap = {};
+
+    for (let panel of this.dashboard.panels) {
+      let stringId = panel.id.toString();
+      this.panelMap[stringId] = panel;
+
+      if (!panel.gridPos) {
+        console.log('panel without gridpos');
+        continue;
+      }
+
+      layout.push({
+        i: stringId,
+        x: panel.gridPos.x,
+        y: panel.gridPos.y,
+        w: panel.gridPos.w,
+        h: panel.gridPos.h,
+      });
+    }
+
+    return layout;
+  }
+
+  onLayoutChange(newLayout) {
+    for (const newPos of newLayout) {
+      this.panelMap[newPos.i].updateGridPos(newPos);
+    }
+  }
+
+  triggerForceUpdate() {
+    this.forceUpdate();
+  }
+
+  onResize(layout, oldItem, newItem) {
+    this.panelMap[newItem.i].updateGridPos(newItem);
+  }
+
+  renderPanels() {
+    const panelElements = [];
+
+    for (let panel of this.dashboard.panels) {
+      const panelClasses = classNames({panel: true, 'panel--fullscreen': panel.fullscreen});
+      panelElements.push(
+        <div key={panel.id.toString()} className={panelClasses}>
+          <DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
+        </div>,
+      );
+    }
+
+    return panelElements;
+  }
+
+  render() {
+    return (
+      <SizedReactLayoutGrid layout={this.buildLayout()} onLayoutChange={this.onLayoutChange} onResize={this.onResize}>
+        {this.renderPanels()}
+      </SizedReactLayoutGrid>
+    );
+  }
+}
+
+coreModule.directive('dashboardGrid', function(reactDirective) {
+  return reactDirective(DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);
+});

+ 40 - 0
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import {PanelModel} from '../PanelModel';
+import {PanelContainer} from './PanelContainer';
+import {AttachedPanel} from './PanelLoader';
+
+export interface DashboardPanelProps {
+  panel: PanelModel;
+  getPanelContainer: () => PanelContainer;
+}
+
+export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
+  element: any;
+  attachedPanel: AttachedPanel;
+
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  componentDidMount() {
+    const panelContainer = this.props.getPanelContainer();
+    const dashboard = panelContainer.getDashboard();
+    const loader = panelContainer.getPanelLoader();
+
+    this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
+  }
+
+  componentWillUnmount() {
+    if (this.attachedPanel) {
+      this.attachedPanel.destroy();
+    }
+  }
+
+  render() {
+    return (
+      <div ref={element => this.element = element} />
+    );
+  }
+}
+

+ 8 - 0
public/app/features/dashboard/dashgrid/PanelContainer.ts

@@ -0,0 +1,8 @@
+import {DashboardModel}  from '../DashboardModel';
+import {PanelLoader} from './PanelLoader';
+
+export interface PanelContainer {
+  getPanelLoader(): PanelLoader;
+  getDashboard(): DashboardModel;
+}
+

+ 34 - 0
public/app/features/dashboard/dashgrid/PanelLoader.ts

@@ -0,0 +1,34 @@
+import angular from 'angular';
+import coreModule from 'app/core/core_module';
+
+export interface AttachedPanel {
+  destroy();
+}
+
+export class PanelLoader {
+
+  /** @ngInject */
+  constructor(private $compile, private $rootScope) {
+  }
+
+  load(elem, panel, dashboard): AttachedPanel {
+    var template = '<plugin-component type="panel"></plugin-component>';
+    var panelScope = this.$rootScope.$new();
+    panelScope.panel = panel;
+    panelScope.dashboard = dashboard;
+
+    const compiledElem = this.$compile(template)(panelScope);
+    const rootNode = angular.element(elem);
+    rootNode.append(compiledElem);
+
+    return {
+      destroy: () => {
+        console.log('AttachedPanel:Destroy, id' + panel.id);
+        panelScope.$destroy();
+        compiledElem.remove();
+      }
+    };
+  }
+}
+
+coreModule.service('panelLoader', PanelLoader);

+ 0 - 211
public/app/features/dashboard/dashgrid/dashgrid.ts

@@ -1,211 +0,0 @@
-// ///<reference path="../../../headers/common.d.ts" />
-//
-// import coreModule from 'app/core/core_module';
-// import {CELL_HEIGHT, CELL_VMARGIN} from '../model';
-//
-// import 'jquery-ui';
-// import 'gridstack/dist/jquery.jQueryUI';
-// import 'gridstack';
-//
-// const template = `
-// <div class="grid-stack">
-//   <dash-grid-item ng-repeat="panel in ctrl.dashboard.panels track by panel.id"
-//                   class="grid-stack-item"
-//                   grid-ctrl="ctrl"
-//                   panel="panel">
-//     <plugin-component type="panel" class="grid-stack-item-content">
-//     </plugin-component>
-//   </dash-grid-item>
-// </div>
-// `;
-//
-// var rowIndex = 0;
-//
-// export class GridCtrl {
-//   options: any;
-//   dashboard: any;
-//   panels: any;
-//   gridstack: any;
-//   gridElem: any;
-//   isInitialized: boolean;
-//   isDestroyed: boolean;
-//   index: number;
-//   changeRenderPromise: any;
-//
-//   #<{(|* @ngInject |)}>#
-//   constructor(private $scope, private $element, private $timeout) {
-//     console.log(this.dashboard);
-//     this.index = rowIndex;
-//     rowIndex += 1;
-//   }
-//
-//   init() {
-//     this.gridElem = this.$element.find('.grid-stack');
-//
-//     this.gridstack = this.gridElem.gridstack({
-//       animate: true,
-//       cellHeight: CELL_HEIGHT,
-//       verticalMargin: CELL_VMARGIN,
-//       acceptWidgets: '.grid-stack-item',
-//       handle: '.grid-drag-handle'
-//     }).data('gridstack');
-//
-//     this.isInitialized = true;
-//
-//     this.gridElem.on('added', (e, items) => {
-//       for (let item of items) {
-//         this.onGridStackItemAdded(item);
-//       }
-//     });
-//
-//     this.gridElem.on('removed', (e, items) => {
-//       for (let item of items) {
-//         this.onGridStackItemRemoved(item);
-//       }
-//     });
-//
-//     this.gridElem.on('change', (e, items) => {
-//       this.$timeout(() => this.onGridStackItemsChanged(items), 50);
-//     });
-//   }
-//
-//   onGridStackItemAdded(item) {
-//     console.log('row: ' + this.index + ' item added', item);
-//   }
-//
-//   onGridStackItemRemoved(item) {
-//     console.log('row: ' + this.index + ' item removed', item.id, item);
-//   }
-//
-//   onGridStackItemsChanged(items) {
-//     console.log('onGridStackItemsChanged');
-//
-//     for (let item of items) {
-//       // find panel
-//       var panel = this.dashboard.getPanelById(parseInt(item.id));
-//
-//       if (!panel) {
-//         console.log('item change but no panel found for item', item);
-//         continue;
-//       }
-//
-//       // update panel model position
-//       panel.x = item.x;
-//       panel.y = item.y;
-//       panel.width = item.width;
-//       panel.height = item.height;
-//
-//       console.log('updating panel: ' + panel.id + ' x: ' + panel.x + ' y: ' + panel.y);
-//     }
-//
-//     this.dashboard.panels.sort(function (a, b) {
-//       let aScore = a.x + (a.y * 12);
-//       let bScore = b.x + (b.y * 12);
-//       if (aScore < bScore) { return -1; }
-//       if (aScore > bScore) { return 1; }
-//       return 0;
-//     });
-//
-//     if (this.changeRenderPromise) {
-//       this.$timeout.cancel(this.changeRenderPromise);
-//     }
-//
-//     this.changeRenderPromise = this.$timeout(() => {
-//       console.log('broadcasting render');
-//       this.$scope.$broadcast('render');
-//     });
-//   }
-//
-//   destroy() {
-//     this.gridstack.destroy();
-//     this.gridstack = null;
-//     this.isDestroyed = true;
-//   }
-// }
-//
-// #<{(|* @ngInject *|)}>#
-// export function dashGrid($timeout) {
-//   return {
-//     restrict: 'E',
-//     template: template,
-//     controller: GridCtrl,
-//     bindToController: true,
-//     controllerAs: 'ctrl',
-//     scope: {
-//       dashboard: "=",
-//     },
-//     link: function(scope, elem, attrs, ctrl) {
-//       $timeout(function() {
-//         ctrl.init();
-//       });
-//
-//       scope.$on('$destroy', () => {
-//         ctrl.destroy();
-//       });
-//     }
-//   };
-// }
-//
-// #<{(|* @ngInject *|)}>#
-// export function dashGridItem($timeout, $rootScope) {
-//   return {
-//     restrict: "E",
-//     scope: {
-//       panel: '=',
-//       gridCtrl: '='
-//     },
-//     link: function (scope, element, attrs) {
-//       let gridCtrl = scope.gridCtrl;
-//       let panel = scope.panel;
-//       let gridStackNode = null;
-//
-//       element.attr({
-//         'data-gs-id': panel.id,
-//         'data-gs-x': panel.x,
-//         'data-gs-y': panel.y,
-//         'data-gs-width': panel.width,
-//         'data-gs-height': panel.height,
-//         'data-gs-no-resize': panel.type === 'row',
-//       });
-//
-//       $rootScope.onAppEvent('panel-fullscreen-exit', (evt, payload) => {
-//         if (panel.id !== payload.panelId) {
-//           return;
-//         }
-//         gridCtrl.gridstack.locked(element, false);
-//         element.removeClass('panel-fullscreen');
-//       }, scope);
-//
-//       $rootScope.onAppEvent('panel-fullscreen-enter', (evt, payload) => {
-//         if (panel.id !== payload.panelId) {
-//           return;
-//         }
-//         element.addClass('panel-fullscreen');
-//       }, scope);
-//
-//       scope.$on('$destroy', () => {
-//         console.log('grid-item scope $destroy');
-//         if (gridCtrl.isDestroyed) {
-//           return;
-//         }
-//
-//         if (gridStackNode) {
-//           console.log('grid-item scope $destroy removeWidget');
-//           gridStackNode._grid.removeWidget(element);
-//         }
-//       });
-//
-//       if (gridCtrl.isInitialized) {
-//         gridCtrl.gridstack.makeWidget(element);
-//         gridStackNode = element.data('_gridstack_node');
-//       } else {
-//         setTimeout(function() {
-//           gridStackNode = element.data('_gridstack_node');
-//         }, 500);
-//       }
-//     }
-//   };
-// }
-//
-// coreModule.directive('dashGrid', dashGrid);
-// coreModule.directive('dashGridItem', dashGridItem);

+ 0 - 232
public/app/features/dashboard/dashgrid/ref.txt

@@ -1,232 +0,0 @@
-Skip to content
-This repository
-Search
-Pull requests
-Issues
-Marketplace
-Gist
- @torkelo
- Sign out
- Unwatch 946
-  Unstar 17,021
- Fork 2,862 grafana/grafana
- Code  Issues 1,079  Pull requests 46  Projects 1  Wiki  Settings Insights
-Branch: gridstack Find file Copy pathgrafana/public/app/core/components/dashgrid/dashgrid.ts
-a6bbcb8  on Jun 13
-@torkelo torkelo ux: gridstack poc
-1 contributor
-RawBlameHistory
-213 lines (181 sloc)  5.45 KB
-///<reference path="../../../headers/common.d.ts" />
-
-import $ from 'jquery';
-import coreModule from '../../core_module';
-
-import 'jquery-ui';
-import 'gridstack';
-import 'gridstack.jquery-ui';
-
-const template = `
-<div gridstack gridstack-handler="ctrl.gridstack" class="grid-stack"
-      options="ctrl.options"
-      on-change="ctrl.onChange(event,items)"
-      on-drag-start="ctrl.onDragStart(event,ui)"
-      on-drag-stop="ctrl.onDragStop(event, ui)"
-      on-resize-start="ctrl.onResizeStart(event, ui)"
-      on-resize-stop="ctrl.onResizeStop(event, ui)">
-      <div gridstack-item ng-repeat="panel in ctrl.panels"
-          class="grid-stack-item"
-          gs-item-id="panel.id"
-          gs-item-x="panel.x"
-          gs-item-y="panel.y"
-          gs-item-width="panel.width"
-          gs-item-height="panel.height"
-          gs-item-autopos="1"
-          on-item-added="ctrl.onItemAdded(item)"
-          on-item-removed="ctrl.onItemRemoved(item)">
-        <plugin-component type="panel" class="panel-margin grid-stack-item-content">
-        </plugin-component>
-      </div>
-</div>
-`;
-
-export class DashGridCtrl {
-  options: any;
-
-  /** @ngInject */
-  constructor(private $rootScope) {
-    this.options = {
-      animate: true,
-    };
-  }
-
-  onResizeStop() {
-    this.$rootScope.$broadcast('render');
-  }
-}
-
-export function dashGrid($timeout) {
-  return {
-    restrict: 'E',
-    template: template,
-    controller: DashGridCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    scope: {
-      dashboard: "="
-    },
-    link: function(scope, elem, attrs, ctrl) {
-
-      ctrl.panels = [];
-      ctrl.dashboard.forEachPanel((panel, panelIndex, row, rowIndex) => {
-        panel.width = 4;
-        panel.height = 4;
-        panel.x = panelIndex * 4;
-        panel.y = rowIndex * 4;
-        ctrl.panels.push(panel);
-      });
-
-    }
-  };
-}
-
-/** @ngInject */
-coreModule.controller('GridstackController', ['$scope', function($scope) {
-
-  var gridstack = null;
-
-  this.init = function(element, options) {
-    gridstack = element.gridstack(options).data('gridstack');
-    return gridstack;
-  };
-
-  this.removeItem = function(element) {
-    if (gridstack) {
-      return gridstack.removeWidget(element, false);
-    }
-    return null;
-  };
-
-  this.addItem = function(element) {
-    if (gridstack) {
-      gridstack.makeWidget(element);
-      return element;
-    }
-    return null;
-  };
-
-}]);
-
-/** @ngInject */
-coreModule.directive('gridstack', ['$timeout', function($timeout) {
-  return {
-    restrict: "A",
-    controller: 'GridstackController',
-    scope: {
-      onChange: '&',
-      onDragStart: '&',
-      onDragStop: '&',
-      onResizeStart: '&',
-      onResizeStop: '&',
-      gridstackHandler: '=',
-      options: '='
-    },
-    link: function (scope, element, attrs, controller, ngModel) {
-
-      var gridstack = controller.init(element, scope.options);
-      scope.gridstackHandler = gridstack;
-
-      element.on('change', function (e, items) {
-        $timeout(function() {
-          scope.$apply();
-          scope.onChange({event: e, items: items});
-        });
-      });
-
-      element.on('dragstart', function(e, ui) {
-        scope.onDragStart({event: e, ui: ui});
-      });
-
-      element.on('dragstop', function(e, ui) {
-        $timeout(function() {
-          scope.$apply();
-          scope.onDragStop({event: e, ui: ui});
-        });
-      });
-
-      element.on('resizestart', function(e, ui) {
-        scope.onResizeStart({event: e, ui: ui});
-      });
-
-      element.on('resizestop', function(e, ui) {
-        $timeout(function() {
-          scope.$apply();
-          scope.onResizeStop({event: e, ui: ui});
-        });
-      });
-
-    }
-  };
-}]);
-
-/** @ngInject */
-coreModule.directive('gridstackItem', ['$timeout', function($timeout) {
-
-  return {
-    restrict: "A",
-    controller: 'GridstackController',
-    require: '^gridstack',
-    scope: {
-      gridstackItem: '=',
-      onItemAdded: '&',
-      onItemRemoved: '&',
-      gsItemId: '=',
-      gsItemX: '=',
-      gsItemY: '=',
-      gsItemWidth: '=',
-      gsItemHeight: '=',
-      gsItemAutopos: '='
-    },
-    link: function (scope, element, attrs, controller) {
-      $(element).attr('data-gs-id', scope.gsItemId);
-      $(element).attr('data-gs-x', scope.gsItemX);
-      $(element).attr('data-gs-y', scope.gsItemY);
-      $(element).attr('data-gs-width', scope.gsItemWidth);
-      $(element).attr('data-gs-height', scope.gsItemHeight);
-      $(element).attr('data-gs-auto-position', scope.gsItemAutopos);
-      var widget = controller.addItem(element);
-      var item = element.data('_gridstack_node');
-      $timeout(function() {
-        scope.onItemAdded({item: item});
-      });
-      scope.$watch(function () { return $(element).attr('data-gs-id'); }, function (val) {
-        scope.gsItemId = val;
-      });
-      scope.$watch(function(){ return $(element).attr('data-gs-x'); }, function(val) {
-        scope.gsItemX = val;
-      });
-
-      scope.$watch(function(){ return $(element).attr('data-gs-y'); }, function(val) {
-        scope.gsItemY = val;
-      });
-
-      scope.$watch(function(){ return $(element).attr('data-gs-width'); }, function(val) {
-        scope.gsItemWidth = val;
-      });
-
-      scope.$watch(function(){ return $(element).attr('data-gs-height'); }, function(val) {
-        scope.gsItemHeight = val;
-      });
-
-      element.bind('$destroy', function() {
-        var item = element.data('_gridstack_node');
-        scope.onItemRemoved({item: item});
-        controller.removeItem(element);
-      });
-    }
-  };
-}]);
-
-coreModule.directive('dashGrid', dashGrid);
-Contact GitHub API Training Shop Blog About
-© 2017 GitHub, Inc. Terms Privacy Security Status Help

+ 1 - 1
public/app/features/dashboard/dashnav/dashnav.html

@@ -70,7 +70,7 @@
 				</ul>
 			</li>
 			<li class="navbar-mini-btn-wrapper" ng-show="::ctrl.dashboard.meta.canSave">
-				<button class="btn btn-secondary btn-mini" ng-click="ctrl.openEditView('add-panel')">
+				<button class="btn btn-secondary btn-mini" ng-click="ctrl.addPanel()">
 					<i class="fa fa-plus-circle"></i> Add Panel
 				</button>
 			</li>

+ 15 - 7
public/app/features/dashboard/dashnav/dashnav.ts

@@ -4,7 +4,7 @@ import _ from 'lodash';
 import moment from 'moment';
 import angular from 'angular';
 import {appEvents, NavModel} from 'app/core/core';
-import {DashboardModel} from '../model';
+import {DashboardModel} from '../DashboardModel';
 
 export class DashNavCtrl {
   dashboard: DashboardModel;
@@ -92,16 +92,16 @@ export class DashNavCtrl {
     }
 
     deleteDashboard() {
-      var confirmText = "";
+      var confirmText = '';
       var text2 = this.dashboard.title;
-      var alerts = this.dashboard.rows.reduce((memo, row) => {
-        memo += row.panels.filter(panel => panel.alert).length;
-        return memo;
-      }, 0);
+
+      const alerts = _.sumBy(this.dashboard.panels, panel => {
+         return panel.alert ? 1 : 0;
+      });
 
       if (alerts > 0) {
         confirmText = 'DELETE';
-        text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
+        text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboard will also delete those alerts`;
       }
 
       appEvents.emit('confirm-modal', {
@@ -145,6 +145,14 @@ export class DashNavCtrl {
       this.$rootScope.appEvent('show-dash-search');
     }
 
+    addPanel() {
+      this.dashboard.addPanel({
+        type: 'graph',
+        gridPos: {x: 0, y: 0, w: 6, h: 5},
+        title: 'New Graph',
+      });
+    }
+
     navItemClicked(navItem, evt) {
       if (navItem.clickHandler) {
         navItem.clickHandler();

+ 0 - 95
public/app/features/dashboard/graphiteImportCtrl.js

@@ -1,95 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/utils/kbn'
-],
-function (angular, _, kbn) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('GraphiteImportCtrl', function($scope, datasourceSrv, dashboardSrv, $location) {
-    $scope.options = {};
-
-    $scope.init = function() {
-      $scope.datasources = [];
-      _.each(datasourceSrv.getAll(), function(ds) {
-        if (ds.type === 'graphite') {
-          $scope.options.sourceName = ds.name;
-          $scope.datasources.push(ds.name);
-        }
-      });
-    };
-
-    $scope.listAll = function() {
-      datasourceSrv.get($scope.options.sourceName).then(function(datasource) {
-        $scope.datasource = datasource;
-        $scope.datasource.listDashboards('').then(function(results) {
-          $scope.dashboards = results;
-        }, function(err) {
-          var message = err.message || err.statusText || 'Error';
-          $scope.appEvent('alert-error', ['Failed to load dashboard list from graphite', message]);
-        });
-      });
-    };
-
-    $scope.import = function(dashName) {
-      $scope.datasource.loadDashboard(dashName).then(function(results) {
-        if (!results.data || !results.data.state) {
-          throw { message: 'no dashboard state received from graphite' };
-        }
-
-        graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name);
-      }, function(err) {
-        var message = err.message || err.statusText || 'Error';
-        $scope.appEvent('alert-error', ['Failed to load dashboard from graphite', message]);
-      });
-    };
-
-    function graphiteToGrafanaTranslator(state, datasource) {
-      var graphsPerRow = 2;
-      var rowHeight = 300;
-      var rowTemplate;
-      var currentRow;
-      var panel;
-
-      rowTemplate = {
-        title: '',
-        panels: [],
-        height: rowHeight
-      };
-
-      currentRow = angular.copy(rowTemplate);
-
-      var newDashboard = dashboardSrv.create({});
-      newDashboard.rows = [];
-      newDashboard.title = state.name;
-      newDashboard.rows.push(currentRow);
-
-      _.each(state.graphs, function(graph, index) {
-        if (currentRow.panels.length === graphsPerRow) {
-          currentRow = angular.copy(rowTemplate);
-          newDashboard.rows.push(currentRow);
-        }
-
-        panel = {
-          type: 'graph',
-          span: 12 / graphsPerRow,
-          title: graph[1].title,
-          targets: [],
-          datasource: datasource,
-          id: index + 1
-        };
-
-        _.each(graph[1].target, function(target) {
-          panel.targets.push({ target: target });
-        });
-
-        currentRow.panels.push(panel);
-      });
-
-      window.grafanaImportDashboard = newDashboard;
-      $location.path('/dashboard-import/' + kbn.slugifyForUrl(newDashboard.title));
-    }
-  });
-});

+ 1 - 1
public/app/features/dashboard/history/history.ts

@@ -6,7 +6,7 @@ import _ from 'lodash';
 import angular from 'angular';
 import moment from 'moment';
 
-import {DashboardModel} from '../model';
+import {DashboardModel} from '../DashboardModel';
 import {HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv} from './history_srv';
 
 export class HistoryListCtrl {

+ 1 - 3
public/app/features/dashboard/history/history_srv.ts

@@ -1,8 +1,6 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
-import {DashboardModel} from '../model';
+import {DashboardModel} from '../DashboardModel';
 
 export interface HistoryListOpts {
   limit: number;

+ 13 - 13
public/app/features/dashboard/partials/settings.html

@@ -5,7 +5,7 @@
 
 	<ul class="gf-tabs">
 		<li class="gf-tabs-item" ng-repeat="tab in ::['General', 'Links', 'Time picker']">
-			<a class="gf-tabs-link" ng-click="editor.index = $index" ng-class="{active: editor.index === $index}">
+			<a class="gf-tabs-link" ng-click="ctrl.editTab = $index" ng-class="{active: ctrl.editTab === $index}">
 				{{::tab}}
 			</a>
 		</li>
@@ -17,30 +17,30 @@
 </div>
 
 <div class="tabbed-view-body">
-	<div ng-if="editor.index == 0">
+	<div ng-if="ctrl.editTab == 0">
 
 		<div class="gf-form-group section">
       <h5 class="section-heading">Details</h5>
 			<div class="gf-form">
 				<label class="gf-form-label width-7">Name</label>
-				<input type="text" class="gf-form-input width-30" ng-model='dashboard.title'></input>
+				<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
 			</div>
       <div class="gf-form">
 				<label class="gf-form-label width-7">Description</label>
-				<input type="text" class="gf-form-input width-30" ng-model='dashboard.description'></input>
+				<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
 			</div>
       <div class="gf-form">
 				<label class="gf-form-label width-7">
           Tags
           <info-popover mode="right-normal">Press enter to add a tag</info-popover>
         </label>
-				<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
+				<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
 				</bootstrap-tagsinput>
 			</div>
 
-      <folder-picker ng-if="!dashboardMeta.isFolder"
-										 initial-title="dashboardMeta.folderTitle"
-										 on-change="onFolderChange($folder)"
+      <folder-picker ng-if="!ctrl.dashboard.meta.isFolder"
+										 initial-title="ctrl.dashboard.meta.folderTitle"
+										 on-change="ctrl.onFolderChange($folder)"
 										 label-class="width-7">
 			</folder-picker>
 		</div>
@@ -51,19 +51,19 @@
         <div class="gf-form">
           <label class="gf-form-label width-11">Timezone</label>
           <div class="gf-form-select-wrapper">
-            <select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
+            <select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
           </div>
         </div>
         <gf-form-switch class="gf-form"
                         label="Editable"
                         tooltip="Uncheck, then save and reload to disable all dashboard editing"
-                        checked="dashboard.editable"
+                        checked="ctrl.dashboard.editable"
                         label-class="width-11">
         </gf-form-switch>
 				<gf-form-switch class="gf-form"
                         label="Hide Controls"
                         tooltip="Hide row controls. Shortcut: CTRL+H or CMD+H"
-                        checked="dashboard.hideControls"
+                        checked="ctrl.dashboard.hideControls"
                         label-class="width-11">
         </gf-form-switch>
       </div>
@@ -79,7 +79,7 @@
           </info-popover>
         </label>
         <div class="gf-form-select-wrapper">
-          <select ng-model="dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
+          <select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
         </div>
       </div>
     </div>
@@ -90,7 +90,7 @@
 	</div>
 
 	<div ng-if="editor.index == 2">
-		<gf-time-picker-settings dashboard="dashboard"></gf-time-picker-settings>
+		<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
 	</div>
 
 </div>

+ 3 - 5
public/app/features/dashboard/save_as_modal.ts

@@ -55,11 +55,9 @@ export class SaveDashboardAsModalCtrl {
     // remove alerts if source dashboard is already persisted
     // do not want to create alert dupes
     if (dashboard.id > 0) {
-      this.clone.rows.forEach(row => {
-        row.panels.forEach(panel => {
-          delete panel.thresholds;
-          delete panel.alert;
-        });
+      this.clone.panels.forEach(panel => {
+        delete panel.thresholds;
+        delete panel.alert;
       });
     }
 

+ 1 - 1
public/app/features/dashboard/specs/dashboard_model_specs.ts

@@ -1,7 +1,7 @@
 import {describe, beforeEach, it, expect} from 'test/lib/common';
 
 import _ from 'lodash';
-import {DashboardModel} from '../model';
+import {DashboardModel} from '../DashboardModel';
 
 describe('DashboardModel', function() {
 

+ 1 - 1
public/app/features/dashboard/specs/exporter_specs.ts

@@ -3,7 +3,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
 import _ from 'lodash';
 import config from 'app/core/config';
 import {DashboardExporter} from '../export/exporter';
-import {DashboardModel} from '../model';
+import {DashboardModel} from '../DashboardModel';
 
 describe('given dashboard with repeated panels', function() {
   var dash, exported;

+ 6 - 12
public/app/features/dashboard/viewStateSrv.js

@@ -9,7 +9,7 @@ function (angular, _, $, config) {
 
   var module = angular.module('grafana.services');
 
-  module.factory('dashboardViewStateSrv', function($location, $timeout) {
+  module.factory('dashboardViewStateSrv', function($location, $timeout, $rootScope) {
 
     // represents the transient view state
     // like fullscreen panel & edit
@@ -154,7 +154,8 @@ function (angular, _, $, config) {
 
       ctrl.editMode = false;
       ctrl.fullscreen = false;
-      ctrl.dashboard.editMode = this.oldDashboardEditMode;
+
+      this.dashboard.setViewMode(ctrl.panel, false, false);
 
       this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id});
 
@@ -162,9 +163,9 @@ function (angular, _, $, config) {
 
       $timeout(function() {
         if (self.oldTimeRange !== ctrl.range) {
-          self.$scope.broadcastRefresh();
+          $rootScope.$broadcast('refresh');
         } else {
-          self.$scope.$broadcast('render');
+          $rootScope.$broadcast('render');
         }
         delete self.fullscreenPanel;
       });
@@ -176,18 +177,11 @@ function (angular, _, $, config) {
       ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
       ctrl.fullscreen = true;
 
-      this.oldDashboardEditMode = this.dashboard.editMode;
       this.oldTimeRange = ctrl.range;
       this.fullscreenPanel = panelScope;
-      this.dashboard.editMode = false;
-
-      $(window).scrollTop(0);
 
+      this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
       this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id});
-
-      $timeout(function() {
-        ctrl.render();
-      });
     };
 
     DashboardViewState.prototype.registerPanel = function(panelScope) {

+ 1 - 3
public/app/features/panel/metrics_tab.ts

@@ -1,6 +1,4 @@
-///<reference path="../../headers/common.d.ts" />
-
-import {DashboardModel} from '../dashboard/model';
+import {DashboardModel} from '../dashboard/DashboardModel';
 import Remarkable from 'remarkable';
 
 export class MetricsTabCtrl {

+ 12 - 18
public/app/features/panel/panel_ctrl.ts

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import $ from 'jquery';
 import {profiler} from 'app/core/profiler';
 import Remarkable from 'remarkable';
-import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/model';
+import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/DashboardModel';
 
 const TITLE_HEIGHT = 25;
 const EMPTY_TITLE_HEIGHT = 9;
@@ -37,7 +37,7 @@ export class PanelCtrl {
     this.$scope = $scope;
     this.$timeout = $injector.get('$timeout');
     this.editorTabIndex = 0;
-    this.events = new Emitter();
+    this.events = this.panel.events;
     this.timing = {};
 
     var plugin = config.panels[this.panel.type];
@@ -47,21 +47,14 @@ export class PanelCtrl {
     }
 
     $scope.$on("refresh", () => this.refresh());
-    $scope.$on("render", () => this.render());
     $scope.$on("$destroy", () => {
       this.events.emit('panel-teardown');
       this.events.removeAllListeners();
     });
-
-    // we should do something interesting
-    // with newly added panels
-    if (this.panel.isNew) {
-      delete this.panel.isNew;
-    }
   }
 
   init() {
-    this.calculatePanelHeight();
+    this.events.on('panel-size-changed', this.onSizeChanged.bind(this));
     this.publishAppEvent('panel-initialized', {scope: this.$scope});
     this.events.emit('panel-initialized');
   }
@@ -71,7 +64,7 @@ export class PanelCtrl {
   }
 
   refresh() {
-   this.events.emit('refresh', null);
+    this.events.emit('refresh', null);
   }
 
   publishAppEvent(evtName, evt) {
@@ -170,23 +163,24 @@ export class PanelCtrl {
        var fullscreenHeight = Math.floor(docHeight * 0.8);
        this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
     } else {
-      this.containerHeight = this.panel.height * CELL_HEIGHT + ((this.panel.height-1) * CELL_VMARGIN);
+      this.containerHeight = this.panel.gridPos.h * CELL_HEIGHT + ((this.panel.gridPos.h-1) * CELL_VMARGIN);
     }
 
     this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
   }
 
   render(payload?) {
-    // ignore if other panel is in fullscreen mode
-    if (this.otherPanelInFullscreenMode()) {
-      return;
-    }
-
-    this.calculatePanelHeight();
     this.timing.renderStart = new Date().getTime();
     this.events.emit('render', payload);
   }
 
+  private onSizeChanged() {
+    this.calculatePanelHeight();
+    this.$timeout(() => {
+      this.render();
+    }, 100);
+  }
+
   duplicate() {
     this.dashboard.duplicatePanel(this.panel);
     this.$timeout(() => {

+ 1 - 0
public/app/features/panel/panel_directive.ts

@@ -98,6 +98,7 @@ module.directive('grafanaPanel', function($rootScope, $document) {
       }
 
       ctrl.events.on('render', () => {
+        console.log('panelDirective::render!');
         if (lastHeight !== ctrl.containerHeight) {
           panelContainer.css({minHeight: ctrl.containerHeight});
           lastHeight = ctrl.containerHeight;

+ 5 - 5
public/app/features/panel/panel_header.ts

@@ -88,7 +88,7 @@ function panelHeader($compile) {
       let menuScope;
 
       elem.click(function(evt) {
-        const targetClass = evt.target.className;
+        //const targetClass = evt.target.className;
 
         // remove existing scope
         if (menuScope) {
@@ -100,10 +100,10 @@ function panelHeader($compile) {
         menuElem.html(menuHtml);
         $compile(menuElem)(menuScope);
 
-        if (targetClass === 'panel-title-text' || targetClass === 'panel-title') {
-          evt.stopPropagation();
-          elem.find('[data-toggle=dropdown]').dropdown('toggle');
-        }
+        // if (targetClass === 'panel-title-text' || targetClass === 'panel-title') {
+        //   evt.stopPropagation();
+        //   elem.find('[data-toggle=dropdown]').dropdown('toggle');
+        // }
       });
     }
   };

+ 1 - 1
public/app/features/plugins/plugin_component.ts

@@ -68,7 +68,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     var componentInfo: any = {
       name: 'panel-plugin-' + scope.panel.type,
       bindings: {dashboard: "=", panel: "=", row: "="},
-      attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"},
+      attrs: {dashboard: "dashboard", panel: "panel"},
     };
 
     let panelInfo = config.panels[scope.panel.type];

+ 6 - 4
public/app/partials/dashboard.html

@@ -1,14 +1,16 @@
-<div dash-class ng-if="dashboard">
-	<dashnav dashboard="dashboard"></dashnav>
+<div dash-class ng-if="ctrl.dashboard">
+	<dashnav dashboard="ctrl.dashboard"></dashnav>
 
 	<div class="scroll-canvas scroll-canvas--dashboard">
 		<div gemini-scrollbar>
 			<div dash-editor-view class="dash-edit-view"></div>
 			<div class="dashboard-container">
 
-				<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>
+				<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
+				</dashboard-submenu>
 
-        <dash-grid dashboard="dashboard"></dash-grid>
+				<dashboard-grid get-panel-container="ctrl.getPanelContainer">
+				</dashboard-grid>
 
 			</div>
 		</div>

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

@@ -26,9 +26,9 @@ describe('grafanaGraph', function() {
 
         beforeEach(angularMocks.inject(function($rootScope, $compile) {
           var ctrl: any = {
-            events: new Emitter(),
             height: 200,
             panel: {
+              events: new Emitter(),
               legend: {},
               grid: { },
               yaxes: [
@@ -65,7 +65,7 @@ describe('grafanaGraph', function() {
 
           var scope = $rootScope.$new();
           scope.ctrl = ctrl;
-
+          scope.ctrl.events = ctrl.panel.events;
 
           $rootScope.onAppEvent = sinon.spy();
 

+ 19 - 13
public/dashboards/home.json

@@ -13,7 +13,6 @@
     {
       "content": "<div class=\"text-center dashboard-header\">\n  <span>Home Dashboard</span>\n</div>",
       "editable": true,
-      "height": 2,
       "id": 1,
       "links": [],
       "mode": "html",
@@ -21,14 +20,16 @@
       "title": "",
       "transparent": true,
       "type": "text",
-      "width": 12,
-      "x": 0,
-      "y": 0
+      "gridPos": {
+        "w": 12,
+        "h": 2,
+        "x": 0,
+        "y": 0
+      }
     },
     {
       "folderId": 0,
       "headings": true,
-      "height": 17,
       "id": 3,
       "limit": 4,
       "links": [],
@@ -40,26 +41,31 @@
       "title": "",
       "transparent": false,
       "type": "dashlist",
-      "width": 7,
-      "x": 0,
-      "y": 6
+      "gridPos": {
+        "w": 7,
+        "h": 17,
+        "x": 0,
+        "y": 6
+      }
     },
     {
       "editable": true,
       "error": false,
-      "height": 17,
       "id": 4,
       "links": [],
       "title": "",
       "transparent": false,
       "type": "pluginlist",
-      "width": 5,
-      "x": 7,
-      "y": 6
+      "gridPos": {
+        "w": 5,
+        "h": 17,
+        "x": 7,
+        "y": 6
+      }
     }
   ],
   "rows": [],
-  "schemaVersion": 15,
+  "schemaVersion": 16,
   "style": "dark",
   "tags": [],
   "templating": {

+ 5 - 4
public/sass/_grafana.scss

@@ -77,11 +77,12 @@
 @import "components/tabbed_view";
 @import "components/query_part";
 @import "components/jsontree";
-@import "components/edit_sidemenu.scss";
+@import "components/edit_sidemenu";
 @import "components/row.scss";
-@import "components/gridstack.scss";
-@import "components/json_explorer.scss";
-@import "components/code_editor.scss";
+@import "components/json_explorer";
+@import "components/code_editor";
+@import "components/dashboard_grid";
+
 
 // PAGES
 @import "pages/login";

+ 3 - 3
public/sass/_variables.dark.scss

@@ -105,9 +105,9 @@ $tight-form-bg:     	$dark-3;
 $tight-form-func-bg: 		    #333;
 $tight-form-func-highlight-bg:  #444;
 
-$modal-background: $black;
-$code-tag-bg:      $gray-1;
-$code-tag-border:  lighten($code-tag-bg, 2%);
+$modal-backdrop-bg: $dark-3;
+$code-tag-bg:       $gray-1;
+$code-tag-border:   lighten($code-tag-bg, 2%);
 
 
 // Lists

+ 3 - 3
public/sass/_variables.light.scss

@@ -112,9 +112,9 @@ $tight-form-bg:    		$gray-6;
 $tight-form-func-bg:            $gray-5;
 $tight-form-func-highlight-bg:  $gray-6;
 
-$modal-background: $body-bg;
-$code-tag-bg:      $gray-6;
-$code-tag-border:  darken($code-tag-bg, 3%);
+$modal-backdrop-bg: $body-bg;
+$code-tag-bg:       $gray-6;
+$code-tag-border:   darken($code-tag-bg, 3%);
 
 // Lists
 $grafanaListBackground:    	   $gray-6;

+ 2 - 2
public/sass/_variables.scss

@@ -222,8 +222,8 @@ $btn-border-radius:              3px;
 $side-menu-width:  60px;
 
 // dashboard
-$panel-margin: 0.4rem;
-$dashboard-padding: ($panel-margin * 2) $panel-margin $panel-margin $panel-margin;
+$panel-margin: 10px;
+$dashboard-padding: $panel-margin * 2;
 
 // tabs
 $tabs-padding-top: 0.6rem;

+ 24 - 0
public/sass/components/_dashboard_grid.scss

@@ -0,0 +1,24 @@
+@import "~react-grid-layout/css/styles.css";
+@import "~react-resizable/css/styles.css";
+
+.panel-in-fullscreen {
+
+  .react-grid-layout {
+    height: 100% !important;
+  }
+
+  .react-grid-item {
+    display: none;
+    transition-property: none !important;
+  }
+
+  .panel--fullscreen {
+    display: block !important;
+    position: unset !important;
+    width: 100% !important;
+    height: 100% !important;
+    transform: translate(0px, 0px) !important;
+  }
+}
+
+

+ 0 - 325
public/sass/components/_gridstack.scss

@@ -1,325 +0,0 @@
-.grid-stack-item > .ui-resizable-handle {
-  filter: none;
-}
-
-.grid-stack {
-  position: relative;
-  min-height: 150px;
-}
-
-.grid-stack.grid-stack-rtl {
-  direction: ltr;
-}
-
-.grid-stack.grid-stack-rtl > .grid-stack-item {
-  direction: rtl;
-}
-
-.grid-stack .grid-stack-placeholder > .placeholder-content {
-  background: $input-label-bg;
-  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236,10.8);
-  margin: 0;
-  position: absolute;
-  top: 0;
-  left: 5px;
-  right: 5px;
-  bottom: 0;
-  width: auto;
-  text-align: center;
-}
-
-.grid-stack > .grid-stack-item {
-  min-width: 8.3333333333%;
-  position: absolute;
-  padding: 0;
-}
-
-.grid-stack > .grid-stack-item > .grid-stack-item-content {
-  margin: 0;
-  position: absolute;
-  top: 0;
-  left: 7px;
-  right: 7px;
-  bottom: 0;
-  width: auto;
-}
-
-.grid-stack > .grid-stack-item > .ui-resizable-handle {
-  position: absolute;
-  display: block;
-  -ms-touch-action: none;
-  touch-action: none;
-  font-size: 10px;
-  color: $text-color-weak;
-}
-
-.grid-stack > .grid-stack-item.ui-resizable-disabled > .ui-resizable-handle,
-.grid-stack > .grid-stack-item.ui-resizable-autohide > .ui-resizable-handle {
-  display: none;
-}
-
-.grid-stack > .grid-stack-item.ui-draggable-dragging, .grid-stack > .grid-stack-item.ui-resizable-resizing {
-  z-index: 100;
-}
-
-.grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content,
-.grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content, .grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content,
-.grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content {
-  box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.2);
-  opacity: 0.8;
-}
-
-.grid-stack > .grid-stack-item > .ui-resizable-se,
-.grid-stack > .grid-stack-item > .ui-resizable-sw {
-  font-family: 'grafana-icons' !important;
-  speak: none;
-  font-style: normal;
-  font-weight: normal;
-  font-variant: normal;
-  text-transform: none;
-  line-height: 1;
-    -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  &::before {
-    content: "\e90b";
-  }
-}
-.grid-stack > .grid-stack-item > .ui-resizable-se {
-  cursor: se-resize;
-  width: 16px;
-  height: 16px;
-  right: 6px;
-  bottom: -2px;
-}
-
-.grid-stack > .grid-stack-item.ui-draggable-dragging > .ui-resizable-handle {
-  display: none !important;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='1'] {
-  width: 8.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='1'] {
-  left: 8.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='1'] {
-  min-width: 8.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='1'] {
-  max-width: 8.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='2'] {
-  width: 16.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='2'] {
-  left: 16.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='2'] {
-  min-width: 16.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='2'] {
-  max-width: 16.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='3'] {
-  width: 25%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='3'] {
-  left: 25%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='3'] {
-  min-width: 25%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='3'] {
-  max-width: 25%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='4'] {
-  width: 33.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='4'] {
-  left: 33.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='4'] {
-  min-width: 33.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='4'] {
-  max-width: 33.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='5'] {
-  width: 41.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='5'] {
-  left: 41.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='5'] {
-  min-width: 41.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='5'] {
-  max-width: 41.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='6'] {
-  width: 50%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='6'] {
-  left: 50%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='6'] {
-  min-width: 50%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='6'] {
-  max-width: 50%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='7'] {
-  width: 58.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='7'] {
-  left: 58.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='7'] {
-  min-width: 58.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='7'] {
-  max-width: 58.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='8'] {
-  width: 66.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='8'] {
-  left: 66.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='8'] {
-  min-width: 66.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='8'] {
-  max-width: 66.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='9'] {
-  width: 75%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='9'] {
-  left: 75%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='9'] {
-  min-width: 75%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='9'] {
-  max-width: 75%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='10'] {
-  width: 83.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='10'] {
-  left: 83.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='10'] {
-  min-width: 83.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='10'] {
-  max-width: 83.3333333333%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='11'] {
-  width: 91.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='11'] {
-  left: 91.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='11'] {
-  min-width: 91.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='11'] {
-  max-width: 91.6666666667%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-width='12'] {
-  width: 100%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-x='12'] {
-  left: 100%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-min-width='12'] {
-  min-width: 100%;
-}
-
-.grid-stack > .grid-stack-item[data-gs-max-width='12'] {
-  max-width: 100%;
-}
-
-.grid-stack.grid-stack-animate,
-.grid-stack.grid-stack-animate .grid-stack-item {
-  -webkit-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-  -moz-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-  -ms-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-  -o-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-  transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-}
-
-.grid-stack.grid-stack-animate .grid-stack-item.ui-draggable-dragging,
-.grid-stack.grid-stack-animate .grid-stack-item.ui-resizable-resizing,
-.grid-stack.grid-stack-animate .grid-stack-item.grid-stack-placeholder {
-  -webkit-transition: left 0s, top 0s, height 0s, width 0s;
-  -moz-transition: left 0s, top 0s, height 0s, width 0s;
-  -ms-transition: left 0s, top 0s, height 0s, width 0s;
-  -o-transition: left 0s, top 0s, height 0s, width 0s;
-  transition: left 0s, top 0s, height 0s, width 0s;
-}
-
-.grid-stack.grid-stack-one-column-mode {
-  height: auto !important;
-}
-
-.grid-stack.grid-stack-one-column-mode > .grid-stack-item {
-  position: relative !important;
-  width: auto !important;
-  left: 0 !important;
-  top: auto !important;
-  margin-bottom: 20px;
-  max-width: none !important;
-}
-
-.grid-stack.grid-stack-one-column-mode > .grid-stack-item > .ui-resizable-handle {
-  display: none;
-}

+ 1 - 1
public/sass/components/_modals.scss

@@ -10,7 +10,7 @@
   bottom: 0;
   left: 0;
   z-index: $zindex-modal-backdrop;
-  background-color: $black;
+  background-color: $modal-backdrop-bg;
 }
 
 .modal-backdrop,

+ 1 - 1
public/sass/components/_navbar.scss

@@ -17,7 +17,7 @@
 
 .sidemenu-open {
   .navbar {
-    padding-left: 0;
+    padding-left: $panel-margin;
   }
 }
 

+ 1 - 1
public/sass/components/_submenu.scss

@@ -5,7 +5,7 @@
   align-content: flex-start;
   align-items: flex-start;
 
-  margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
+  margin: 0 0 $panel-margin 0;
 }
 
 .annotation-disabled, .annotation-disabled a {

+ 2 - 0
public/sass/pages/_dashboard.scss

@@ -25,6 +25,7 @@ div.flot-text {
   background-color: $panel-bg;
   border: $panel-border;
   position: relative;
+  height: 100%;
 
   &.panel-transparent {
     background-color: transparent;
@@ -34,6 +35,7 @@ div.flot-text {
 
 .panel-content {
   padding: 0px 10px 5px 10px;
+  height: 100%;
 }
 
 .panel-title-container {

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

@@ -1,4 +0,0 @@
-declare let helpers: any;
-export default helpers;
-
-

+ 0 - 180
public/test/specs/helpers.js

@@ -1,180 +0,0 @@
-define([
- 'lodash',
- 'app/core/config',
- 'app/core/utils/datemath',
-], function(_, config, dateMath) {
-  'use strict';
-
-  function ControllerTestContext() {
-    var self = this;
-
-    this.datasource = {};
-    this.$element = {};
-    this.annotationsSrv = {};
-    this.timeSrv = new TimeSrvStub();
-    this.templateSrv = new TemplateSrvStub();
-    this.datasourceSrv = {
-      getMetricSources: function() {},
-      get: function() {
-        return {
-          then: function(callback) {
-            callback(self.datasource);
-          }
-        };
-      }
-    };
-
-    this.providePhase = function(mocks) {
-      return window.module(function($provide) {
-        $provide.value('datasourceSrv', self.datasourceSrv);
-        $provide.value('annotationsSrv', self.annotationsSrv);
-        $provide.value('timeSrv', self.timeSrv);
-        $provide.value('templateSrv', self.templateSrv);
-        $provide.value('$element', self.$element);
-        _.each(mocks, function(value, key) {
-          $provide.value(key, value);
-        });
-      });
-    };
-
-    this.createPanelController = function(Ctrl) {
-      return window.inject(function($controller, $rootScope, $q, $location, $browser) {
-        self.scope = $rootScope.$new();
-        self.$location = $location;
-        self.$browser = $browser;
-        self.$q = $q;
-        self.panel = {type: 'test'};
-        self.dashboard = {meta: {}};
-
-        $rootScope.appEvent = sinon.spy();
-        $rootScope.onAppEvent = sinon.spy();
-        $rootScope.colors = [];
-
-        for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
-
-        config.panels['test'] = {info: {}};
-        self.ctrl = $controller(Ctrl, {$scope: self.scope}, {
-          panel: self.panel, dashboard: self.dashboard, row: {}
-        });
-      });
-    };
-
-    this.createControllerPhase = function(controllerName) {
-      return window.inject(function($controller, $rootScope, $q, $location, $browser) {
-        self.scope = $rootScope.$new();
-        self.$location = $location;
-        self.$browser = $browser;
-        self.scope.contextSrv = {};
-        self.scope.panel = {};
-        self.scope.row = { panels:[] };
-        self.scope.dashboard = {meta: {}};
-        self.scope.dashboardMeta = {};
-        self.scope.dashboardViewState = new DashboardViewStateStub();
-        self.scope.appEvent = sinon.spy();
-        self.scope.onAppEvent = sinon.spy();
-
-        $rootScope.colors = [];
-        for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
-
-        self.$q = $q;
-        self.scope.skipDataOnInit = true;
-        self.scope.skipAutoInit = true;
-        self.controller = $controller(controllerName, {
-          $scope: self.scope
-        });
-      });
-    };
-  }
-
-  function ServiceTestContext() {
-    var self = this;
-    self.templateSrv = new TemplateSrvStub();
-    self.timeSrv = new TimeSrvStub();
-    self.datasourceSrv = {};
-    self.backendSrv = {};
-    self.$routeParams = {};
-
-    this.providePhase = function(mocks) {
-      return window.module(function($provide) {
-        _.each(mocks, function(key) {
-          $provide.value(key, self[key]);
-        });
-      });
-    };
-
-    this.createService = function(name) {
-      return window.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) {
-        self.$q = $q;
-        self.$rootScope = $rootScope;
-        self.$httpBackend =  $httpBackend;
-        self.$location = $location;
-
-        self.$rootScope.onAppEvent = function() {};
-        self.$rootScope.appEvent = function() {};
-        self.$timeout = $timeout;
-
-        self.service = $injector.get(name);
-      });
-    };
-  }
-
-  function DashboardViewStateStub() {
-    this.registerPanel = function() {
-    };
-  }
-
-  function TimeSrvStub() {
-    this.init = sinon.spy();
-    this.time = { from:'now-1h', to: 'now'};
-    this.timeRange = function(parse) {
-      if (parse === false) {
-        return this.time;
-      }
-      return {
-        from : dateMath.parse(this.time.from, false),
-        to : dateMath.parse(this.time.to, true)
-      };
-    };
-
-    this.replace = function(target) {
-      return target;
-    };
-
-    this.setTime = function(time) {
-      this.time = time;
-    };
-  }
-
-  function ContextSrvStub() {
-    this.hasRole = function() {
-      return true;
-    };
-  }
-
-  function TemplateSrvStub() {
-    this.variables = [];
-    this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g };
-    this.data = {};
-    this.replace = function(text) {
-      return _.template(text, this.templateSettings)(this.data);
-    };
-    this.init = function() {};
-    this.getAdhocFilters = function() { return []; };
-    this.fillVariableValuesForUrl = function() {};
-    this.updateTemplateData = function() { };
-    this.variableExists = function() { return false; };
-    this.variableInitialized = function() { };
-    this.highlightVariablesAsHtml = function(str) { return str; };
-    this.setGrafanaVariable = function(name, value) {
-      this.data[name] = value;
-    };
-  }
-
-  return {
-    ControllerTestContext: ControllerTestContext,
-    TimeSrvStub: TimeSrvStub,
-    ContextSrvStub: ContextSrvStub,
-    ServiceTestContext: ServiceTestContext
-  };
-
-});

+ 195 - 0
public/test/specs/helpers.ts

@@ -0,0 +1,195 @@
+import _ from 'lodash';
+import config from 'app/core/config';
+import * as dateMath from 'app/core/utils/datemath';
+import {angularMocks, sinon} from '../lib/common';
+import {PanelModel} from 'app/features/dashboard/PanelModel';
+
+export function ControllerTestContext() {
+  var self = this;
+
+  this.datasource = {};
+  this.$element = {};
+  this.annotationsSrv = {};
+  this.timeSrv = new TimeSrvStub();
+  this.templateSrv = new TemplateSrvStub();
+  this.datasourceSrv = {
+    getMetricSources: function() {},
+    get: function() {
+      return {
+        then: function(callback) {
+          callback(self.datasource);
+        },
+      };
+    },
+  };
+
+  this.providePhase = function(mocks) {
+    return angularMocks.module(function($provide) {
+      $provide.value('datasourceSrv', self.datasourceSrv);
+      $provide.value('annotationsSrv', self.annotationsSrv);
+      $provide.value('timeSrv', self.timeSrv);
+      $provide.value('templateSrv', self.templateSrv);
+      $provide.value('$element', self.$element);
+      _.each(mocks, function(value, key) {
+        $provide.value(key, value);
+      });
+    });
+  };
+
+  this.createPanelController = function(Ctrl) {
+    return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) {
+      self.scope = $rootScope.$new();
+      self.$location = $location;
+      self.$browser = $browser;
+      self.$q = $q;
+      self.panel = new PanelModel({type: 'test'});
+      self.dashboard = {meta: {}};
+
+      $rootScope.appEvent = sinon.spy();
+      $rootScope.onAppEvent = sinon.spy();
+      $rootScope.colors = [];
+
+      for (var i = 0; i < 50; i++) {
+        $rootScope.colors.push('#' + i);
+      }
+
+      config.panels['test'] = {info: {}};
+      self.ctrl = $controller(
+        Ctrl,
+        {$scope: self.scope},
+        {
+          panel: self.panel,
+          dashboard: self.dashboard,
+        },
+      );
+    });
+  };
+
+  this.createControllerPhase = function(controllerName) {
+    return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) {
+      self.scope = $rootScope.$new();
+      self.$location = $location;
+      self.$browser = $browser;
+      self.scope.contextSrv = {};
+      self.scope.panel = {};
+      self.scope.dashboard = {meta: {}};
+      self.scope.dashboardMeta = {};
+      self.scope.dashboardViewState = new DashboardViewStateStub();
+      self.scope.appEvent = sinon.spy();
+      self.scope.onAppEvent = sinon.spy();
+
+      $rootScope.colors = [];
+      for (var i = 0; i < 50; i++) {
+        $rootScope.colors.push('#' + i);
+      }
+
+      self.$q = $q;
+      self.scope.skipDataOnInit = true;
+      self.scope.skipAutoInit = true;
+      self.controller = $controller(controllerName, {
+        $scope: self.scope,
+      });
+    });
+  };
+}
+
+export function ServiceTestContext() {
+  var self = this;
+  self.templateSrv = new TemplateSrvStub();
+  self.timeSrv = new TimeSrvStub();
+  self.datasourceSrv = {};
+  self.backendSrv = {};
+  self.$routeParams = {};
+
+  this.providePhase = function(mocks) {
+    return angularMocks.module(function($provide) {
+      _.each(mocks, function(key) {
+        $provide.value(key, self[key]);
+      });
+    });
+  };
+
+  this.createService = function(name) {
+    return angularMocks.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) {
+      self.$q = $q;
+      self.$rootScope = $rootScope;
+      self.$httpBackend = $httpBackend;
+      self.$location = $location;
+
+      self.$rootScope.onAppEvent = function() {};
+      self.$rootScope.appEvent = function() {};
+      self.$timeout = $timeout;
+
+      self.service = $injector.get(name);
+    });
+  };
+}
+
+export function DashboardViewStateStub() {
+  this.registerPanel = function() {};
+}
+
+export function TimeSrvStub() {
+  this.init = sinon.spy();
+  this.time = {from: 'now-1h', to: 'now'};
+  this.timeRange = function(parse) {
+    if (parse === false) {
+      return this.time;
+    }
+    return {
+      from: dateMath.parse(this.time.from, false),
+      to: dateMath.parse(this.time.to, true),
+    };
+  };
+
+  this.replace = function(target) {
+    return target;
+  };
+
+  this.setTime = function(time) {
+    this.time = time;
+  };
+}
+
+export function ContextSrvStub() {
+  this.hasRole = function() {
+    return true;
+  };
+}
+
+export function TemplateSrvStub() {
+  this.variables = [];
+  this.templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g};
+  this.data = {};
+  this.replace = function(text) {
+    return _.template(text, this.templateSettings)(this.data);
+  };
+  this.init = function() {};
+  this.getAdhocFilters = function() {
+    return [];
+  };
+  this.fillVariableValuesForUrl = function() {};
+  this.updateTemplateData = function() {};
+  this.variableExists = function() {
+    return false;
+  };
+  this.variableInitialized = function() {};
+  this.highlightVariablesAsHtml = function(str) {
+    return str;
+  };
+  this.setGrafanaVariable = function(name, value) {
+    this.data[name] = value;
+  };
+}
+
+var allDeps = {
+  ContextSrvStub: ContextSrvStub,
+  TemplateSrvStub: TemplateSrvStub,
+  TimeSrvStub: TimeSrvStub,
+  ControllerTestContext: ControllerTestContext,
+  ServiceTestContext: ServiceTestContext,
+  DashboardViewStateStub: DashboardViewStateStub
+};
+
+// for legacy
+export default allDeps;

+ 0 - 130
public/test/test-main.js

@@ -1,130 +0,0 @@
-(function() {
-  "use strict";
-
-  // Tun on full stack traces in errors to help debugging
-  Error.stackTraceLimit=Infinity;
-
-  window.__karma__.loaded = function() {};
-
-  System.config({
-    baseURL: '/base/',
-    defaultJSExtensions: true,
-    paths: {
-      'mousetrap': 'vendor/npm/mousetrap/mousetrap.js',
-      'eventemitter3': 'vendor/npm/eventemitter3/index.js',
-      'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
-      'tether': 'vendor/npm/tether/dist/js/tether.js',
-      'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js',
-      'moment': 'vendor/moment.js',
-      "jquery": "vendor/jquery/dist/jquery.js",
-      'lodash-src': 'vendor/lodash/dist/lodash.js',
-      "lodash": 'app/core/lodash_extended.js',
-      "angular": 'vendor/angular/angular.js',
-      'angular-mocks': 'vendor/angular-mocks/angular-mocks.js',
-      "bootstrap":  "vendor/bootstrap/bootstrap.js",
-      'angular-route':          'vendor/angular-route/angular-route.js',
-      'angular-sanitize':       'vendor/angular-sanitize/angular-sanitize.js',
-      "angular-ui":             "vendor/angular-ui/ui-bootstrap-tpls.js",
-      "angular-strap":          "vendor/angular-other/angular-strap.js",
-      "angular-dragdrop":       "vendor/angular-native-dragdrop/draganddrop.js",
-      "angular-bindonce":       "vendor/angular-bindonce/bindonce.js",
-      "spectrum": "vendor/spectrum.js",
-      "bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js",
-      "jquery.flot": "vendor/flot/jquery.flot",
-      "jquery.flot.pie": "vendor/flot/jquery.flot.pie",
-      "jquery.flot.selection": "vendor/flot/jquery.flot.selection",
-      "jquery.flot.stack": "vendor/flot/jquery.flot.stack",
-      "jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent",
-      "jquery.flot.time": "vendor/flot/jquery.flot.time",
-      "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
-      "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
-      "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
-      "d3": "vendor/d3/d3.js",
-      "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
-      "twemoji": "vendor/npm/twemoji/2/twemoji.amd.js",
-      "ace": "vendor/npm/ace-builds/src-noconflict/ace",
-    },
-
-    packages: {
-      app: {
-        defaultExtension: 'js',
-      },
-      vendor: {
-        defaultExtension: 'js',
-      },
-    },
-
-    map: {
-    },
-
-    meta: {
-      'vendor/angular/angular.js': {
-        format: 'global',
-        deps: ['jquery'],
-        exports: 'angular',
-      },
-      'vendor/angular-mocks/angular-mocks.js': {
-        format: 'global',
-        deps: ['angular'],
-      },
-      'vendor/npm/eventemitter3/index.js': {
-        format: 'cjs',
-        exports: 'EventEmitter'
-      },
-      'vendor/npm/mousetrap/mousetrap.js': {
-        format: 'global',
-        exports: 'Mousetrap'
-      },
-      'vendor/npm/ace-builds/src-noconflict/ace.js': {
-        format: 'global',
-        exports: 'ace'
-      },
-    }
-  });
-
-  function file2moduleName(filePath) {
-    return filePath.replace(/\\/g, '/')
-    .replace(/^\/base\//, '')
-      .replace(/\.\w*$/, '');
-  }
-
-  function onlySpecFiles(path) {
-    return /specs.*/.test(path);
-  }
-
-  window.grafanaBootData = {settings: {}};
-
-  var modules = ['angular', 'angular-mocks', 'app/app'];
-  var promises = modules.map(function(name) {
-    return System.import(name);
-  });
-
-  Promise.all(promises).then(function(deps) {
-    var angular = deps[0];
-
-    angular.module('grafana', ['ngRoute']);
-    angular.module('grafana.services', ['ngRoute', '$strap.directives']);
-    angular.module('grafana.panels', []);
-    angular.module('grafana.controllers', []);
-    angular.module('grafana.directives', []);
-    angular.module('grafana.filters', []);
-    angular.module('grafana.routes', ['ngRoute']);
-
-    // load specs
-    return Promise.all(
-      Object.keys(window.__karma__.files) // All files served by Karma.
-      .filter(onlySpecFiles)
-      .map(file2moduleName)
-      .map(function(path) {
-        // console.log(path);
-        return System.import(path);
-      }));
-  }).then(function()  {
-    window.__karma__.start();
-  }, function(error) {
-    window.__karma__.error(error.stack || error);
-  }).catch(function(error) {
-    window.__karma__.error(error.stack || error);
-  });
-
-})();

+ 1 - 1
scripts/webpack/webpack.dev.js

@@ -10,7 +10,7 @@ const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 module.exports = merge(common, {
-  devtool: "eval-source-map",
+  devtool: "cheap-module-source-map",
 
   entry: {
     dark: './public/sass/grafana.dark.scss',

+ 53 - 15
yarn.lock

@@ -1072,6 +1072,10 @@ base@^0.11.1:
     mixin-deep "^1.2.0"
     pascalcase "^0.1.1"
 
+batch-processor@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8"
+
 bcrypt-pbkdf@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
@@ -1509,6 +1513,10 @@ class-utils@^0.3.5:
     lazy-cache "^2.0.2"
     static-extend "^0.1.1"
 
+classnames@2.x, classnames@^2.2.5:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
+
 clean-css@3.4.x, clean-css@~3.4.2:
   version "3.4.28"
   resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
@@ -2411,6 +2419,12 @@ elegant-spinner@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
 
+element-resize-detector@^1.1.12:
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.12.tgz#8b3fd6eedda17f9c00b360a0ea2df9927ae80ba2"
+  dependencies:
+    batch-processor "^1.0.0"
+
 elliptic@^6.0.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -3445,14 +3459,6 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6,
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
 
-"gridstack@https://github.com/grafana/gridstack.js#grafana":
-  version "1.0.0-dev"
-  resolved "https://github.com/grafana/gridstack.js#bd40b3fe4dafc99350145c7b4761d8693593f6fe"
-  dependencies:
-    jquery "^3.1.0"
-    jquery-ui "^1.12.0"
-    lodash "^4.14.2"
-
 growl@1.10.3:
   version "1.10.3"
   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
@@ -4434,11 +4440,7 @@ jest-validate@^21.1.0:
     leven "^2.1.0"
     pretty-format "^21.2.1"
 
-jquery-ui@^1.12.0:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
-
-jquery@^3.1.0, jquery@^3.2.1:
+jquery@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
 
@@ -4975,6 +4977,10 @@ lodash.flattendeep@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
 
+lodash.isequal@^4.0.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
 lodash.kebabcase@^4.0.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -5011,7 +5017,7 @@ lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, loda
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
-lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.14.2, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4:
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
@@ -6719,7 +6725,7 @@ promzard@^0.3.0:
   dependencies:
     read "1"
 
-prop-types@^15.5.10, prop-types@^15.6.0:
+prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0:
   version "15.6.0"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
   dependencies:
@@ -6877,6 +6883,38 @@ react-dom@^16.0.0:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
+"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4"
+  dependencies:
+    classnames "^2.2.5"
+    prop-types "^15.5.10"
+
+react-grid-layout@^0.16.0:
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.0.tgz#f74363cd134b2f8a763224d7b6287cbb68e6de05"
+  dependencies:
+    classnames "2.x"
+    lodash.isequal "^4.0.0"
+    prop-types "15.x"
+    react-draggable "^3.0.3"
+    react-resizable "^1.7.5"
+
+react-resizable@^1.7.5:
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
+  dependencies:
+    prop-types "15.x"
+    react-draggable "^2.2.6 || ^3.0.3"
+
+react-sizeme@^2.3.6:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678"
+  dependencies:
+    element-resize-detector "^1.1.12"
+    invariant "^2.2.2"
+    lodash "^4.17.4"
+
 react-test-renderer@^16.0.0:
   version "16.0.0"
   resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15"