소스 검색

progress on new design

Torkel Ödegaard 9 년 전
부모
커밋
9c50893221

+ 2 - 1
package.json

@@ -79,6 +79,7 @@
     "tether": "^1.2.0",
     "tether-drop": "^1.4.2",
     "tslint": "^3.4.0",
-    "typescript": "^1.7.5"
+    "typescript": "^1.7.5",
+    "virtual-scroll": "^1.1.1"
   }
 }

+ 211 - 0
public/app/core/components/scroll/scroll.ts

@@ -0,0 +1,211 @@
+// ///<reference path="../../headers/common.d.ts" />
+//
+// import _ from 'lodash';
+//
+// var objectAssign = require('object-assign');
+// var Emitter = require('tiny-emitter');
+// var Lethargy = require('lethargy').Lethargy;
+// var support = require('./support');
+// var clone = require('./clone');
+// var bindAll = require('bindall-standalone');
+// var EVT_ID = 'virtualscroll';
+//
+// var keyCodes = {
+//     LEFT: 37,
+//     UP: 38,
+//     RIGHT: 39,
+//     DOWN: 40
+// };
+//
+// function VirtualScroll(this: any, options) {
+//     _.bindAll(this, '_onWheel', '_onMouseWheel', '_onTouchStart', '_onTouchMove', '_onKeyDown');
+//
+//     this.el = window;
+//     if (options && options.el) {
+//         this.el = options.el;
+//         delete options.el;
+//     }
+//
+//     this.options = _.assign({
+//         mouseMultiplier: 1,
+//         touchMultiplier: 2,
+//         firefoxMultiplier: 15,
+//         keyStep: 120,
+//         preventTouch: false,
+//         unpreventTouchClass: 'vs-touchmove-allowed',
+//         limitInertia: false
+//     }, options);
+//
+//     if (this.options.limitInertia) this._lethargy = new Lethargy();
+//
+//     this._emitter = new Emitter();
+//     this._event = {
+//         y: 0,
+//         x: 0,
+//         deltaX: 0,
+//         deltaY: 0
+//     };
+//
+//     this.touchStartX = null;
+//     this.touchStartY = null;
+//     this.bodyTouchAction = null;
+// }
+//
+// VirtualScroll.prototype._notify = function(e) {
+//     var evt = this._event;
+//     evt.x += evt.deltaX;
+//     evt.y += evt.deltaY;
+//
+//    this._emitter.emit(EVT_ID, {
+//         x: evt.x,
+//         y: evt.y,
+//         deltaX: evt.deltaX,
+//         deltaY: evt.deltaY,
+//         originalEvent: e
+//    });
+// };
+//
+// VirtualScroll.prototype._onWheel = function(e) {
+//     var options = this.options;
+//     if (this._lethargy && this._lethargy.check(e) === false) return;
+//
+//     var evt = this._event;
+//
+//     // In Chrome and in Firefox (at least the new one)
+//     evt.deltaX = e.wheelDeltaX || e.deltaX * -1;
+//     evt.deltaY = e.wheelDeltaY || e.deltaY * -1;
+//
+//     // for our purpose deltamode = 1 means user is on a wheel mouse, not touch pad
+//     // real meaning: https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#Delta_modes
+//     if(support.isFirefox && e.deltaMode == 1) {
+//         evt.deltaX *= options.firefoxMultiplier;
+//         evt.deltaY *= options.firefoxMultiplier;
+//     }
+//
+//     evt.deltaX *= options.mouseMultiplier;
+//     evt.deltaY *= options.mouseMultiplier;
+//
+//     this._notify(e);
+// };
+//
+// VirtualScroll.prototype._onMouseWheel = function(e) {
+//     if (this.options.limitInertia && this._lethargy.check(e) === false) return;
+//
+//     var evt = this._event;
+//
+//     // In Safari, IE and in Chrome if 'wheel' isn't defined
+//     evt.deltaX = (e.wheelDeltaX) ? e.wheelDeltaX : 0;
+//     evt.deltaY = (e.wheelDeltaY) ? e.wheelDeltaY : e.wheelDelta;
+//
+//     this._notify(e);
+// };
+//
+// VirtualScroll.prototype._onTouchStart = function(e) {
+//     var t = (e.targetTouches) ? e.targetTouches[0] : e;
+//     this.touchStartX = t.pageX;
+//     this.touchStartY = t.pageY;
+// };
+//
+// VirtualScroll.prototype._onTouchMove = function(e) {
+//     var options = this.options;
+//     if(options.preventTouch
+//         && !e.target.classList.contains(options.unpreventTouchClass)) {
+//         e.preventDefault();
+//     }
+//
+//     var evt = this._event;
+//
+//     var t = (e.targetTouches) ? e.targetTouches[0] : e;
+//
+//     evt.deltaX = (t.pageX - this.touchStartX) * options.touchMultiplier;
+//     evt.deltaY = (t.pageY - this.touchStartY) * options.touchMultiplier;
+//
+//     this.touchStartX = t.pageX;
+//     this.touchStartY = t.pageY;
+//
+//     this._notify(e);
+// };
+//
+// VirtualScroll.prototype._onKeyDown = function(e) {
+//     var evt = this._event;
+//     evt.deltaX = evt.deltaY = 0;
+//
+//     switch(e.keyCode) {
+//         case keyCodes.LEFT:
+//         case keyCodes.UP:
+//             evt.deltaY = this.options.keyStep;
+//             break;
+//
+//         case keyCodes.RIGHT:
+//         case keyCodes.DOWN:
+//             evt.deltaY = - this.options.keyStep;
+//             break;
+//
+//         default:
+//             return;
+//     }
+//
+//     this._notify(e);
+// };
+//
+// VirtualScroll.prototype._bind = function() {
+//     if(support.hasWheelEvent) this.el.addEventListener('wheel', this._onWheel);
+//     if(support.hasMouseWheelEvent) this.el.addEventListener('mousewheel', this._onMouseWheel);
+//
+//     if(support.hasTouch) {
+//         this.el.addEventListener('touchstart', this._onTouchStart);
+//         this.el.addEventListener('touchmove', this._onTouchMove);
+//     }
+//
+//     if(support.hasPointer && support.hasTouchWin) {
+//         this.bodyTouchAction = document.body.style.msTouchAction;
+//         document.body.style.msTouchAction = 'none';
+//         this.el.addEventListener('MSPointerDown', this._onTouchStart, true);
+//         this.el.addEventListener('MSPointerMove', this._onTouchMove, true);
+//     }
+//
+//     if(support.hasKeyDown) document.addEventListener('keydown', this._onKeyDown);
+// };
+//
+// VirtualScroll.prototype._unbind = function() {
+//     if(support.hasWheelEvent) this.el.removeEventListener('wheel', this._onWheel);
+//     if(support.hasMouseWheelEvent) this.el.removeEventListener('mousewheel', this._onMouseWheel);
+//
+//     if(support.hasTouch) {
+//         this.el.removeEventListener('touchstart', this._onTouchStart);
+//         this.el.removeEventListener('touchmove', this._onTouchMove);
+//     }
+//
+//     if(support.hasPointer && support.hasTouchWin) {
+//         document.body.style.msTouchAction = this.bodyTouchAction;
+//         this.el.removeEventListener('MSPointerDown', this._onTouchStart, true);
+//         this.el.removeEventListener('MSPointerMove', this._onTouchMove, true);
+//     }
+//
+//     if(support.hasKeyDown) document.removeEventListener('keydown', this._onKeyDown);
+// };
+//
+// VirtualScroll.prototype.on = function(cb, ctx) {
+//   this._emitter.on(EVT_ID, cb, ctx);
+//
+//   var events = this._emitter.e;
+//   if (events && events[EVT_ID] && events[EVT_ID].length === 1) this._bind();
+// };
+//
+// VirtualScroll.prototype.off = function(cb, ctx) {
+//   this._emitter.off(EVT_ID, cb, ctx);
+//
+//   var events = this._emitter.e;
+//   if (!events[EVT_ID] || events[EVT_ID].length <= 0) this._unbind();
+// };
+//
+// VirtualScroll.prototype.reset = function() {
+//     var evt = this._event;
+//     evt.x = 0;
+//     evt.y = 0;
+// };
+//
+// VirtualScroll.prototype.destroy = function() {
+//     this._emitter.off();
+//     this._unbind();
+// };

