Browse Source

newgrid: added constants, changed grid to 24 cols, added tests for panel repeats

Torkel Ödegaard 8 years ago
parent
commit
e5a6cb6241

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

@@ -0,0 +1,6 @@
+
+export const GRID_CELL_HEIGHT = 20;
+export const GRID_CELL_VMARGIN = 10;
+export const GRID_COLUMN_COUNT = 24;
+
+

+ 325 - 292
public/app/features/dashboard/dashboard_model.ts

@@ -1,15 +1,13 @@
 import moment from 'moment';
 import moment from 'moment';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
+import {GRID_COLUMN_COUNT, GRID_CELL_HEIGHT} from 'app/core/constants';
 import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
 import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
 import {Emitter, contextSrv} from 'app/core/core';
 import {Emitter, contextSrv} from 'app/core/core';
-import {DashboardRow} from './row/row_model';
-import {PanelModel} from './panel_model';
 import sortByKeys from 'app/core/utils/sort_by_keys';
 import sortByKeys from 'app/core/utils/sort_by_keys';
 
 
-export const CELL_HEIGHT = 30;
-export const CELL_VMARGIN = 10;
-export const COL_COUNT = 12;
+import {DashboardRow} from './row/row_model';
+import {PanelModel} from './panel_model';
 
 
 export class DashboardModel {
 export class DashboardModel {
   id: any;
   id: any;
@@ -48,10 +46,10 @@ export class DashboardModel {
   events: Emitter;
   events: Emitter;
 
 
   static nonPersistedProperties: {[str: string]: boolean} = {
   static nonPersistedProperties: {[str: string]: boolean} = {
-    "events": true,
-    "meta": true,
-    "panels": true, // needs special handling
-    "templating": true, // needs special handling
+    events: true,
+    meta: true,
+    panels: true, // needs special handling
+    templating: true, // needs special handling
   };
   };
 
 
   constructor(data, meta?) {
   constructor(data, meta?) {
@@ -66,12 +64,12 @@ export class DashboardModel {
     this.autoUpdate = data.autoUpdate;
     this.autoUpdate = data.autoUpdate;
     this.description = data.description;
     this.description = data.description;
     this.tags = data.tags || [];
     this.tags = data.tags || [];
-    this.style = data.style || "dark";
+    this.style = data.style || 'dark';
     this.timezone = data.timezone || '';
     this.timezone = data.timezone || '';
     this.editable = data.editable !== false;
     this.editable = data.editable !== false;
     this.graphTooltip = data.graphTooltip || 0;
     this.graphTooltip = data.graphTooltip || 0;
     this.hideControls = data.hideControls || false;
     this.hideControls = data.hideControls || false;
-    this.time = data.time || { from: 'now-6h', to: 'now' };
+    this.time = data.time || {from: 'now-6h', to: 'now'};
     this.timepicker = data.timepicker || {};
     this.timepicker = data.timepicker || {};
     this.templating = this.ensureListExist(data.templating);
     this.templating = this.ensureListExist(data.templating);
     this.annotations = this.ensureListExist(data.annotations);
     this.annotations = this.ensureListExist(data.annotations);
@@ -144,7 +142,7 @@ export class DashboardModel {
 
 
     // get variable save models
     // get variable save models
     copy.templating = {
     copy.templating = {
-      list: _.map(this.templating.list, variable => variable.getSaveModel ? variable.getSaveModel() : variable),
+      list: _.map(this.templating.list, variable => (variable.getSaveModel ? variable.getSaveModel() : variable)),
     };
     };
 
 
     // get panel save models
     // get panel save models
@@ -166,17 +164,24 @@ export class DashboardModel {
   }
   }
 
 
   private ensureListExist(data) {
   private ensureListExist(data) {
-    if (!data) { data = {}; }
-    if (!data.list) { data.list = []; }
+    if (!data) {
+      data = {};
+    }
+    if (!data.list) {
+      data.list = [];
+    }
     return data;
     return data;
   }
   }
 
 
   getNextPanelId() {
   getNextPanelId() {
-    var j, panel, max = 0;
-    for (j = 0; j < this.panels.length; j++) {
-      panel = this.panels[j];
-      if (panel.id > max) { max = panel.id; }
+    let max = 0;
+
+    for (let panel of this.panels) {
+      if (panel.id > max) {
+        max = panel.id;
+      }
     }
     }
+
     return max + 1;
     return max + 1;
   }
   }
 
 
@@ -225,9 +230,13 @@ export class DashboardModel {
     }
     }
 
 
     this.iteration = (this.iteration || new Date().getTime()) + 1;
     this.iteration = (this.iteration || new Date().getTime()) + 1;
-
     let panelsToRemove = [];
     let panelsToRemove = [];
 
 
+    // cleanup scopedVars
+    for (let panel of this.panels) {
+      delete panel.scopedVars;
+    }
+
     for (let panel of this.panels) {
     for (let panel of this.panels) {
       if (panel.repeat) {
       if (panel.repeat) {
         if (!cleanUpOnly) {
         if (!cleanUpOnly) {
@@ -263,7 +272,9 @@ export class DashboardModel {
 
 
   repeatPanel(panel: PanelModel) {
   repeatPanel(panel: PanelModel) {
     var variable = _.find(this.templating.list, {name: panel.repeat});
     var variable = _.find(this.templating.list, {name: panel.repeat});
-    if (!variable) { return; }
+    if (!variable) {
+      return;
+    }
 
 
     var selected;
     var selected;
     if (variable.current.text === 'All') {
     if (variable.current.text === 'All') {
@@ -276,7 +287,7 @@ export class DashboardModel {
       var option = selected[index];
       var option = selected[index];
       var copy = this.getRepeatClone(panel, index);
       var copy = this.getRepeatClone(panel, index);
 
 
-      copy.scopedVars = copy.scopedVars || {};
+      copy.scopedVars = {};
       copy.scopedVars[variable.name] = option;
       copy.scopedVars[variable.name] = option;
 
 
       // souce panel uses original possition
       // souce panel uses original possition
@@ -285,9 +296,9 @@ export class DashboardModel {
       }
       }
 
 
       if (panel.repeatDirection === 'Y') {
       if (panel.repeatDirection === 'Y') {
-        copy.gridPos.y = panel.gridPos.y + (panel.gridPos.h*index);
+        copy.gridPos.y = panel.gridPos.y + panel.gridPos.h * index;
       } else {
       } else {
-        copy.gridPos.x = panel.gridPos.x + (panel.gridPos.w*index);
+        copy.gridPos.x = panel.gridPos.x + panel.gridPos.w * index;
       }
       }
     }
     }
   }
   }
@@ -304,46 +315,39 @@ export class DashboardModel {
 
 
   updateSubmenuVisibility() {
   updateSubmenuVisibility() {
     this.meta.submenuEnabled = (() => {
     this.meta.submenuEnabled = (() => {
-      if (this.links.length > 0) { return true; }
+      if (this.links.length > 0) {
+        return true;
+      }
 
 
       var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
       var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2);
-      if (visibleVars.length > 0) { return true; }
+      if (visibleVars.length > 0) {
+        return true;
+      }
 
 
       var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
       var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true);
-      if (visibleAnnotations.length > 0) { return true; }
+      if (visibleAnnotations.length > 0) {
+        return true;
+      }
 
 
       return false;
       return false;
     })();
     })();
   }
   }
 
 
   getPanelInfoById(panelId) {
   getPanelInfoById(panelId) {
-    var result: any = {};
-    _.each(this.rows, function(row) {
-      _.each(row.panels, function(panel, index) {
-        if (panel.id === panelId) {
-          result.panel = panel;
-          result.row = row;
-          result.index = index;
-        }
-      });
-    });
-
-    _.each(this.panels, function(panel, index) {
-      if (panel.id === panelId) {
-        result.panel = panel;
-        result.index = index;
+    for (let i = 0; i < this.panels.length; i++) {
+      if (this.panels[i].id === panelId) {
+        return {
+          panel: this.panels[i],
+          index: i,
+        };
       }
       }
-    });
-
-    if (!result.panel) {
-      return null;
     }
     }
 
 
-    return result;
+    return null;
   }
   }
 
 
   duplicatePanel(panel) {
   duplicatePanel(panel) {
-    const newPanel = _.cloneDeep(panel.getSaveModel());
+    const newPanel = panel.getSaveModel();
     newPanel.id = this.getNextPanelId();
     newPanel.id = this.getNextPanelId();
 
 
     delete newPanel.repeat;
     delete newPanel.repeat;
@@ -356,7 +360,7 @@ export class DashboardModel {
     delete newPanel.alert;
     delete newPanel.alert;
 
 
     // does it fit to the right?
     // does it fit to the right?
-    if (panel.gridPos.x + (panel.gridPos.w*2) <= COL_COUNT) {
+    if (panel.gridPos.x + panel.gridPos.w * 2 <= GRID_COLUMN_COUNT) {
       newPanel.gridPos.x += panel.gridPos.w;
       newPanel.gridPos.x += panel.gridPos.w;
     } else {
     } else {
       // add bellow
       // add bellow
@@ -372,9 +376,7 @@ export class DashboardModel {
     format = format || 'YYYY-MM-DD HH:mm:ss';
     format = format || 'YYYY-MM-DD HH:mm:ss';
     let timezone = this.getTimezone();
     let timezone = this.getTimezone();
 
 
-    return timezone === 'browser' ?
-      moment(date).format(format) :
-      moment.utc(date).format(format);
+    return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format);
   }
   }
 
 
   destroy() {
   destroy() {
@@ -407,9 +409,7 @@ export class DashboardModel {
   getRelativeTime(date) {
   getRelativeTime(date) {
     date = moment.isMoment(date) ? date : moment(date);
     date = moment.isMoment(date) ? date : moment(date);
 
 
-    return this.timezone === 'browser' ?
-      moment(date).fromNow() :
-      moment.utc(date).fromNow();
+    return this.timezone === 'browser' ? moment(date).fromNow() : moment.utc(date).fromNow();
   }
   }
 
 
   getNextQueryLetter(panel) {
   getNextQueryLetter(panel) {
@@ -442,7 +442,6 @@ export class DashboardModel {
 
 
     // version 2 schema changes
     // version 2 schema changes
     if (oldVersion < 2) {
     if (oldVersion < 2) {
-
       if (old.services) {
       if (old.services) {
         if (old.services.filter) {
         if (old.services.filter) {
           this.time = old.services.filter.time;
           this.time = old.services.filter.time;
@@ -460,7 +459,9 @@ export class DashboardModel {
           return;
           return;
         }
         }
 
 
-        if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
+        if (_.isBoolean(panel.legend)) {
+          panel.legend = {show: panel.legend};
+        }
 
 
         if (panel.grid) {
         if (panel.grid) {
           if (panel.grid.min) {
           if (panel.grid.min) {
@@ -502,9 +503,11 @@ export class DashboardModel {
     if (oldVersion < 4) {
     if (oldVersion < 4) {
       // move aliasYAxis changes
       // move aliasYAxis changes
       panelUpgrades.push(function(panel) {
       panelUpgrades.push(function(panel) {
-        if (panel.type !== 'graph') { return; }
+        if (panel.type !== 'graph') {
+          return;
+        }
         _.each(panel.aliasYAxis, function(value, key) {
         _.each(panel.aliasYAxis, function(value, key) {
-          panel.seriesOverrides = [{ alias: key, yaxis: value }];
+          panel.seriesOverrides = [{alias: key, yaxis: value}];
         });
         });
         delete panel.aliasYAxis;
         delete panel.aliasYAxis;
       });
       });
@@ -512,7 +515,7 @@ export class DashboardModel {
 
 
     if (oldVersion < 6) {
     if (oldVersion < 6) {
       // move pulldowns to new schema
       // move pulldowns to new schema
-      var annotations = _.find(old.pulldowns, { type: 'annotations' });
+      var annotations = _.find(old.pulldowns, {type: 'annotations'});
 
 
       if (annotations) {
       if (annotations) {
         this.annotations = {
         this.annotations = {
@@ -521,12 +524,20 @@ export class DashboardModel {
       }
       }
 
 
       // update template variables
       // update template variables
-      for (i = 0 ; i < this.templating.list.length; i++) {
+      for (i = 0; i < this.templating.list.length; i++) {
         var variable = this.templating.list[i];
         var variable = this.templating.list[i];
-        if (variable.datasource === void 0) { variable.datasource = null; }
-        if (variable.type === 'filter') { variable.type = 'query'; }
-        if (variable.type === void 0) { variable.type = 'query'; }
-        if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
+        if (variable.datasource === void 0) {
+          variable.datasource = null;
+        }
+        if (variable.type === 'filter') {
+          variable.type = 'query';
+        }
+        if (variable.type === void 0) {
+          variable.type = 'query';
+        }
+        if (variable.allFormat === void 0) {
+          variable.allFormat = 'glob';
+        }
       }
       }
     }
     }
 
 
@@ -537,277 +548,299 @@ export class DashboardModel {
 
 
       // ensure query refIds
       // ensure query refIds
       panelUpgrades.push(function(panel) {
       panelUpgrades.push(function(panel) {
-        _.each(panel.targets, function(target) {
-          if (!target.refId) {
-            target.refId = this.getNextQueryLetter(panel);
+        _.each(
+          panel.targets,
+          function(target) {
+            if (!target.refId) {
+              target.refId = this.getNextQueryLetter(panel);
             }
             }
-          }.bind(this));
-        });
-      }
+          }.bind(this),
+        );
+      });
+    }
 
 
-      if (oldVersion < 8) {
-        panelUpgrades.push(function(panel) {
-          _.each(panel.targets, function(target) {
-            // update old influxdb query schema
-            if (target.fields && target.tags && target.groupBy) {
-              if (target.rawQuery) {
-                delete target.fields;
-                delete target.fill;
-              } else {
-                target.select = _.map(target.fields, function(field) {
-                  var parts = [];
-                  parts.push({type: 'field', params: [field.name]});
-                  parts.push({type: field.func, params: []});
-                  if (field.mathExpr) {
-                    parts.push({type: 'math', params: [field.mathExpr]});
-                  }
-                  if (field.asExpr) {
-                    parts.push({type: 'alias', params: [field.asExpr]});
-                  }
-                  return parts;
-                });
-                delete target.fields;
-                _.each(target.groupBy, function(part) {
-                  if (part.type === 'time' && part.interval)  {
-                    part.params = [part.interval];
-                    delete part.interval;
-                  }
-                  if (part.type === 'tag' && part.key) {
-                    part.params = [part.key];
-                    delete part.key;
-                  }
-                });
-
-                if (target.fill) {
-                  target.groupBy.push({type: 'fill', params: [target.fill]});
-                  delete target.fill;
+    if (oldVersion < 8) {
+      panelUpgrades.push(function(panel) {
+        _.each(panel.targets, function(target) {
+          // update old influxdb query schema
+          if (target.fields && target.tags && target.groupBy) {
+            if (target.rawQuery) {
+              delete target.fields;
+              delete target.fill;
+            } else {
+              target.select = _.map(target.fields, function(field) {
+                var parts = [];
+                parts.push({type: 'field', params: [field.name]});
+                parts.push({type: field.func, params: []});
+                if (field.mathExpr) {
+                  parts.push({type: 'math', params: [field.mathExpr]});
                 }
                 }
+                if (field.asExpr) {
+                  parts.push({type: 'alias', params: [field.asExpr]});
+                }
+                return parts;
+              });
+              delete target.fields;
+              _.each(target.groupBy, function(part) {
+                if (part.type === 'time' && part.interval) {
+                  part.params = [part.interval];
+                  delete part.interval;
+                }
+                if (part.type === 'tag' && part.key) {
+                  part.params = [part.key];
+                  delete part.key;
+                }
+              });
+
+              if (target.fill) {
+                target.groupBy.push({type: 'fill', params: [target.fill]});
+                delete target.fill;
               }
               }
             }
             }
-          });
+          }
         });
         });
-      }
+      });
+    }
 
 
-      // schema version 9 changes
-      if (oldVersion < 9) {
-        // move aliasYAxis changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
+    // schema version 9 changes
+    if (oldVersion < 9) {
+      // move aliasYAxis changes
+      panelUpgrades.push(function(panel) {
+        if (panel.type !== 'singlestat' && panel.thresholds !== '') {
+          return;
+        }
 
 
-          if (panel.thresholds) {
-            var k = panel.thresholds.split(",");
+        if (panel.thresholds) {
+          var k = panel.thresholds.split(',');
 
 
-            if (k.length >= 3) {
-              k.shift();
-              panel.thresholds = k.join(",");
-            }
+          if (k.length >= 3) {
+            k.shift();
+            panel.thresholds = k.join(',');
           }
           }
-        });
-      }
+        }
+      });
+    }
 
 
-      // schema version 10 changes
-      if (oldVersion < 10) {
-        // move aliasYAxis changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'table') { return; }
-
-          _.each(panel.styles, function(style) {
-            if (style.thresholds && style.thresholds.length >= 3) {
-              var k = style.thresholds;
-              k.shift();
-              style.thresholds = k;
-            }
-          });
-        });
-      }
+    // schema version 10 changes
+    if (oldVersion < 10) {
+      // move aliasYAxis changes
+      panelUpgrades.push(function(panel) {
+        if (panel.type !== 'table') {
+          return;
+        }
 
 
-      if (oldVersion < 12) {
-        // update template variables
-        _.each(this.templating.list, function(templateVariable) {
-          if (templateVariable.refresh) { templateVariable.refresh = 1; }
-          if (!templateVariable.refresh) { templateVariable.refresh = 0; }
-          if (templateVariable.hideVariable) {
-            templateVariable.hide = 2;
-          } else if (templateVariable.hideLabel) {
-            templateVariable.hide = 1;
+        _.each(panel.styles, function(style) {
+          if (style.thresholds && style.thresholds.length >= 3) {
+            var k = style.thresholds;
+            k.shift();
+            style.thresholds = k;
           }
           }
         });
         });
-      }
+      });
+    }
 
 
-      if (oldVersion < 12) {
-        // update graph yaxes changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'graph') { return; }
-          if (!panel.grid) { return; }
-
-          if (!panel.yaxes) {
-            panel.yaxes = [
-              {
-                show: panel['y-axis'],
-                min: panel.grid.leftMin,
-                max: panel.grid.leftMax,
-                logBase: panel.grid.leftLogBase,
-                format: panel.y_formats[0],
-                label: panel.leftYAxisLabel,
-              },
-              {
-                show: panel['y-axis'],
-                min: panel.grid.rightMin,
-                max: panel.grid.rightMax,
-                logBase: panel.grid.rightLogBase,
-                format: panel.y_formats[1],
-                label: panel.rightYAxisLabel,
-              }
-            ];
-
-            panel.xaxis = {
-              show: panel['x-axis'],
-            };
-
-            delete panel.grid.leftMin;
-            delete panel.grid.leftMax;
-            delete panel.grid.leftLogBase;
-            delete panel.grid.rightMin;
-            delete panel.grid.rightMax;
-            delete panel.grid.rightLogBase;
-            delete panel.y_formats;
-            delete panel.leftYAxisLabel;
-            delete panel.rightYAxisLabel;
-            delete panel['y-axis'];
-            delete panel['x-axis'];
-          }
-        });
-      }
+    if (oldVersion < 12) {
+      // update template variables
+      _.each(this.templating.list, function(templateVariable) {
+        if (templateVariable.refresh) {
+          templateVariable.refresh = 1;
+        }
+        if (!templateVariable.refresh) {
+          templateVariable.refresh = 0;
+        }
+        if (templateVariable.hideVariable) {
+          templateVariable.hide = 2;
+        } else if (templateVariable.hideLabel) {
+          templateVariable.hide = 1;
+        }
+      });
+    }
 
 
-      if (oldVersion < 13) {
-        // update graph yaxes changes
-        panelUpgrades.push(function(panel) {
-          if (panel.type !== 'graph') { return; }
-          if (!panel.grid) { return; }
-
-          panel.thresholds = [];
-          var t1: any = {}, t2: any = {};
-
-          if (panel.grid.threshold1 !== null) {
-            t1.value = panel.grid.threshold1;
-            if (panel.grid.thresholdLine) {
-              t1.line = true;
-              t1.lineColor = panel.grid.threshold1Color;
-              t1.colorMode = 'custom';
-            } else {
-              t1.fill = true;
-              t1.fillColor = panel.grid.threshold1Color;
-              t1.colorMode = 'custom';
-            }
+    if (oldVersion < 12) {
+      // update graph yaxes changes
+      panelUpgrades.push(function(panel) {
+        if (panel.type !== 'graph') {
+          return;
+        }
+        if (!panel.grid) {
+          return;
+        }
+
+        if (!panel.yaxes) {
+          panel.yaxes = [
+            {
+              show: panel['y-axis'],
+              min: panel.grid.leftMin,
+              max: panel.grid.leftMax,
+              logBase: panel.grid.leftLogBase,
+              format: panel.y_formats[0],
+              label: panel.leftYAxisLabel,
+            },
+            {
+              show: panel['y-axis'],
+              min: panel.grid.rightMin,
+              max: panel.grid.rightMax,
+              logBase: panel.grid.rightLogBase,
+              format: panel.y_formats[1],
+              label: panel.rightYAxisLabel,
+            },
+          ];
+
+          panel.xaxis = {
+            show: panel['x-axis'],
+          };
+
+          delete panel.grid.leftMin;
+          delete panel.grid.leftMax;
+          delete panel.grid.leftLogBase;
+          delete panel.grid.rightMin;
+          delete panel.grid.rightMax;
+          delete panel.grid.rightLogBase;
+          delete panel.y_formats;
+          delete panel.leftYAxisLabel;
+          delete panel.rightYAxisLabel;
+          delete panel['y-axis'];
+          delete panel['x-axis'];
+        }
+      });
+    }
+
+    if (oldVersion < 13) {
+      // update graph yaxes changes
+      panelUpgrades.push(function(panel) {
+        if (panel.type !== 'graph') {
+          return;
+        }
+        if (!panel.grid) {
+          return;
+        }
+
+        panel.thresholds = [];
+        var t1: any = {},
+          t2: any = {};
+
+        if (panel.grid.threshold1 !== null) {
+          t1.value = panel.grid.threshold1;
+          if (panel.grid.thresholdLine) {
+            t1.line = true;
+            t1.lineColor = panel.grid.threshold1Color;
+            t1.colorMode = 'custom';
+          } else {
+            t1.fill = true;
+            t1.fillColor = panel.grid.threshold1Color;
+            t1.colorMode = 'custom';
           }
           }
+        }
 
 
-          if (panel.grid.threshold2 !== null) {
-            t2.value = panel.grid.threshold2;
-            if (panel.grid.thresholdLine) {
-              t2.line = true;
-              t2.lineColor = panel.grid.threshold2Color;
-              t2.colorMode = 'custom';
-            } else {
-              t2.fill = true;
-              t2.fillColor = panel.grid.threshold2Color;
-              t2.colorMode = 'custom';
-            }
+        if (panel.grid.threshold2 !== null) {
+          t2.value = panel.grid.threshold2;
+          if (panel.grid.thresholdLine) {
+            t2.line = true;
+            t2.lineColor = panel.grid.threshold2Color;
+            t2.colorMode = 'custom';
+          } else {
+            t2.fill = true;
+            t2.fillColor = panel.grid.threshold2Color;
+            t2.colorMode = 'custom';
           }
           }
+        }
 
 
-          if (_.isNumber(t1.value)) {
-            if (_.isNumber(t2.value)) {
-              if (t1.value > t2.value) {
-                t1.op = t2.op = 'lt';
-                panel.thresholds.push(t1);
-                panel.thresholds.push(t2);
-              } else {
-                t1.op = t2.op = 'gt';
-                panel.thresholds.push(t1);
-                panel.thresholds.push(t2);
-              }
+        if (_.isNumber(t1.value)) {
+          if (_.isNumber(t2.value)) {
+            if (t1.value > t2.value) {
+              t1.op = t2.op = 'lt';
+              panel.thresholds.push(t1);
+              panel.thresholds.push(t2);
             } else {
             } else {
-              t1.op = 'gt';
+              t1.op = t2.op = 'gt';
               panel.thresholds.push(t1);
               panel.thresholds.push(t1);
+              panel.thresholds.push(t2);
             }
             }
+          } else {
+            t1.op = 'gt';
+            panel.thresholds.push(t1);
           }
           }
+        }
 
 
-          delete panel.grid.threshold1;
-          delete panel.grid.threshold1Color;
-          delete panel.grid.threshold2;
-          delete panel.grid.threshold2Color;
-          delete panel.grid.thresholdLine;
-        });
-      }
+        delete panel.grid.threshold1;
+        delete panel.grid.threshold1Color;
+        delete panel.grid.threshold2;
+        delete panel.grid.threshold2Color;
+        delete panel.grid.thresholdLine;
+      });
+    }
 
 
-      if (oldVersion < 14) {
-        this.graphTooltip = old.sharedCrosshair ? 1 : 0;
-      }
+    if (oldVersion < 14) {
+      this.graphTooltip = old.sharedCrosshair ? 1 : 0;
+    }
 
 
-      if (oldVersion < 16) {
-        this.upgradeToGridLayout(old);
-      }
+    if (oldVersion < 16) {
+      this.upgradeToGridLayout(old);
+    }
 
 
-      if (panelUpgrades.length === 0) {
-        return;
-      }
+    if (panelUpgrades.length === 0) {
+      return;
+    }
 
 
-      for (j = 0; j < this.panels.length; j++) {
-        for (k = 0; k < panelUpgrades.length; k++) {
-          panelUpgrades[k].call(this, this.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;
+  upgradeToGridLayout(old) {
+    let yPos = 0;
+    let widthFactor = GRID_COLUMN_COUNT / 12;
+    //let rowIds = 1000;
+    //
+
+    if (!old.rows) {
+      return;
+    }
+
+    for (let row of old.rows) {
+      let xPos = 0;
+      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 (!old.rows) {
-        return;
+      if (_.isString(height)) {
+        height = parseInt(height.replace('px', ''), 10);
       }
       }
 
 
-      for (let row of old.rows) {
-        let xPos = 0;
-        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);
-        }
-
-        const rowGridHeight = Math.ceil(height / CELL_HEIGHT);
+      const rowGridHeight = Math.ceil(height / GRID_CELL_HEIGHT);
 
 
-        for (let panel of row.panels) {
-          // should wrap to next row?
-          if (xPos + panel.span >= 12) {
-            yPos += rowGridHeight;
-          }
+      for (let panel of row.panels) {
+        const panelWidth = Math.floor(panel.span) * widthFactor;
 
 
-          panel.gridPos = { x: xPos, y: yPos, w: panel.span, h: rowGridHeight };
+        // should wrap to next row?
+        if (xPos + panelWidth >= GRID_COLUMN_COUNT) {
+          yPos += rowGridHeight;
+        }
 
 
-          delete panel.span;
+        panel.gridPos = {x: xPos, y: yPos, w: panelWidth, h: rowGridHeight};
 
 
-          xPos += panel.gridPos.w;
+        delete panel.span;
 
 
-          this.panels.push(new PanelModel(panel));
-        }
+        xPos += panel.gridPos.w;
 
 
-        yPos += rowGridHeight;
+        this.panels.push(new PanelModel(panel));
       }
       }
 
 
+      yPos += rowGridHeight;
     }
     }
+  }
 }
 }

+ 4 - 5
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import ReactGridLayout from 'react-grid-layout';
 import ReactGridLayout from 'react-grid-layout';
-import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard_model';
+import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT} from 'app/core/constants';
 import {DashboardPanel} from './DashboardPanel';
 import {DashboardPanel} from './DashboardPanel';
 import {DashboardModel} from '../dashboard_model';
 import {DashboardModel} from '../dashboard_model';
 import {PanelContainer} from './PanelContainer';
 import {PanelContainer} from './PanelContainer';
@@ -9,7 +9,6 @@ import {PanelModel} from '../panel_model';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import sizeMe from 'react-sizeme';
 import sizeMe from 'react-sizeme';
 
 
-const COLUMN_COUNT = 12;
 let lastGridWidth = 1200;
 let lastGridWidth = 1200;
 
 
 function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) {
 function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) {
@@ -32,9 +31,9 @@ function GridWrapper({size, layout, onLayoutChange, children, onResize, onResize
       measureBeforeMount={false}
       measureBeforeMount={false}
       containerPadding={[0, 0]}
       containerPadding={[0, 0]}
       useCSSTransforms={false}
       useCSSTransforms={false}
-      margin={[CELL_VMARGIN, CELL_VMARGIN]}
-      cols={COLUMN_COUNT}
-      rowHeight={CELL_HEIGHT}
+      margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
+      cols={GRID_COLUMN_COUNT}
+      rowHeight={GRID_CELL_HEIGHT}
       draggableHandle=".grid-drag-handle"
       draggableHandle=".grid-drag-handle"
       layout={layout}
       layout={layout}
       onResize={onResize}
       onResize={onResize}

+ 13 - 4
public/app/features/dashboard/repeat_option/repeat_option.ts

@@ -1,11 +1,9 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import {coreModule} from 'app/core/core';
 import {coreModule} from 'app/core/core';
 
 
 var template = `
 var template = `
 <div class="gf-form-select-wrapper max-width-13">
 <div class="gf-form-select-wrapper max-width-13">
-<select class="gf-form-input" ng-model="model.repeat" ng-options="f.value as f.text for f in variables">
-<option value=""></option>
+  <select class="gf-form-input" ng-model="model.repeat" ng-options="f.value as f.text for f in variables" ng-change="optionChanged()">
+  <option value=""></option>
 </div>
 </div>
 `;
 `;
 
 
@@ -29,6 +27,17 @@ function dashRepeatOptionDirective(variableSrv) {
       }
       }
 
 
       scope.variables.unshift({text: 'Disabled', value: null});
       scope.variables.unshift({text: 'Disabled', value: null});
+
+      // if repeat is set and no direction set to horizontal
+      if (scope.panel.repeat && !scope.panel.repeatDirection) {
+        scope.panel.repeatDirection = 'h';
+      }
+
+      scope.optionChanged = function() {
+        if (scope.panel.repeat) {
+          scope.panel.repeatDirection = 'h';
+        }
+      };
     }
     }
   };
   };
 }
 }

+ 110 - 4
public/app/features/dashboard/specs/dashboard_model_specs.ts

@@ -400,20 +400,126 @@ describe('DashboardModel', function() {
   });
   });
 
 
   describe('updateSubmenuVisibility with hidden annotation toggle', function() {
   describe('updateSubmenuVisibility with hidden annotation toggle', function() {
-    var model;
+    var dashboard;
 
 
     beforeEach(function() {
     beforeEach(function() {
-      model = new DashboardModel({
+      dashboard = new DashboardModel({
         annotations: {
         annotations: {
           list: [{hide: true}]
           list: [{hide: true}]
         }
         }
       });
       });
-      model.updateSubmenuVisibility();
+      dashboard.updateSubmenuVisibility();
     });
     });
 
 
     it('should not enable submmenu', function() {
     it('should not enable submmenu', function() {
-      expect(model.meta.submenuEnabled).to.be(false);
+      expect(dashboard.meta.submenuEnabled).to.be(false);
     });
     });
   });
   });
 
 
+  describe('given dashboard with panel repeat', function(ctx) {
+    var dashboard;
+
+    beforeEach(function() {
+      dashboard = new DashboardModel({
+        panels: [{id: 2, repeat: 'apps'}],
+        templating:  {
+          list: [{
+            name: 'apps',
+            current: {
+              text: 'se1, se2, se3',
+              value: ['se1', 'se2', 'se3']
+            },
+            options: [
+              {text: 'se1', value: 'se1', selected: true},
+              {text: 'se2', value: 'se2', selected: true},
+              {text: 'se3', value: 'se3', selected: true},
+              {text: 'se4', value: 'se4', selected: false}
+            ]
+          }]
+        }
+      });
+      dashboard.processRepeats();
+    });
+
+    it('should repeat panel 3 times', function() {
+      expect(dashboard.panels.length).to.be(3);
+    });
+
+    it('should mark panel repeated', function() {
+      expect(dashboard.panels[0].repeat).to.be('apps');
+      expect(dashboard.panels[1].repeatPanelId).to.be(2);
+    });
+
+    it('should set scopedVars on panels', function() {
+      expect(dashboard.panels[0].scopedVars.apps.value).to.be('se1');
+      expect(dashboard.panels[1].scopedVars.apps.value).to.be('se2');
+      expect(dashboard.panels[2].scopedVars.apps.value).to.be('se3');
+    });
+
+    describe('After a second iteration', function() {
+      var repeatedPanelAfterIteration1;
+
+      beforeEach(function() {
+        repeatedPanelAfterIteration1 = dashboard.panels[1];
+        dashboard.panels[0].fill = 10;
+        dashboard.processRepeats();
+      });
+
+      it('reused panel should copy properties from source', function() {
+        expect(dashboard.panels[1].fill).to.be(10);
+      });
+
+      it('should have same panel count', function() {
+        expect(dashboard.panels.length).to.be(3);
+      });
+    });
+
+    describe('After a second iteration with different variable', function() {
+      beforeEach(function() {
+        dashboard.templating.list.push({
+          name: 'server',
+          current: { text: 'se1, se2, se3', value: ['se1']},
+          options: [{text: 'se1', value: 'se1', selected: true}]
+        });
+        dashboard.panels[0].repeat = "server";
+        dashboard.processRepeats();
+      });
+
+      it('should remove scopedVars value for last variable', function() {
+        expect(dashboard.panels[0].scopedVars.apps).to.be(undefined);
+      });
+
+      it('should have new variable value in scopedVars', function() {
+        expect(dashboard.panels[0].scopedVars.server.value).to.be("se1");
+      });
+    });
+
+    describe('After a second iteration and selected values reduced', function() {
+      beforeEach(function() {
+        dashboard.templating.list[0].options[1].selected = false;
+        dashboard.processRepeats();
+      });
+
+      it('should clean up repeated panel', function() {
+        expect(dashboard.panels.length).to.be(2);
+      });
+    });
+
+    describe('After a second iteration and panel repeat is turned off', function() {
+      beforeEach(function() {
+        dashboard.panels[0].repeat = null;
+        dashboard.processRepeats();
+      });
+
+      it('should clean up repeated panel', function() {
+        expect(dashboard.panels.length).to.be(1);
+      });
+
+      it('should remove scoped vars from reused panel', function() {
+        expect(dashboard.panels[0].scopedVars).to.be(undefined);
+      });
+    });
+
+  });
+
 });
 });

+ 0 - 287
public/app/features/dashboard/specs/dynamic_dashboard_srv_specs.ts

@@ -1,287 +0,0 @@
-// import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
-//
-// import '../dashboard_srv';
-// import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
-//
-// function dynamicDashScenario(desc, func)  {
-//
-//   describe.skip(desc, function() {
-//     var ctx: any = {};
-//
-//     ctx.setup = function (setupFunc) {
-//
-//       beforeEach(angularMocks.module('grafana.core'));
-//       beforeEach(angularMocks.module('grafana.services'));
-//       beforeEach(angularMocks.module(function($provide) {
-//         $provide.value('contextSrv', {
-//           user: { timezone: 'utc'}
-//         });
-//       }));
-//
-//       beforeEach(angularMocks.inject(function(dashboardSrv) {
-//         ctx.dashboardSrv = dashboardSrv;
-//
-//         var model = {
-//           rows: [],
-//           templating: { list: [] }
-//         };
-//
-//         setupFunc(model);
-//         ctx.dash = ctx.dashboardSrv.create(model);
-//         ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
-//         ctx.dynamicDashboardSrv.init(ctx.dash);
-//         ctx.dynamicDashboardSrv.process();
-//         ctx.rows = ctx.dash.rows;
-//       }));
-//     };
-//
-//     func(ctx);
-//   });
-// }
-//
-// dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
-//   ctx.setup(function(dash) {
-//     dash.rows.push({
-//       panels: [{id: 2, repeat: 'apps'}]
-//     });
-//     dash.templating.list.push({
-//       name: 'apps',
-//       current: {
-//         text: 'se1, se2, se3',
-//         value: ['se1', 'se2', 'se3']
-//       },
-//       options: [
-//         {text: 'se1', value: 'se1', selected: true},
-//         {text: 'se2', value: 'se2', selected: true},
-//         {text: 'se3', value: 'se3', selected: true},
-//         {text: 'se4', value: 'se4', selected: false}
-//       ]
-//     });
-//   });
-//
-//   it('should repeat panel one time', function() {
-//     expect(ctx.rows[0].panels.length).to.be(3);
-//   });
-//
-//   it('should mark panel repeated', function() {
-//     expect(ctx.rows[0].panels[0].repeat).to.be('apps');
-//     expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
-//   });
-//
-//   it('should set scopedVars on panels', function() {
-//     expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
-//     expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
-//     expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
-//   });
-//
-//   describe('After a second iteration', function() {
-//     var repeatedPanelAfterIteration1;
-//
-//     beforeEach(function() {
-//       repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
-//       ctx.rows[0].panels[0].fill = 10;
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should have reused same panel instances', function() {
-//       expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
-//     });
-//
-//     it('reused panel should copy properties from source', function() {
-//       expect(ctx.rows[0].panels[1].fill).to.be(10);
-//     });
-//
-//     it('should have same panel count', function() {
-//       expect(ctx.rows[0].panels.length).to.be(3);
-//     });
-//   });
-//
-//   describe('After a second iteration with different variable', function() {
-//     beforeEach(function() {
-//       ctx.dash.templating.list.push({
-//         name: 'server',
-//         current: { text: 'se1, se2, se3', value: ['se1']},
-//         options: [{text: 'se1', value: 'se1', selected: true}]
-//       });
-//       ctx.rows[0].panels[0].repeat = "server";
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should remove scopedVars value for last variable', function() {
-//       expect(ctx.rows[0].panels[0].scopedVars.apps).to.be(undefined);
-//     });
-//
-//     it('should have new variable value in scopedVars', function() {
-//       expect(ctx.rows[0].panels[0].scopedVars.server.value).to.be("se1");
-//     });
-//   });
-//
-//   describe('After a second iteration and selected values reduced', function() {
-//     beforeEach(function() {
-//       ctx.dash.templating.list[0].options[1].selected = false;
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should clean up repeated panel', function() {
-//       expect(ctx.rows[0].panels.length).to.be(2);
-//     });
-//   });
-//
-//   describe('After a second iteration and panel repeat is turned off', function() {
-//     beforeEach(function() {
-//       ctx.rows[0].panels[0].repeat = null;
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should clean up repeated panel', function() {
-//       expect(ctx.rows[0].panels.length).to.be(1);
-//     });
-//
-//     it('should remove scoped vars from reused panel', function() {
-//       expect(ctx.rows[0].panels[0].scopedVars).to.be(undefined);
-//     });
-//   });
-//
-// });
-//
-// dynamicDashScenario('given dashboard with row repeat', function(ctx) {
-//   ctx.setup(function(dash) {
-//     dash.rows.push({
-//       repeat: 'servers',
-//       panels: [{id: 2}]
-//     });
-//     dash.rows.push({panels: []});
-//     dash.templating.list.push({
-//       name: 'servers',
-//       current: {
-//         text: 'se1, se2',
-//         value: ['se1', 'se2']
-//       },
-//       options: [
-//         {text: 'se1', value: 'se1', selected: true},
-//         {text: 'se2', value: 'se2', selected: true},
-//       ]
-//     });
-//   });
-//
-//   it('should repeat row one time', function() {
-//     expect(ctx.rows.length).to.be(3);
-//   });
-//
-//   it('should keep panel ids on first row', function() {
-//     expect(ctx.rows[0].panels[0].id).to.be(2);
-//   });
-//
-//   it('should keep first row as repeat', function() {
-//     expect(ctx.rows[0].repeat).to.be('servers');
-//   });
-//
-//   it('should clear repeat field on repeated row', function() {
-//     expect(ctx.rows[1].repeat).to.be(null);
-//   });
-//
-//   it('should add scopedVars to rows', function() {
-//     expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
-//     expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
-//   });
-//
-//   it('should generate a repeartRowId based on repeat row index', function() {
-//     expect(ctx.rows[1].repeatRowId).to.be(1);
-//     expect(ctx.rows[1].repeatIteration).to.be(ctx.dynamicDashboardSrv.iteration);
-//   });
-//
-//   it('should set scopedVars on row panels', function() {
-//     expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
-//     expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
-//   });
-//
-//   describe('After a second iteration', function() {
-//     var repeatedRowAfterFirstIteration;
-//
-//     beforeEach(function() {
-//       repeatedRowAfterFirstIteration = ctx.rows[1];
-//       ctx.rows[0].height = 500;
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should still only have 2 rows', function() {
-//       expect(ctx.rows.length).to.be(3);
-//     });
-//
-//     it.skip('should have updated props from source', function() {
-//       expect(ctx.rows[1].height).to.be(500);
-//     });
-//
-//     it('should reuse row instance', function() {
-//       expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
-//     });
-//   });
-//
-//   describe('After a second iteration and selected values reduced', function() {
-//     beforeEach(function() {
-//       ctx.dash.templating.list[0].options[1].selected = false;
-//       ctx.dynamicDashboardSrv.process();
-//     });
-//
-//     it('should remove repeated second row', function() {
-//       expect(ctx.rows.length).to.be(2);
-//     });
-//   });
-// });
-//
-// dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
-//   ctx.setup(function(dash) {
-//     dash.rows.push({
-//       repeat: 'servers',
-//       panels: [{id: 2, repeat: 'metric'}]
-//     });
-//     dash.templating.list.push({
-//       name: 'servers',
-//       current: { text: 'se1, se2', value: ['se1', 'se2'] },
-//       options: [
-//         {text: 'se1', value: 'se1', selected: true},
-//         {text: 'se2', value: 'se2', selected: true},
-//       ]
-//     });
-//     dash.templating.list.push({
-//       name: 'metric',
-//       current: { text: 'm1, m2', value: ['m1', 'm2'] },
-//       options: [
-//         {text: 'm1', value: 'm1', selected: true},
-//         {text: 'm2', value: 'm2', selected: true},
-//       ]
-//     });
-//   });
-//
-//   it('should repeat row one time', function() {
-//     expect(ctx.rows.length).to.be(2);
-//   });
-//
-//   it('should repeat panel on both rows', function() {
-//     expect(ctx.rows[0].panels.length).to.be(2);
-//     expect(ctx.rows[1].panels.length).to.be(2);
-//   });
-//
-//   it('should keep panel ids on first row', function() {
-//     expect(ctx.rows[0].panels[0].id).to.be(2);
-//   });
-//
-//   it('should mark second row as repeated', function() {
-//     expect(ctx.rows[0].repeat).to.be('servers');
-//   });
-//
-//   it('should clear repeat field on repeated row', function() {
-//     expect(ctx.rows[1].repeat).to.be(null);
-//   });
-//
-//   it('should generate a repeartRowId based on repeat row index', function() {
-//     expect(ctx.rows[1].repeatRowId).to.be(1);
-//   });
-//
-//   it('should set scopedVars on row panels', function() {
-//     expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
-//     expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
-//   });
-//
-// });
-

+ 2 - 2
public/app/features/panel/panel_ctrl.ts

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
 import {appEvents, profiler} from 'app/core/core';
 import {appEvents, profiler} from 'app/core/core';
 import Remarkable from 'remarkable';
 import Remarkable from 'remarkable';
-import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/dashboard_model';
+import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
 
 
 const TITLE_HEIGHT = 25;
 const TITLE_HEIGHT = 25;
 const EMPTY_TITLE_HEIGHT = 9;
 const EMPTY_TITLE_HEIGHT = 9;
@@ -163,7 +163,7 @@ export class PanelCtrl {
        var fullscreenHeight = Math.floor(docHeight * 0.8);
        var fullscreenHeight = Math.floor(docHeight * 0.8);
        this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
        this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
     } else {
     } else {
-      this.containerHeight = this.panel.gridPos.h * CELL_HEIGHT + ((this.panel.gridPos.h-1) * CELL_VMARGIN);
+      this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + ((this.panel.gridPos.h-1) * GRID_CELL_VMARGIN);
     }
     }
 
 
     this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
     this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));

+ 8 - 8
public/app/partials/panelgeneral.html

@@ -9,24 +9,24 @@
 			<span class="gf-form-label width-7">Description</span>
 			<span class="gf-form-label width-7">Description</span>
 			<textarea class="gf-form-input width-25" rows="3" ng-model="ctrl.panel.description"  placeholder="Panel description, supports markdown & links"></textarea>
 			<textarea class="gf-form-input width-25" rows="3" ng-model="ctrl.panel.description"  placeholder="Panel description, supports markdown & links"></textarea>
 		</div>
 		</div>
+    <gf-form-switch class="gf-form" label-class="width-7" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
 	</div>
 	</div>
 
 
   <div class="section gf-form-group">
   <div class="section gf-form-group">
-    <h5 class="section-heading">Options</h5>
-		<gf-form-switch class="gf-form" label-class="width-8" switch-class="max-width-6" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
+    <h5 class="section-heading">Repeat</h5>
 		<div class="gf-form">
 		<div class="gf-form">
-			<span class="gf-form-label width-8">Repeat Panel</span>
+			<span class="gf-form-label width-9">For each value of</span>
 			<dash-repeat-option model="ctrl.panel"></dash-repeat-option>
 			<dash-repeat-option model="ctrl.panel"></dash-repeat-option>
 		</div>
 		</div>
-		<div class="gf-form">
-			<span class="gf-form-label width-8">Min width</span>
+		<div class="gf-form" ng-show="ctrl.panel.repeat">
+			<span class="gf-form-label width-9">Min width</span>
 			<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12]">
 			<select class="gf-form-input" ng-model="ctrl.panel.minSpan" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12]">
 				<option value=""></option>
 				<option value=""></option>
 			</select>
 			</select>
 		</div>
 		</div>
-    <div class="gf-form">
-			<span class="gf-form-label width-8">Direction</span>
-			<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f for f in ['X', 'Y']">
+    <div class="gf-form" ng-show="ctrl.panel.repeat">
+			<span class="gf-form-label width-9">Direction</span>
+			<select class="gf-form-input" ng-model="ctrl.panel.repeatDirection" ng-options="f.value as f.text for f in [{value: 'v', text: 'Vertical'}, {value: 'h', text: 'Horizontal'}]">
 				<option value=""></option>
 				<option value=""></option>
 			</select>
 			</select>
 		</div>
 		</div>

+ 5 - 5
public/dashboards/home.json

@@ -21,8 +21,8 @@
       "transparent": true,
       "transparent": true,
       "type": "text",
       "type": "text",
       "gridPos": {
       "gridPos": {
-        "w": 12,
-        "h": 2,
+        "w": 24,
+        "h": 3,
         "x": 0,
         "x": 0,
         "y": 0
         "y": 0
       }
       }
@@ -42,7 +42,7 @@
       "transparent": false,
       "transparent": false,
       "type": "dashlist",
       "type": "dashlist",
       "gridPos": {
       "gridPos": {
-        "w": 7,
+        "w": 12,
         "h": 17,
         "h": 17,
         "x": 0,
         "x": 0,
         "y": 6
         "y": 6
@@ -57,9 +57,9 @@
       "transparent": false,
       "transparent": false,
       "type": "pluginlist",
       "type": "pluginlist",
       "gridPos": {
       "gridPos": {
-        "w": 5,
+        "w": 12,
         "h": 17,
         "h": 17,
-        "x": 7,
+        "x": 12,
         "y": 6
         "y": 6
       }
       }
     }
     }