+ 58 - 0
public/app/features/dashboard/row/add_panel.html

@@ -0,0 +1,58 @@
+<div class="dash-row-options">
+
+  <div class="gf-form-inline">
+    <div class="gf-form">
+      <span class="gf-form-label">Panel search</span>
+      <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()"></input>
+    </div>
+  </div>
+
+  <div class="add-panel-panels-scroll">
+    <div class="add-panel-panels">
+      <div class="add-panel-item" ng-repeat="panel in ctrl.panelHits" ng-class="{active: $index === ctrl.activeIndex}" ng-click="ctrl.addPanel(panel)">
+        <img class="add-panel-item-img" ng-src="{{panel.info.logos.small}}"></img>
+        <div class="add-panel-item-name">{{panel.name}}</div>
+      </div>
+    </div>
+  </div>
+
+</div>
+
+<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 1">
+  <div class="gf-form-group">
+    <h5 class="section-heading">Options</h5>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Title</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.title'></input>
+      </div>
+      <div class="gf-form">
+        <label class="gf-form-label width-6">Size</label>
+        <div class="gf-form-select-wrapper">
+          <select class="input-small gf-form-input" ng-model="ctrl.row.titleSize" ng-options="f for f in ctrl.fontSizes"></select>
+        </div>
+      </div>
+      <gf-form-switch class="gf-form" label="Show" checked="ctrl.row.showTitle">
+      </gf-form-switch>
+    </div>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Height</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.height'></input>
+      </div>
+    </div>
+  </div>
+
+  <h5 class="section-heading">Row Templating</h5>
+
+  <div class="gf-form-group">
+    <div class="gf-form">
+      <span class="gf-form-label">Repeat Row</span>
+      <div class="gf-form-select-wrapper max-width-10">
+        <select class="gf-form-input" ng-model="row.repeat" ng-options="f.name as f.name for f in dashboard.templating.list">
+          <option value=""></option>
+      </div>
+    </div>
+  </div>
+</div>
+

+ 113 - 0
public/app/features/dashboard/row/add_panel.ts

@@ -0,0 +1,113 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import _ from 'lodash';
+
+import config from 'app/core/config';
+import {coreModule, appEvents} from 'app/core/core';
+// import VirtualScroll from 'virtual-scroll';
+// console.log(VirtualScroll);
+
+export class AddPanelCtrl {
+  row: any;
+  dashboard: any;
+  rowCtrl: any;
+  allPanels: any;
+  panelHits: any;
+  activeIndex: any;
+  panelSearch: any;
+
+  /** @ngInject */
+  constructor(private $scope, private $timeout, private $rootScope) {
+    this.row = this.rowCtrl.row;
+    this.dashboard = this.rowCtrl.dashboard;
+    this.allPanels = _.orderBy(_.map(config.panels, item => item), 'sort');
+    this.panelHits = this.allPanels;
+    this.activeIndex = 0;
+  }
+
+  keyDown(evt) {
+    if (evt.keyCode === 27) {
+      this.rowCtrl.showOptions = false;
+      return;
+    }
+
+    if (evt.keyCode === 40 || evt.keyCode === 39) {
+      this.moveSelection(1);
+    }
+
+    if (evt.keyCode === 38 || evt.keyCode === 37) {
+      this.moveSelection(-1);
+    }
+
+    if (evt.keyCode === 13) {
+      var selectedPanel = this.panelHits[this.activeIndex];
+      if (selectedPanel) {
+        this.addPanel(selectedPanel);
+      }
+    }
+  }
+
+  moveSelection(direction) {
+    var max = this.panelHits.length;
+    var newIndex = this.activeIndex + direction;
+    this.activeIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
+  }
+
+  panelSearchChanged() {
+    var items = this.allPanels.slice();
+    var startsWith = [];
+    var contains = [];
+    var searchLower = this.panelSearch.toLowerCase();
+    var item;
+
+    while (item = items.shift()) {
+      var nameLower = item.name.toLowerCase();
+      if (nameLower.indexOf(searchLower) === 0) {
+        startsWith.push(item);
+      } else if (nameLower.indexOf(searchLower) !== -1) {
+        contains.push(item);
+      }
+    }
+
+    this.panelHits = startsWith.concat(contains);
+    this.activeIndex = 0;
+  }
+
+  addPanel(panelPluginInfo) {
+    var defaultSpan = 12;
+    var _as = 12 - this.dashboard.rowSpan(this.row);
+
+    var panel = {
+      id: null,
+      title: config.new_panel_title,
+      error: false,
+      span: _as < defaultSpan && _as > 0 ? _as : defaultSpan,
+      editable: true,
+      type: panelPluginInfo.id,
+      isNew: true,
+    };
+
+    this.rowCtrl.dropView = 0;
+    this.dashboard.addPanel(panel, this.row);
+    this.$timeout(() => {
+      this.$rootScope.appEvent('panel-change-view', {
+        fullscreen: true, edit: true, panelId: panel.id
+      });
+    });
+  }
+}
+
+export function addPanelDirective() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/dashboard/row/add_panel.html',
+    controller: AddPanelCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      rowCtrl: "=",
+    },
+  };
+}
+
+coreModule.directive('dashRowAddPanel', addPanelDirective);

+ 26 - 64
public/app/features/dashboard/row/options.html

@@ -1,77 +1,39 @@
 <div class="dash-row-options">
 
-  <div class="edit-tab-with-sidemenu">
-    <aside class="edit-sidemenu-aside">
-      <ul class="edit-sidemenu">
-        <li ng-class="{active: ctrl.subTabIndex === 0}">
-          <a ng-click="ctrl.subTabIndex = 0">Add Panel</a>
-        </li>
-        <li ng-class="{active: ctrl.subTabIndex === 1}">
-          <a ng-click="ctrl.subTabIndex = 1">Row Options</a>
-        </li>
-        <li ng-class="{active: ctrl.subTabIndex === 2}">
-          <a ng-click="ctrl.deleteRow()">Delete</a>
-        </li>
-      </ul>
-    </aside>
-
-    <div class="edit-tab-content" ng-if="ctrl.subTabIndex === 0">
-      <h5 class="section-heading">Add Panel</h5>
-      <div class="gf-form-inline">
-        <div class="gf-form">
-          <span class="gf-form-label width-6">Search</span>
-          <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()"></input>
-        </div>
+  <div class="gf-form-group">
+    <h5 class="section-heading">Options</h5>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Title</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.title'></input>
       </div>
-
-      <div class="add-panel-panels-wrapper">
-        <div class="add-panel-panels">
-          <div class="add-panel-item" ng-repeat="panel in ctrl.panelHits" ng-class="{active: $index === ctrl.activeIndex}" ng-click="ctrl.addPanel(panel)">
-            <img class="add-panel-item-img" ng-src="{{panel.info.logos.small}}"></img>
-            <div class="add-panel-item-name">{{panel.name}}</div>
-          </div>
+      <div class="gf-form">
+        <label class="gf-form-label width-6">Size</label>
+        <div class="gf-form-select-wrapper">
+          <select class="input-small gf-form-input" ng-model="ctrl.row.titleSize" ng-options="f for f in ctrl.fontSizes"></select>
         </div>
       </div>
-
+      <gf-form-switch class="gf-form" label="Show" checked="ctrl.row.showTitle">
+      </gf-form-switch>
     </div>
-
-    <div class="edit-tab-content" ng-if="ctrl.subTabIndex === 1">
-      <div class="gf-form-group">
-        <h5 class="section-heading">Options</h5>
-        <div class="gf-form-inline">
-          <div class="gf-form">
-            <span class="gf-form-label width-6">Title</span>
-            <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.title'></input>
-          </div>
-          <div class="gf-form">
-            <label class="gf-form-label width-6">Size</label>
-            <div class="gf-form-select-wrapper">
-              <select class="input-small gf-form-input" ng-model="ctrl.row.titleSize" ng-options="f for f in ctrl.fontSizes"></select>
-            </div>
-          </div>
-          <gf-form-switch class="gf-form" label="Show" checked="ctrl.row.showTitle">
-          </gf-form-switch>
-        </div>
-        <div class="gf-form-inline">
-          <div class="gf-form">
-            <span class="gf-form-label width-6">Height</span>
-            <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.height'></input>
-          </div>
-        </div>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-6">Height</span>
+        <input type="text" class="gf-form-input max-width-14" ng-model='ctrl.row.height'></input>
       </div>
+    </div>
+  </div>
 
-      <h5 class="section-heading">Row Templating</h5>
+  <h5 class="section-heading">Row Templating</h5>
 
-      <div class="gf-form-group">
-        <div class="gf-form">
-          <span class="gf-form-label">Repeat Row</span>
-          <div class="gf-form-select-wrapper max-width-10">
-            <select class="gf-form-input" ng-model="row.repeat" ng-options="f.name as f.name for f in dashboard.templating.list">
-              <option value=""></option>
-          </div>
-        </div>
+  <div class="gf-form-group">
+    <div class="gf-form">
+      <span class="gf-form-label">Repeat Row</span>
+      <div class="gf-form-select-wrapper max-width-10">
+        <select class="gf-form-input" ng-model="row.repeat" ng-options="f.name as f.name for f in dashboard.templating.list">
+          <option value=""></option>
       </div>
     </div>
-
   </div>
 </div>
+

+ 2 - 81
public/app/features/dashboard/row/options.ts

@@ -4,99 +4,20 @@ import _ from 'lodash';
 
 import config from 'app/core/config';
 import {coreModule, appEvents} from 'app/core/core';
+// import VirtualScroll from 'virtual-scroll';
+// console.log(VirtualScroll);
 
 export class RowOptionsCtrl {
   row: any;
   dashboard: any;
   rowCtrl: any;
-  subTabIndex: number;
-  allPanels: any;
-  panelHits: any;
-  activeIndex: any;
-  panelSearch: any;
-
   fontSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
 
   /** @ngInject */
   constructor(private $scope, private $timeout, private $rootScope) {
     this.row = this.rowCtrl.row;
     this.dashboard = this.rowCtrl.dashboard;
-    this.subTabIndex = 0;
     this.row.titleSize = this.row.titleSize || 'h6';
-    this.allPanels = _.orderBy(_.map(config.panels, item => item), 'sort');
-    this.panelHits = this.allPanels;
-    this.activeIndex = 0;
-  }
-
-  keyDown(evt) {
-    if (evt.keyCode === 27) {
-      this.rowCtrl.showOptions = false;
-      return;
-    }
-
-    if (evt.keyCode === 40 || evt.keyCode === 39) {
-      this.moveSelection(1);
-    }
-
-    if (evt.keyCode === 38 || evt.keyCode === 37) {
-      this.moveSelection(-1);
-    }
-
-    if (evt.keyCode === 13) {
-      var selectedPanel = this.panelHits[this.activeIndex];
-      if (selectedPanel) {
-        this.addPanel(selectedPanel);
-      }
-    }
-  }
-
-  moveSelection(direction) {
-    var max = this.panelHits.length;
-    var newIndex = this.activeIndex + direction;
-    this.activeIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
-  }
-
-  panelSearchChanged() {
-    var items = this.allPanels.slice();
-    var startsWith = [];
-    var contains = [];
-    var searchLower = this.panelSearch.toLowerCase();
-    var item;
-
-    while (item = items.shift()) {
-      var nameLower = item.name.toLowerCase();
-      if (nameLower.indexOf(searchLower) === 0) {
-        startsWith.push(item);
-      } else if (nameLower.indexOf(searchLower) !== -1) {
-        contains.push(item);
-      }
-    }
-
-    this.panelHits = startsWith.concat(contains);
-    this.activeIndex = 0;
-  }
-
-  addPanel(panelPluginInfo) {
-    var defaultSpan = 12;
-    var _as = 12 - this.dashboard.rowSpan(this.row);
-
-    var panel = {
-      id: null,
-      title: config.new_panel_title,
-      error: false,
-      span: _as < defaultSpan && _as > 0 ? _as : defaultSpan,
-      editable: true,
-      type: panelPluginInfo.id,
-      isNew: true,
-    };
-
-    this.rowCtrl.showOptions = false;
-    this.dashboard.addPanel(panel, this.row);
-    this.$timeout(() => {
-      this.$rootScope.appEvent('panel-change-view', {
-        fullscreen: true, edit: true, panelId: panel.id
-      });
-    });
   }
 
   deleteRow() {

+ 14 - 8
public/app/features/dashboard/row/row.html

@@ -1,21 +1,27 @@
 <div class="dash-row-header">
-  <a class="dash-row-header-title" ng-click="ctrl.showOptions = !ctrl.showOptions">
+  <a class="dash-row-header-title" ng-click="ctrl.toggleCollapse()">
+    <span class="dash-row-collapse-toggle pointer">
+      <i class="fa fa-chevron-down" ng-show="!ctrl.row.collapse"></i>
+      <i class="fa fa-chevron-right" ng-show="ctrl.row.collapse"></i>
+    </span>
     <span ng-class="ctrl.row.titleSize">{{ctrl.row.title}}</span>
-    <i class="fa fa-caret-down"></i>
   </a>
 
   <div class="dash-row-header-spacer">
   </div>
 
-  <div class="dash-row-collapse-toggle" ng-click="ctrl.row.collapse = !ctrl.row.collapse">
-    <a class="pointer">
-      <i class="fa fa-chevron-down" ng-show="!ctrl.row.collapse"></i>
-      <i class="fa fa-chevron-right" ng-show="ctrl.row.collapse"></i>
-    </a>
+  <div class="dash-row-header-actions">
+    <a class="pointer" ng-click="ctrl.showAddPanel()">Add Panel <i class="fa fa-plus"></i></a>
+    <a class="pointer" ng-click="ctrl.showRowOptions()">Row Options <i class="fa fa-cog"></i></a>
   </div>
 </div>
 
-<div ng-if="ctrl.showOptions">
+
+<div ng-if="ctrl.dropView === 1">
+  <dash-row-add-panel row-ctrl="ctrl"></dash-row-add-panel>
+</div>
+
+<div ng-if="ctrl.dropView === 2">
   <dash-row-options row-ctrl="ctrl"></dash-row-options>
 </div>
 

+ 16 - 2
public/app/features/dashboard/row/row.ts

@@ -8,18 +8,19 @@ import config from 'app/core/config';
 import {coreModule} from 'app/core/core';
 
 import './options';
+import './add_panel';
 
 export class DashRowCtrl {
   dashboard: any;
   row: any;
-  showOptions: boolean;
+  dropView: number;
 
   /** @ngInject */
   constructor(private $scope, private $rootScope, private $timeout, private uiSegmentSrv, private $q) {
     this.row.title = this.row.title || 'Row title';
 
     if (this.row.isNew) {
-      this.showOptions = true;
+      this.dropView = 1;
       delete this.row.isNew;
     }
   }
@@ -76,6 +77,19 @@ export class DashRowCtrl {
       _.move(rowsList, rowIndex, newIndex);
     }
   }
+
+  toggleCollapse() {
+    this.dropView = 0;
+    this.row.collapse = !this.row.collapse;
+  }
+
+  showAddPanel() {
+    this.dropView = this.dropView === 1 ? 0 : 1;
+  }
+
+  showRowOptions() {
+    this.dropView = this.dropView === 2 ? 0 : 2;
+  }
 }
 
 export function rowDirective($rootScope) {

+ 5 - 0
public/app/headers/common.d.ts

@@ -52,3 +52,8 @@ declare module 'eventemitter3' {
   var config: any;
   export default config;
 }
+
+declare module 'virtual-scroll' {
+  var config: any;
+  export default config;
+}

+ 5 - 0
public/app/system.conf.js

@@ -2,6 +2,7 @@ System.config({
   defaultJSExtenions: true,
   baseURL: 'public',
   paths: {
+    'virtual-scroll': 'vendor/npm/virtual-scroll/src/index.js',
     'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
     'tether': 'vendor/npm/tether/dist/js/tether.js',
     'eventemitter3': 'vendor/npm/eventemitter3/index.js',
@@ -52,6 +53,10 @@ System.config({
   },
 
   meta: {
+    'vendor/npm/virtual-scroll/src/indx.js': {
+      format: 'cjs',
+      exports: 'VirtualScroll',
+    },
     'vendor/angular/angular.js': {
       format: 'global',
       deps: ['jquery'],

+ 2 - 1
public/sass/components/edit_sidemenu.scss

@@ -6,10 +6,11 @@
 
 .edit-tab-content {
   flex-grow: 1;
+  min-width: 0;
 }
 
 .edit-sidemenu-aside {
-  min-width: 16rem;
+  min-width: 15rem;
 }
 
 .edit-sidemenu {

+ 46 - 24
public/sass/pages/_dashboard.scss

@@ -206,24 +206,54 @@ div.flot-text {
 //
 
 .dash-row {
+  border-left: 1px solid $dark-2;
 }
 
 .dash-row-header {
+  position: relative;
   display: flex;
   flex-direction: row;
   margin-right: $panel-margin;
   margin-left: $gf-form-margin;
-  border-bottom: $panel-border;
+  border-bottom: 1px solid $dark-4;
+
+  &:hover {
+    .dash-row-header-actions {
+      display: block;
+    }
+  }
 }
 
 .dash-row-header-title {
-  padding: 0.7rem;
-  i {
-    font-size: 0.9rem;
+  padding: 0.6rem;
+
+  .dash-row-collapse-toggle {
+    font-size: $font-size-sm;
+    color: $text-muted;
     position: relative;
-    top: 2px;
-    left: 1px;
+    left: -5px;
+  }
+
+  &:hover {
+    .dash-row-collapse-toggle {
+      color: $link-color;
+    }
+  }
+}
+
+.dash-row-header-actions {
+  position: absolute;
+  display: none;
+  color: $text-muted;
+  font-size: $font-size-sm;
+  bottom: 5px;
+  right: 1rem;
+  a {
     color: $text-muted;
+    padding-left: 1rem;
+    &:hover {
+      color: $link-hover-color;
+    }
   }
 }
 
@@ -242,20 +272,6 @@ div.flot-text {
   flex: 50;
 }
 
-.dash-row-collapse-toggle {
-  flex-grow: 30;
-  cursor: pointer;
-  text-align: right;
-  margin-right: 0.6rem;
-  font-size: $font-size-sm;
-  line-height: 2.5rem;
-  a {
-    color: $text-muted;
-  }
-  &:hover a {
-    color: $link-color;
-  }
-}
 
 .dash-edit-mode {
   .dash-row {
@@ -287,17 +303,23 @@ div.flot-text {
   }
 }
 
+.add-panel-panels-scroll {
+  width: 100%;
+  overflow: hidden;
+}
+
 .add-panel-panels {
+  display: flex;
+  flex-direction: row;
 }
 
 .add-panel-item {
   background: $input-label-bg;
   padding: $spacer;
-  min-width: 10rem;
-  max-width: 10rem;
+  min-width: 9rem;
+  max-width: 9rem;
   text-align: center;
   margin: $gf-form-margin;
-  float: left;
   cursor: pointer;
 
   &.active,
@@ -313,5 +335,5 @@ div.flot-text {
 }
 
 .add-panel-item-img {
-  width: 3.5rem;
+  width: 3rem;
 }

+ 2 - 0
tasks/options/copy.js

@@ -31,6 +31,8 @@ module.exports = function(config) {
         'tether-drop/**/*',
         'tether-drop/**/*',
         'remarkable/dist/*',
+        'remarkable/dist/*',
+        'virtual-scroll/**/*',
       ],
       dest: '<%= srcDir %>/vendor/npm'
     }