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

Merge pull request #13578 from grafana/react-panels-step1

WIP: React panels step1
Torkel Ödegaard 7 лет назад
Родитель
Сommit
4f4bba7f8c
94 измененных файлов с 2160 добавлено и 507 удалено
  1. 3 0
      conf/defaults.ini
  2. 1 1
      pkg/api/api.go
  3. 7 3
      pkg/api/frontendsettings.go
  4. 1 1
      pkg/api/index.go
  5. 5 0
      pkg/setting/setting.go
  6. 18 27
      public/app/app.ts
  7. 1 0
      public/app/core/components/scroll/scroll.ts
  8. 2 1
      public/app/core/config.ts
  9. 3 0
      public/app/core/constants.ts
  10. 0 2
      public/app/core/core.ts
  11. 17 1
      public/app/core/core_module.ts
  12. 11 6
      public/app/core/directives/dash_class.ts
  13. 9 2
      public/app/core/reducers/location.ts
  14. 14 20
      public/app/core/services/dynamic_directive_srv.ts
  15. 2 2
      public/app/core/services/keybindingSrv.ts
  16. 0 1
      public/app/features/dashboard/all.ts
  17. 9 12
      public/app/features/dashboard/dashboard_ctrl.ts
  18. 37 0
      public/app/features/dashboard/dashboard_model.ts
  19. 4 7
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  20. 22 24
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  21. 1 3
      public/app/features/dashboard/dashgrid/DashboardGridDirective.ts
  22. 134 27
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  23. 7 17
      public/app/features/dashboard/dashgrid/DashboardRow.tsx
  24. 150 0
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  25. 84 0
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  26. 0 7
      public/app/features/dashboard/dashgrid/PanelContainer.ts
  27. 121 0
      public/app/features/dashboard/dashgrid/PanelEditor.tsx
  28. 83 0
      public/app/features/dashboard/dashgrid/PanelHeader.tsx
  29. 53 0
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  30. 69 0
      public/app/features/dashboard/dashgrid/VizTypePicker.tsx
  31. 2 0
      public/app/features/dashboard/dashnav/dashnav.ts
  32. 45 4
      public/app/features/dashboard/panel_model.ts
  33. 1 1
      public/app/features/dashboard/settings/settings.ts
  34. 1 2
      public/app/features/dashboard/share_snapshot_ctrl.ts
  35. 17 7
      public/app/features/dashboard/specs/AddPanelPanel.test.tsx
  36. 4 9
      public/app/features/dashboard/specs/DashboardRow.test.tsx
  37. 1 1
      public/app/features/dashboard/specs/exporter.test.ts
  38. 7 7
      public/app/features/dashboard/specs/viewstate_srv.test.ts
  39. 2 2
      public/app/features/dashboard/submenu/submenu.ts
  40. 21 12
      public/app/features/dashboard/time_srv.ts
  41. 1 1
      public/app/features/dashboard/timepicker/settings.html
  42. 2 1
      public/app/features/dashboard/timepicker/timepicker.ts
  43. 27 85
      public/app/features/dashboard/view_state_srv.ts
  44. 1 28
      public/app/features/panel/metrics_panel_ctrl.ts
  45. 34 2
      public/app/features/panel/metrics_tab.ts
  46. 17 20
      public/app/features/panel/panel_ctrl.ts
  47. 43 44
      public/app/features/panel/panel_directive.ts
  48. 18 10
      public/app/features/panel/panel_editor_tab.ts
  49. 0 15
      public/app/features/panel/panel_header.ts
  50. 0 4
      public/app/features/panel/partials/metrics_tab.html
  51. 73 0
      public/app/features/panel/viz_tab.ts
  52. 4 0
      public/app/features/plugins/built_in_plugins.ts
  53. 17 1
      public/app/features/plugins/datasource_srv.ts
  54. 8 6
      public/app/features/plugins/plugin_component.ts
  55. 3 1
      public/app/features/plugins/plugin_loader.ts
  56. 7 4
      public/app/features/templating/specs/variable_srv.test.ts
  57. 3 2
      public/app/features/templating/specs/variable_srv_init.test.ts
  58. 9 9
      public/app/features/templating/variable_srv.ts
  59. 2 3
      public/app/partials/dashboard.html
  60. 3 2
      public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
  61. 3 4
      public/app/plugins/datasource/elasticsearch/bucket_agg.ts
  62. 3 4
      public/app/plugins/datasource/elasticsearch/metric_agg.ts
  63. 2 2
      public/app/plugins/datasource/graphite/add_graphite_func.ts
  64. 2 2
      public/app/plugins/datasource/graphite/func_editor.ts
  65. 3 3
      public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
  66. 3 3
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  67. 0 1
      public/app/plugins/datasource/testdata/datasource.ts
  68. 2 4
      public/app/plugins/panel/graph/legend.ts
  69. 1 1
      public/app/plugins/panel/graph/module.ts
  70. 2 2
      public/app/plugins/panel/graph/series_overrides_ctrl.ts
  71. 5 0
      public/app/plugins/panel/graph2/README.md
  72. 26 0
      public/app/plugins/panel/graph2/img/icn-text-panel.svg
  73. 43 0
      public/app/plugins/panel/graph2/module.tsx
  74. 17 0
      public/app/plugins/panel/graph2/plugin.json
  75. 3 5
      public/app/plugins/panel/heatmap/color_legend.ts
  76. 5 0
      public/app/plugins/panel/text2/README.md
  77. 186 0
      public/app/plugins/panel/text2/img/icn-graph-panel.svg
  78. 14 0
      public/app/plugins/panel/text2/module.tsx
  79. 19 0
      public/app/plugins/panel/text2/plugin.json
  80. 7 3
      public/app/routes/GrafanaCtrl.ts
  81. 24 0
      public/app/types/index.ts
  82. 1 0
      public/app/types/location.ts
  83. 7 0
      public/app/types/panel.ts
  84. 22 0
      public/app/types/plugins.ts
  85. 91 0
      public/app/types/series.ts
  86. 124 0
      public/app/viz/Graph.tsx
  87. 168 0
      public/app/viz/state/timeSeries.ts
  88. 1 0
      public/sass/_grafana.scss
  89. 5 1
      public/sass/components/_dashboard_grid.scss
  90. 0 4
      public/sass/components/_panel_add_panel.scss
  91. 5 0
      public/sass/components/_scrollbar.scss
  92. 10 11
      public/sass/components/_tabbed_view.scss
  93. 81 0
      public/sass/components/_viz_editor.scss
  94. 34 10
      public/sass/pages/_dashboard.scss

+ 3 - 0
conf/defaults.ini

@@ -554,3 +554,6 @@ container_name =
 # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
 server_url =
 callback_url =
+
+[panels]
+enable_alpha = false

+ 1 - 1
pkg/api/api.go

@@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
 			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 
-		apiRoute.Get("/frontend/settings/", GetFrontendSettings)
+		apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
 		apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
 		apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
 

+ 7 - 3
pkg/api/frontendsettings.go

@@ -11,7 +11,7 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 )
 
-func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
+func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 	orgDataSources := make([]*m.DataSource, 0)
 
 	if c.OrgId != 0 {
@@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 
 	panels := map[string]interface{}{}
 	for _, panel := range enabledPlugins.Panels {
+		if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
+			continue
+		}
+
 		panels[panel.Id] = map[string]interface{}{
 			"module":       panel.Module,
 			"baseUrl":      panel.BaseUrl,
@@ -196,8 +200,8 @@ func getPanelSort(id string) int {
 	return sort
 }
 
-func GetFrontendSettings(c *m.ReqContext) {
-	settings, err := getFrontendSettingsMap(c)
+func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 		c.JsonApiErr(400, "Failed to get frontend settings", err)
 		return

+ 1 - 1
pkg/api/index.go

@@ -18,7 +18,7 @@ const (
 )
 
 func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
-	settings, err := getFrontendSettingsMap(c)
+	settings, err := hs.getFrontendSettingsMap(c)
 	if err != nil {
 		return nil, err
 	}

+ 5 - 0
pkg/setting/setting.go

@@ -213,6 +213,8 @@ type Cfg struct {
 	TempDataLifetime time.Duration
 
 	MetricsEndpointEnabled bool
+
+	EnableAlphaPanels bool
 }
 
 type CommandLineArgs struct {
@@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	explore := iniFile.Section("explore")
 	ExploreEnabled = explore.Key("enabled").MustBool(false)
 
+	panels := iniFile.Section("panels")
+	cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
+
 	cfg.readSessionConfig()
 	cfg.readSmtpSettings()
 	cfg.readQuotaSettings()

+ 18 - 27
public/app/app.ts

@@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => {
   return array;
 };
 
-import { coreModule, registerAngularDirectives } from './core/core';
-import { setupAngularRoutes } from './routes/routes';
+import { coreModule, angularModules } from 'app/core/core_module';
+import { registerAngularDirectives } from 'app/core/core';
+import { setupAngularRoutes } from 'app/routes/routes';
+
+import 'app/routes/GrafanaCtrl';
+import 'app/features/all';
 
 // import symlinked extensions
 const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
@@ -109,39 +113,26 @@ export class GrafanaApp {
       'react',
     ];
 
-    const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
-
-    _.each(moduleTypes, type => {
-      const moduleName = 'grafana.' + type;
-      this.useModule(angular.module(moduleName, []));
-    });
-
     // makes it possible to add dynamic stuff
-    this.useModule(coreModule);
+    _.each(angularModules, m => {
+      this.useModule(m);
+    });
 
     // register react angular wrappers
     coreModule.config(setupAngularRoutes);
     registerAngularDirectives();
 
-    const preBootRequires = [import('app/features/all')];
+    // disable tool tip animation
+    $.fn.tooltip.defaults.animation = false;
 
-    Promise.all(preBootRequires)
-      .then(() => {
-        // disable tool tip animation
-        $.fn.tooltip.defaults.animation = false;
-
-        // bootstrap the app
-        angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
-          _.each(this.preBootModules, module => {
-            _.extend(module, this.registerFunctions);
-          });
-
-          this.preBootModules = null;
-        });
-      })
-      .catch(err => {
-        console.log('Application boot failed:', err);
+    // bootstrap the app
+    angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
+      _.each(this.preBootModules, module => {
+        _.extend(module, this.registerFunctions);
       });
+
+      this.preBootModules = null;
+    });
   }
 }
 

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

@@ -18,6 +18,7 @@ export function geminiScrollbar() {
       let scrollRoot = elem.parent();
       const scroller = elem;
 
+      console.log('scroll');
       if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
         scrollRoot = scroller;
       }

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

@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { PanelPlugin } from 'app/types/plugins';
 
 export interface BuildInfo {
   version: string;
@@ -9,7 +10,7 @@ export interface BuildInfo {
 
 export class Settings {
   datasources: any;
-  panels: any;
+  panels: PanelPlugin[];
   appSubUrl: string;
   windowTitlePrefix: string;
   buildInfo: BuildInfo;

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

@@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250;
 export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
 
 export const LS_PANEL_COPY_KEY = 'panel-copy';
+
+export const DASHBOARD_TOOLBAR_HEIGHT = 55;
+export const DASHBOARD_TOP_PADDING = 20;

+ 0 - 2
public/app/core/core.ts

@@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker';
 import './services/search_srv';
 import './services/ng_react';
 
-import { grafanaAppDirective } from './components/grafana_app';
 import { searchDirective } from './components/search/search';
 import { infoPopover } from './components/info_popover';
 import { navbarDirective } from './components/navbar/navbar';
@@ -60,7 +59,6 @@ export {
   registerAngularDirectives,
   arrayJoin,
   coreModule,
-  grafanaAppDirective,
   navbarDirective,
   searchDirective,
   liveSrv,

+ 17 - 1
public/app/core/core_module.ts

@@ -1,2 +1,18 @@
 import angular from 'angular';
-export default angular.module('grafana.core', ['ngRoute']);
+
+const coreModule = angular.module('grafana.core', ['ngRoute']);
+
+// legacy modules
+const angularModules = [
+  coreModule,
+  angular.module('grafana.controllers', []),
+  angular.module('grafana.directives', []),
+  angular.module('grafana.factories', []),
+  angular.module('grafana.services', []),
+  angular.module('grafana.filters', []),
+  angular.module('grafana.routes', []),
+];
+
+export { angularModules, coreModule };
+
+export default coreModule;

+ 11 - 6
public/app/core/directives/dash_class.ts

@@ -2,16 +2,21 @@ import _ from 'lodash';
 import coreModule from '../core_module';
 
 /** @ngInject */
-export function dashClass() {
+function dashClass($timeout) {
   return {
     link: ($scope, elem) => {
-      $scope.onAppEvent('panel-fullscreen-enter', () => {
-        elem.toggleClass('panel-in-fullscreen', true);
+      $scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
+        console.log('view-mode-changed', panel.fullscreen);
+        if (panel.fullscreen) {
+          elem.addClass('panel-in-fullscreen');
+        } else {
+          $timeout(() => {
+            elem.removeClass('panel-in-fullscreen');
+          });
+        }
       });
 
-      $scope.onAppEvent('panel-fullscreen-exit', () => {
-        elem.toggleClass('panel-in-fullscreen', false);
-      });
+      elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
 
       $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
         if (newValue) {

+ 9 - 2
public/app/core/reducers/location.ts

@@ -1,6 +1,7 @@
 import { Action } from 'app/core/actions/location';
 import { LocationState } from 'app/types';
 import { renderUrl } from 'app/core/utils/url';
+import _ from 'lodash';
 
 export const initialState: LocationState = {
   url: '',
@@ -12,11 +13,17 @@ export const initialState: LocationState = {
 export const locationReducer = (state = initialState, action: Action): LocationState => {
   switch (action.type) {
     case 'UPDATE_LOCATION': {
-      const { path, query, routeParams } = action.payload;
+      const { path, routeParams } = action.payload;
+      let query = action.payload.query || state.query;
+
+      if (action.payload.partial) {
+        query = _.defaults(query, state.query);
+      }
+
       return {
         url: renderUrl(path || state.path, query),
         path: path || state.path,
-        query: query || state.query,
+        query: query,
         routeParams: routeParams || state.routeParams,
       };
     }

+ 14 - 20
public/app/core/services/dynamic_directive_srv.ts

@@ -3,7 +3,7 @@ import coreModule from '../core_module';
 
 class DynamicDirectiveSrv {
   /** @ngInject */
-  constructor(private $compile, private $rootScope) {}
+  constructor(private $compile) {}
 
   addDirective(element, name, scope) {
     const child = angular.element(document.createElement(name));
@@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
   }
 
   link(scope, elem, attrs, options) {
-    options
-      .directive(scope)
-      .then(directiveInfo => {
-        if (!directiveInfo || !directiveInfo.fn) {
-          elem.empty();
-          return;
-        }
-
-        if (!directiveInfo.fn.registered) {
-          coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
-          directiveInfo.fn.registered = true;
-        }
-
-        this.addDirective(elem, directiveInfo.name, scope);
-      })
-      .catch(err => {
-        console.log('Plugin load:', err);
-        this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
-      });
+    const directiveInfo = options.directive(scope);
+    if (!directiveInfo || !directiveInfo.fn) {
+      elem.empty();
+      return;
+    }
+
+    if (!directiveInfo.fn.registered) {
+      console.log('register panel tab');
+      coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
+      directiveInfo.fn.registered = true;
+    }
+
+    this.addDirective(elem, directiveInfo.name, scope);
   }
 
   create(options) {

+ 2 - 2
public/app/core/services/keybindingSrv.ts

@@ -148,7 +148,7 @@ export class KeybindingSrv {
     this.bind('mod+o', () => {
       dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
       appEvents.emit('graph-hover-clear');
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
 
     this.bind('mod+s', e => {
@@ -257,7 +257,7 @@ export class KeybindingSrv {
     });
 
     this.bind('d r', () => {
-      this.$rootScope.$broadcast('refresh');
+      dashboard.startRefresh();
     });
 
     this.bind('d s', () => {

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

@@ -22,7 +22,6 @@ import './export_data/export_data_modal';
 import './ad_hoc_filters';
 import './repeat_option/repeat_option';
 import './dashgrid/DashboardGridDirective';
-import './dashgrid/PanelLoader';
 import './dashgrid/RowOptions';
 import './folder_picker/folder_picker';
 import './move_to_folder_modal/move_to_folder';

+ 9 - 12
public/app/features/dashboard/dashboard_ctrl.ts

@@ -1,11 +1,10 @@
 import config from 'app/core/config';
 
 import coreModule from 'app/core/core_module';
-import { PanelContainer } from './dashgrid/PanelContainer';
 import { DashboardModel } from './dashboard_model';
 import { PanelModel } from './panel_model';
 
-export class DashboardCtrl implements PanelContainer {
+export class DashboardCtrl {
   dashboard: DashboardModel;
   dashboardViewState: any;
   loadedFallbackDashboard: boolean;
@@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer {
     private dashboardSrv,
     private unsavedChangesSrv,
     private dashboardViewStateSrv,
-    public playlistSrv,
-    private panelLoader
+    public playlistSrv
   ) {
     // temp hack due to way dashboards are loaded
     // can't use controllerAs on route yet
@@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
     return this.dashboard;
   }
 
-  getPanelLoader() {
-    return this.panelLoader;
-  }
-
-  timezoneChanged() {
-    this.$rootScope.$broadcast('refresh');
-  }
-
   getPanelContainer() {
     return this;
   }
@@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer {
     this.dashboard.removePanel(panel);
   }
 
+  onDestroy() {
+    if (this.dashboard) {
+      this.dashboard.destroy();
+    }
+  }
+
   init(dashboard) {
     this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
     this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
     this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
+    this.$scope.$on('$destroy', this.onDestroy.bind(this));
     this.setupDashboard(dashboard);
   }
 }

+ 37 - 0
public/app/features/dashboard/dashboard_model.ts

@@ -200,6 +200,43 @@ export class DashboardModel {
     this.events.emit('view-mode-changed', panel);
   }
 
+  timeRangeUpdated() {
+    this.events.emit('time-range-updated');
+  }
+
+  startRefresh() {
+    this.events.emit('refresh');
+
+    for (const panel of this.panels) {
+      if (!this.otherPanelInFullscreen(panel)) {
+        panel.refresh();
+      }
+    }
+  }
+
+  render() {
+    this.events.emit('render');
+
+    for (const panel of this.panels) {
+      panel.render();
+    }
+  }
+
+  panelInitialized(panel: PanelModel) {
+    if (!this.otherPanelInFullscreen(panel)) {
+      panel.refresh();
+    }
+  }
+
+  otherPanelInFullscreen(panel: PanelModel) {
+    return this.meta.fullscreen && !panel.fullscreen;
+  }
+
+  changePanelType(panel: PanelModel, pluginId: string) {
+    panel.changeType(pluginId);
+    this.events.emit('panel-type-changed', panel);
+  }
+
   private ensureListExist(data) {
     if (!data) {
       data = {};

+ 4 - 7
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -3,7 +3,7 @@ import _ from 'lodash';
 import classNames from 'classnames';
 import config from 'app/core/config';
 import { PanelModel } from '../panel_model';
-import { PanelContainer } from './PanelContainer';
+import { DashboardModel } from '../dashboard_model';
 import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
 import store from 'app/core/store';
 import { LS_PANEL_COPY_KEY } from 'app/core/constants';
@@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words';
 
 export interface AddPanelPanelProps {
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
 export interface AddPanelPanelState {
@@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
   }
 
   onAddPanel = panelPluginInfo => {
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
+    const dashboard = this.props.dashboard;
     const { gridPos } = this.props.panel;
 
     const newPanel: any = {
@@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
 
   handleCloseAddPanel(evt) {
     evt.preventDefault();
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
-    dashboard.removePanel(dashboard.panels[0]);
+    this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
   }
 
   renderText(text: string) {

+ 22 - 24
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -3,7 +3,6 @@ import ReactGridLayout from 'react-grid-layout';
 import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
 import { DashboardPanel } from './DashboardPanel';
 import { DashboardModel } from '../dashboard_model';
-import { PanelContainer } from './PanelContainer';
 import { PanelModel } from '../panel_model';
 import classNames from 'classnames';
 import sizeMe from 'react-sizeme';
@@ -60,18 +59,15 @@ function GridWrapper({
 const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
 
 export interface DashboardGridProps {
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
 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);
     this.onResizeStop = this.onResizeStop.bind(this);
@@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
     this.state = { animated: false };
 
     // subscribe to dashboard events
-    this.dashboard = this.panelContainer.getDashboard();
-    this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
-    this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
+    const dashboard = this.props.dashboard;
+    dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
+    dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
+    dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
+    dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
+    dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
+    dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
+    dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
   }
 
   buildLayout() {
     const layout = [];
     this.panelMap = {};
 
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       const stringId = panel.id.toString();
       this.panelMap[stringId] = panel;
 
@@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       this.panelMap[newPos.i].updateGridPos(newPos);
     }
 
-    this.dashboard.sortPanelsByGridPos();
+    this.props.dashboard.sortPanelsByGridPos();
   }
 
   triggerForceUpdate() {
@@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   }
 
   onWidthChange() {
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       panel.resizeDone();
     }
   }
 
+  onViewModeChanged(payload) {
+    this.setState({ animated: !payload.fullscreen });
+  }
+
   updateGridPos(item, layout) {
     this.panelMap[item.i].updateGridPos(item);
 
@@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
 
   componentDidMount() {
     setTimeout(() => {
-      this.setState(() => {
-        return { animated: true };
-      });
+      this.setState({ animated: true });
     });
   }
 
   renderPanels() {
     const panelElements = [];
 
-    for (const panel of this.dashboard.panels) {
+    for (const panel of this.props.dashboard.panels) {
       const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
       panelElements.push(
-        /** panel-id is set for html bookmarks */
-        <div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id.toString()}`}>
-          <DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
+        <div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
+          <DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
         </div>
       );
     }
@@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
       <SizedReactLayoutGrid
         className={classNames({ layout: true, animated: this.state.animated })}
         layout={this.buildLayout()}
-        isResizable={this.dashboard.meta.canEdit}
-        isDraggable={this.dashboard.meta.canEdit}
+        isResizable={this.props.dashboard.meta.canEdit}
+        isDraggable={this.props.dashboard.meta.canEdit}
         onLayoutChange={this.onLayoutChange}
         onWidthChange={this.onWidthChange}
         onDragStop={this.onDragStop}

+ 1 - 3
public/app/features/dashboard/dashgrid/DashboardGridDirective.ts

@@ -1,6 +1,4 @@
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 import { DashboardGrid } from './DashboardGrid';
 
-react2AngularDirective('dashboardGrid', DashboardGrid, [
-  ['getPanelContainer', { watchDepth: 'reference', wrapApply: false }],
-]);
+react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);

+ 134 - 27
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,54 +1,161 @@
 import React from 'react';
-import {PanelModel} from '../panel_model';
-import {PanelContainer} from './PanelContainer';
-import {AttachedPanel} from './PanelLoader';
-import {DashboardRow} from './DashboardRow';
-import {AddPanelPanel} from './AddPanelPanel';
+import config from 'app/core/config';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { DashboardRow } from './DashboardRow';
+import { AddPanelPanel } from './AddPanelPanel';
+import { importPluginModule } from 'app/features/plugins/plugin_loader';
+import { PluginExports, PanelPlugin } from 'app/types/plugins';
+import { PanelChrome } from './PanelChrome';
+import { PanelEditor } from './PanelEditor';
 
-export interface DashboardPanelProps {
+export interface Props {
+  panelType: string;
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
-export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
+export interface State {
+  pluginExports: PluginExports;
+}
+
+export class DashboardPanel extends React.Component<Props, State> {
   element: any;
-  attachedPanel: AttachedPanel;
+  angularPanel: AngularComponent;
+  pluginInfo: any;
+  specialPanels = {};
 
   constructor(props) {
     super(props);
-    this.state = {};
+
+    this.state = {
+      pluginExports: null,
+    };
+
+    this.specialPanels['row'] = this.renderRow.bind(this);
+    this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
+  }
+
+  isSpecial() {
+    return this.specialPanels[this.props.panel.type];
+  }
+
+  renderRow() {
+    return <DashboardRow panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  renderAddPanel() {
+    return <AddPanelPanel panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  onPluginTypeChanged = (plugin: PanelPlugin) => {
+    this.props.panel.changeType(plugin.id);
+    this.loadPlugin();
+  };
+
+  onAngularPluginTypeChanged = () => {
+    this.loadPlugin();
+  };
+
+  loadPlugin() {
+    if (this.isSpecial()) {
+      return;
+    }
+
+    // handle plugin loading & changing of plugin type
+    if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
+      this.pluginInfo = config.panels[this.props.panel.type];
+
+      if (this.pluginInfo.exports) {
+        this.cleanUpAngularPanel();
+        this.setState({ pluginExports: this.pluginInfo.exports });
+      } else {
+        importPluginModule(this.pluginInfo.module).then(pluginExports => {
+          this.cleanUpAngularPanel();
+          // cache plugin exports (saves a promise async cycle next time)
+          this.pluginInfo.exports = pluginExports;
+          // update panel state
+          this.setState({ pluginExports: pluginExports });
+        });
+      }
+    }
   }
 
   componentDidMount() {
-    if (!this.element) {
+    this.loadPlugin();
+  }
+
+  componentDidUpdate() {
+    this.loadPlugin();
+
+    // handle angular plugin loading
+    if (!this.element || this.angularPanel) {
       return;
     }
 
-    const panelContainer = this.props.getPanelContainer();
-    const dashboard = panelContainer.getDashboard();
-    const loader = panelContainer.getPanelLoader();
-    this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
+    const loader = getAngularLoader();
+    const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
+    const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard };
+    this.angularPanel = loader.load(this.element, scopeProps, template);
   }
 
-  componentWillUnmount() {
-    if (this.attachedPanel) {
-      this.attachedPanel.destroy();
+  cleanUpAngularPanel() {
+    if (this.angularPanel) {
+      this.angularPanel.destroy();
+      this.angularPanel = null;
     }
   }
 
+  componentWillUnmount() {
+    this.cleanUpAngularPanel();
+  }
+
+  renderReactPanel() {
+    const { pluginExports } = this.state;
+    const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper';
+    const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
+
+    // this might look strange with these classes that change when edit, but
+    // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel
+    return (
+      <div className={containerClass}>
+        <div className={panelWrapperClass}>
+          <PanelChrome
+            component={pluginExports.PanelComponent}
+            panel={this.props.panel}
+            dashboard={this.props.dashboard}
+          />
+        </div>
+        {this.props.panel.isEditing && (
+          <div className="panel-editor-container__editor">
+            <PanelEditor
+              panel={this.props.panel}
+              panelType={this.props.panel.type}
+              dashboard={this.props.dashboard}
+              onTypeChanged={this.onPluginTypeChanged}
+              pluginExports={pluginExports}
+            />
+          </div>
+        )}
+      </div>
+    );
+  }
+
   render() {
-    // special handling for rows
-    if (this.props.panel.type === 'row') {
-      return <DashboardRow panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
+    if (this.isSpecial()) {
+      return this.specialPanels[this.props.panel.type]();
     }
 
-    if (this.props.panel.type === 'add-panel') {
-      return <AddPanelPanel panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
+    if (!this.state.pluginExports) {
+      return null;
     }
 
-    return (
-      <div ref={element => this.element = element} className="panel-height-helper" />
-    );
+    if (this.state.pluginExports.PanelComponent) {
+      return this.renderReactPanel();
+    }
+
+    // legacy angular rendering
+    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
   }
 }
-

+ 7 - 17
public/app/features/dashboard/dashgrid/DashboardRow.tsx

@@ -1,19 +1,16 @@
 import React from 'react';
 import classNames from 'classnames';
 import { PanelModel } from '../panel_model';
-import { PanelContainer } from './PanelContainer';
+import { DashboardModel } from '../dashboard_model';
 import templateSrv from 'app/features/templating/template_srv';
 import appEvents from 'app/core/app_events';
 
 export interface DashboardRowProps {
   panel: PanelModel;
-  getPanelContainer: () => PanelContainer;
+  dashboard: DashboardModel;
 }
 
 export class DashboardRow extends React.Component<DashboardRowProps, any> {
-  dashboard: any;
-  panelContainer: any;
-
   constructor(props) {
     super(props);
 
@@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       collapsed: this.props.panel.collapsed,
     };
 
-    this.panelContainer = this.props.getPanelContainer();
-    this.dashboard = this.panelContainer.getDashboard();
-
     this.toggle = this.toggle.bind(this);
     this.openSettings = this.openSettings.bind(this);
     this.delete = this.delete.bind(this);
@@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
 
   toggle() {
-    this.dashboard.toggleRow(this.props.panel);
+    this.props.dashboard.toggleRow(this.props.panel);
 
     this.setState(prevState => {
       return { collapsed: !prevState.collapsed };
@@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
   }
 
   update() {
-    this.dashboard.processRepeats();
+    this.props.dashboard.processRepeats();
     this.forceUpdate();
   }
 
@@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       altActionText: 'Delete row only',
       icon: 'fa-trash',
       onConfirm: () => {
-        const panelContainer = this.props.getPanelContainer();
-        const dashboard = panelContainer.getDashboard();
-        dashboard.removeRow(this.props.panel, true);
+        this.props.dashboard.removeRow(this.props.panel, true);
       },
       onAltAction: () => {
-        const panelContainer = this.props.getPanelContainer();
-        const dashboard = panelContainer.getDashboard();
-        dashboard.removeRow(this.props.panel, false);
+        this.props.dashboard.removeRow(this.props.panel, false);
       },
     });
   }
@@ -87,7 +77,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
     const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
     const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
     const panels = count === 1 ? 'panel' : 'panels';
-    const canEdit = this.dashboard.meta.canEdit === true;
+    const canEdit = this.props.dashboard.meta.canEdit === true;
 
     return (
       <div className={classes}>

+ 150 - 0
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -0,0 +1,150 @@
+// Library
+import React, { Component } from 'react';
+
+// Services
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+
+// Types
+import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
+
+interface RenderProps {
+  loading: LoadingState;
+  timeSeries: TimeSeries[];
+}
+
+export interface Props {
+  datasource: string | null;
+  queries: any[];
+  panelId?: number;
+  dashboardId?: number;
+  isVisible?: boolean;
+  timeRange?: TimeRange;
+  refreshCounter: number;
+  children: (r: RenderProps) => JSX.Element;
+}
+
+export interface State {
+  isFirstLoad: boolean;
+  loading: LoadingState;
+  response: DataQueryResponse;
+}
+
+export class DataPanel extends Component<Props, State> {
+  static defaultProps = {
+    isVisible: true,
+    panelId: 1,
+    dashboardId: 1,
+  };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      loading: LoadingState.NotStarted,
+      response: {
+        data: [],
+      },
+      isFirstLoad: true,
+    };
+  }
+
+  componentDidMount() {
+    console.log('DataPanel mount');
+  }
+
+  async componentDidUpdate(prevProps: Props) {
+    if (!this.hasPropsChanged(prevProps)) {
+      return;
+    }
+
+    this.issueQueries();
+  }
+
+  hasPropsChanged(prevProps: Props) {
+    return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible;
+  }
+
+  issueQueries = async () => {
+    const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props;
+
+    if (!isVisible) {
+      return;
+    }
+
+    if (!queries.length) {
+      this.setState({ loading: LoadingState.Done });
+      return;
+    }
+
+    this.setState({ loading: LoadingState.Loading });
+
+    try {
+      const dataSourceSrv = getDatasourceSrv();
+      const ds = await dataSourceSrv.get(datasource);
+
+      const queryOptions: DataQueryOptions = {
+        timezone: 'browser',
+        panelId: panelId,
+        dashboardId: dashboardId,
+        range: timeRange,
+        rangeRaw: timeRange.raw,
+        interval: '1s',
+        intervalMs: 60000,
+        targets: queries,
+        maxDataPoints: 500,
+        scopedVars: {},
+        cacheTimeout: null,
+      };
+
+      console.log('Issuing DataPanel query', queryOptions);
+      const resp = await ds.query(queryOptions);
+      console.log('Issuing DataPanel query Resp', resp);
+
+      this.setState({
+        loading: LoadingState.Done,
+        response: resp,
+      });
+    } catch (err) {
+      console.log('Loading error', err);
+      this.setState({ loading: LoadingState.Error });
+    }
+  };
+
+  render() {
+    const { response, loading, isFirstLoad } = this.state;
+    console.log('data panel render');
+    const timeSeries = response.data;
+
+    if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
+      return (
+        <div className="loading">
+          <p>Loading</p>
+        </div>
+      );
+    }
+
+    return (
+      <>
+        {this.loadingSpinner}
+        {this.props.children({
+          timeSeries,
+          loading,
+        })}
+      </>
+    );
+  }
+
+  private get loadingSpinner(): JSX.Element {
+    const { loading } = this.state;
+
+    if (loading === LoadingState.Loading) {
+      return (
+        <div className="panel__loading">
+          <i className="fa fa-spinner fa-spin" />
+        </div>
+      );
+    }
+
+    return null;
+  }
+}

+ 84 - 0
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -0,0 +1,84 @@
+// Libraries
+import React, { ComponentClass, PureComponent } from 'react';
+
+// Services
+import { getTimeSrv } from '../time_srv';
+
+// Components
+import { PanelHeader } from './PanelHeader';
+import { DataPanel } from './DataPanel';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { TimeRange, PanelProps } from 'app/types';
+
+export interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+  component: ComponentClass<PanelProps>;
+}
+
+export interface State {
+  refreshCounter: number;
+  timeRange?: TimeRange;
+}
+
+export class PanelChrome extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      refreshCounter: 0,
+    };
+  }
+
+  componentDidMount() {
+    this.props.panel.events.on('refresh', this.onRefresh);
+    this.props.dashboard.panelInitialized(this.props.panel);
+  }
+
+  componentWillUnmount() {
+    this.props.panel.events.off('refresh', this.onRefresh);
+  }
+
+  onRefresh = () => {
+    const timeSrv = getTimeSrv();
+    const timeRange = timeSrv.timeRange();
+
+    this.setState({
+      refreshCounter: this.state.refreshCounter + 1,
+      timeRange: timeRange,
+    });
+  };
+
+  get isVisible() {
+    return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
+  }
+
+  render() {
+    const { panel, dashboard } = this.props;
+    const { datasource, targets } = panel;
+    const { refreshCounter, timeRange } = this.state;
+    const PanelComponent = this.props.component;
+
+    return (
+      <div className="panel-container">
+        <PanelHeader panel={panel} dashboard={dashboard} />
+        <div className="panel-content">
+          <DataPanel
+            datasource={datasource}
+            queries={targets}
+            timeRange={timeRange}
+            isVisible={this.isVisible}
+            refreshCounter={refreshCounter}
+          >
+            {({ loading, timeSeries }) => {
+              return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />;
+            }}
+          </DataPanel>
+        </div>
+      </div>
+    );
+  }
+}

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

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

+ 121 - 0
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -0,0 +1,121 @@
+import React from 'react';
+import classNames from 'classnames';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { store } from 'app/store/configureStore';
+import { QueriesTab } from './QueriesTab';
+import { PanelPlugin, PluginExports } from 'app/types/plugins';
+import { VizTypePicker } from './VizTypePicker';
+import { updateLocation } from 'app/core/actions';
+
+interface PanelEditorProps {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+  panelType: string;
+  pluginExports: PluginExports;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+interface PanelEditorTab {
+  id: string;
+  text: string;
+  icon: string;
+}
+
+export class PanelEditor extends React.Component<PanelEditorProps, any> {
+  tabs: PanelEditorTab[];
+
+  constructor(props) {
+    super(props);
+
+    this.tabs = [
+      { id: 'queries', text: 'Queries', icon: 'fa fa-database' },
+      { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
+    ];
+  }
+
+  renderQueriesTab() {
+    return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
+  }
+
+  renderPanelOptions() {
+    const { pluginExports } = this.props;
+
+    if (pluginExports.PanelOptions) {
+      const PanelOptions = pluginExports.PanelOptions;
+      return <PanelOptions />;
+    } else {
+      return <p>Visualization has no options</p>;
+    }
+  }
+
+  renderVizTab() {
+    return (
+      <div className="viz-editor">
+        <div className="viz-editor-col1">
+          <VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.props.onTypeChanged} />
+        </div>
+        <div className="viz-editor-col2">
+          <h5 className="page-heading">Options</h5>
+          {this.renderPanelOptions()}
+        </div>
+      </div>
+    );
+  }
+
+  onChangeTab = (tab: PanelEditorTab) => {
+    store.dispatch(
+      updateLocation({
+        query: { tab: tab.id },
+        partial: true,
+      })
+    );
+  };
+
+  render() {
+    const { location } = store.getState();
+    const activeTab = location.query.tab || 'queries';
+
+    return (
+      <div className="tabbed-view tabbed-view--new">
+        <div className="tabbed-view-header">
+          <ul className="gf-tabs">
+            {this.tabs.map(tab => {
+              return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
+            })}
+          </ul>
+
+          <button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
+            <i className="fa fa-remove" />
+          </button>
+        </div>
+
+        <div className="tabbed-view-body">
+          {activeTab === 'queries' && this.renderQueriesTab()}
+          {activeTab === 'visualization' && this.renderVizTab()}
+        </div>
+      </div>
+    );
+  }
+}
+
+interface TabItemParams {
+  tab: PanelEditorTab;
+  activeTab: string;
+  onClick: (tab: PanelEditorTab) => void;
+}
+
+function TabItem({ tab, activeTab, onClick }: TabItemParams) {
+  const tabClasses = classNames({
+    'gf-tabs-link': true,
+    active: activeTab === tab.id,
+  });
+
+  return (
+    <li className="gf-tabs-item" key={tab.id}>
+      <a className={tabClasses} onClick={() => onClick(tab)}>
+        <i className={tab.icon} /> {tab.text}
+      </a>
+    </li>
+  );
+}

+ 83 - 0
public/app/features/dashboard/dashgrid/PanelHeader.tsx

@@ -0,0 +1,83 @@
+import React from 'react';
+import classNames from 'classnames';
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { store } from 'app/store/configureStore';
+import { updateLocation } from 'app/core/actions';
+
+interface PanelHeaderProps {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+}
+
+export class PanelHeader extends React.Component<PanelHeaderProps, any> {
+  onEditPanel = () => {
+    store.dispatch(
+      updateLocation({
+        query: {
+          panelId: this.props.panel.id,
+          edit: true,
+          fullscreen: true,
+        },
+      })
+    );
+  };
+
+  onViewPanel = () => {
+    store.dispatch(
+      updateLocation({
+        query: {
+          panelId: this.props.panel.id,
+          edit: false,
+          fullscreen: true,
+        },
+      })
+    );
+  };
+
+  render() {
+    const isFullscreen = false;
+    const isLoading = false;
+    const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
+
+    return (
+      <div className={panelHeaderClass}>
+        <span className="panel-info-corner">
+          <i className="fa" />
+          <span className="panel-info-corner-inner" />
+        </span>
+
+        {isLoading && (
+          <span className="panel-loading">
+            <i className="fa fa-spinner fa-spin" />
+          </span>
+        )}
+
+        <div className="panel-title-container">
+          <span className="panel-title">
+            <span className="icon-gf panel-alert-icon" />
+            <span className="panel-title-text">{this.props.panel.title}</span>
+            <span className="panel-menu-container dropdown">
+              <span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
+              <ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
+                <li>
+                  <a onClick={this.onEditPanel}>
+                    <i className="fa fa-fw fa-edit" /> Edit
+                  </a>
+                </li>
+                <li>
+                  <a onClick={this.onViewPanel}>
+                    <i className="fa fa-fw fa-eye" /> View
+                  </a>
+                </li>
+              </ul>
+            </span>
+            <span className="panel-time-info">
+              <i className="fa fa-clock-o" /> 4m
+            </span>
+          </span>
+        </div>
+      </div>
+    );
+  }
+}

+ 53 - 0
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -0,0 +1,53 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Services & utils
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+
+// Types
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+
+interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+}
+
+export class QueriesTab extends PureComponent<Props> {
+  element: any;
+  component: AngularComponent;
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentDidMount() {
+    if (!this.element) {
+      return;
+    }
+
+    const { panel, dashboard } = this.props;
+
+    const loader = getAngularLoader();
+    const template = '<metrics-tab />';
+    const scopeProps = {
+      ctrl: {
+        panel: panel,
+        dashboard: dashboard,
+        refresh: () => panel.refresh(),
+      },
+    };
+
+    this.component = loader.load(this.element, scopeProps, template);
+  }
+
+  componentWillUnmount() {
+    if (this.component) {
+      this.component.destroy();
+    }
+  }
+
+  render() {
+    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
+  }
+}

+ 69 - 0
public/app/features/dashboard/dashgrid/VizTypePicker.tsx

@@ -0,0 +1,69 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import config from 'app/core/config';
+import { PanelPlugin } from 'app/types/plugins';
+import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
+import _ from 'lodash';
+
+interface Props {
+  currentType: string;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+interface State {
+  pluginList: PanelPlugin[];
+}
+
+export class VizTypePicker extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pluginList: this.getPanelPlugins(''),
+    };
+  }
+
+  getPanelPlugins(filter) {
+    const panels = _.chain(config.panels)
+      .filter({ hideFromList: false })
+      .map(item => item)
+      .value();
+
+    // add sort by sort property
+    return _.sortBy(panels, 'sort');
+  }
+
+  renderVizPlugin = (plugin, index) => {
+    const cssClass = classNames({
+      'viz-picker__item': true,
+      'viz-picker__item--selected': plugin.id === this.props.currentType,
+    });
+
+    return (
+      <div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
+        <img className="viz-picker__item-img" src={plugin.info.logos.small} />
+        <div className="viz-picker__item-name">{plugin.name}</div>
+      </div>
+    );
+  };
+
+  render() {
+    return (
+      <div className="viz-picker">
+        <div className="viz-picker__search">
+          <div className="gf-form gf-form--grow">
+            <label className="gf-form--has-input-icon gf-form--grow">
+              <input type="text" className="gf-form-input" placeholder="Search type" />
+              <i className="gf-form-input-icon fa fa-search" />
+            </label>
+          </div>
+        </div>
+        <div className="viz-picker__items">
+          <CustomScrollbar>
+            <div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
+          </CustomScrollbar>
+        </div>
+      </div>
+    );
+  }
+}

+ 2 - 0
public/app/features/dashboard/dashnav/dashnav.ts

@@ -42,6 +42,8 @@ export class DashNavCtrl {
     } else if (search.fullscreen) {
       delete search.fullscreen;
       delete search.edit;
+      delete search.tab;
+      delete search.panelId;
     }
     this.$location.search(search);
   }

+ 45 - 4
public/app/features/dashboard/panel_model.ts

@@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = {
   events: true,
   fullscreen: true,
   isEditing: true,
+  hasRefreshed: true,
+};
+
+const defaults: any = {
+  gridPos: { x: 0, y: 0, h: 3, w: 6 },
+  datasource: null,
+  targets: [{}],
 };
 
 export class PanelModel {
@@ -31,10 +38,14 @@ export class PanelModel {
   collapsed?: boolean;
   panels?: any;
   soloMode?: boolean;
+  targets: any[];
+  datasource: string;
+  thresholds?: any;
 
   // non persisted
   fullscreen: boolean;
   isEditing: boolean;
+  hasRefreshed: boolean;
   events: Emitter;
 
   constructor(model) {
@@ -45,9 +56,8 @@ export class PanelModel {
       this[property] = model[property];
     }
 
-    if (!this.gridPos) {
-      this.gridPos = { x: 0, y: 0, h: 3, w: 6 };
-    }
+    // defaults
+    _.defaultsDeep(this, _.cloneDeep(defaults));
   }
 
   getSaveModel() {
@@ -57,6 +67,10 @@ export class PanelModel {
         continue;
       }
 
+      if (_.isEqual(this[property], defaults[property])) {
+        continue;
+      }
+
       model[property] = _.cloneDeep(this[property]);
     }
 
@@ -82,7 +96,6 @@ export class PanelModel {
     this.gridPos.h = newPos.h;
 
     if (sizeChanged) {
-      console.log('PanelModel sizeChanged event and render events fired');
       this.events.emit('panel-size-changed');
     }
   }
@@ -91,6 +104,34 @@ export class PanelModel {
     this.events.emit('panel-size-changed');
   }
 
+  refresh() {
+    this.hasRefreshed = true;
+    this.events.emit('refresh');
+  }
+
+  render() {
+    if (!this.hasRefreshed) {
+      this.refresh();
+    } else {
+      this.events.emit('render');
+    }
+  }
+
+  panelInitialized() {
+    this.events.emit('panel-initialized');
+  }
+
+  initEditMode() {
+    this.events.emit('panel-init-edit-mode');
+  }
+
+  changeType(pluginId: string) {
+    this.type = pluginId;
+
+    delete this.thresholds;
+    delete this.alert;
+  }
+
   destroy() {
     this.events.removeAllListeners();
   }

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

@@ -32,7 +32,7 @@ export class SettingsCtrl {
 
     this.$scope.$on('$destroy', () => {
       this.dashboard.updateSubmenuVisibility();
-      this.$rootScope.$broadcast('refresh');
+      this.dashboard.startRefresh();
       setTimeout(() => {
         this.$rootScope.appEvent('dash-scroll', { restore: true });
       });

+ 1 - 2
public/app/features/dashboard/share_snapshot_ctrl.ts

@@ -46,8 +46,7 @@ export class ShareSnapshotCtrl {
 
       $scope.loading = true;
       $scope.snapshot.external = external;
-
-      $rootScope.$broadcast('refresh');
+      $scope.dashboard.startRefresh();
 
       $timeout(() => {
         $scope.saveSnapshot(external);

+ 17 - 7
public/app/features/dashboard/specs/AddPanelPanel.test.tsx

@@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({
 }));
 
 describe('AddPanelPanel', () => {
-  let wrapper, dashboardMock, getPanelContainer, panel;
+  let wrapper, dashboardMock, panel;
 
   beforeEach(() => {
     config.panels = [
@@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Singlestat',
         sort: 2,
+        module: '',
+        baseUrl: '',
+        meta: {},
         info: {
           logos: {
             small: '',
@@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
         hideFromList: true,
         name: 'Hidden',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Graph',
         sort: 1,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Zabbix',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
         hideFromList: false,
         name: 'Piechart',
         sort: 100,
+        meta: {},
+        module: '',
+        baseUrl: '',
         info: {
           logos: {
             small: '',
@@ -77,13 +92,8 @@ describe('AddPanelPanel', () => {
 
     dashboardMock = { toggleRow: jest.fn() };
 
-    getPanelContainer = jest.fn().mockReturnValue({
-      getDashboard: jest.fn().mockReturnValue(dashboardMock),
-      getPanelLoader: jest.fn(),
-    });
-
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<AddPanelPanel panel={panel} dashboard={dashboardMock} />);
   });
 
   it('should fetch all panels sorted with core plugins first', () => {

+ 4 - 9
public/app/features/dashboard/specs/DashboardRow.test.tsx

@@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow';
 import { PanelModel } from '../panel_model';
 
 describe('DashboardRow', () => {
-  let wrapper, panel, getPanelContainer, dashboardMock;
+  let wrapper, panel, dashboardMock;
 
   beforeEach(() => {
     dashboardMock = {
@@ -14,13 +14,8 @@ describe('DashboardRow', () => {
       },
     };
 
-    getPanelContainer = jest.fn().mockReturnValue({
-      getDashboard: jest.fn().mockReturnValue(dashboardMock),
-      getPanelLoader: jest.fn(),
-    });
-
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
   });
 
   it('Should not have collapsed class when collaped is false', () => {
@@ -41,14 +36,14 @@ describe('DashboardRow', () => {
 
   it('should not show row drag handle when cannot edit', () => {
     dashboardMock.meta.canEdit = false;
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
     expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
   });
 
   it('should have zero actions when cannot edit', () => {
     dashboardMock.meta.canEdit = false;
     panel = new PanelModel({ collapsed: false });
-    wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
+    wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
     expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
   });
 });

+ 1 - 1
public/app/features/dashboard/specs/exporter.test.ts

@@ -240,5 +240,5 @@ stubs['-- Grafana --'] = {
 };
 
 function getStub(arg) {
-  return Promise.resolve(stubs[arg]);
+  return Promise.resolve(stubs[arg || 'gfdb']);
 }

+ 7 - 7
public/app/features/dashboard/specs/viewstate_srv.test.ts

@@ -2,6 +2,7 @@
 import 'app/features/dashboard/view_state_srv';
 import config from 'app/core/config';
 import { DashboardViewState } from '../view_state_srv';
+import { DashboardModel } from '../dashboard_model';
 
 describe('when updating view state', () => {
   const location = {
@@ -10,14 +11,13 @@ describe('when updating view state', () => {
   };
 
   const $scope = {
+    appEvent: jest.fn(),
     onAppEvent: jest.fn(() => {}),
-    dashboard: {
-      meta: {},
-      panels: [],
-    },
+    dashboard: new DashboardModel({
+      panels: [{ id: 1 }],
+    }),
   };
 
-  const $rootScope = {};
   let viewState;
 
   beforeEach(() => {
@@ -33,7 +33,7 @@ describe('when updating view state', () => {
       location.search = jest.fn(() => {
         return { fullscreen: true, edit: true, panelId: 1 };
       });
-      viewState = new DashboardViewState($scope, location, {}, $rootScope);
+      viewState = new DashboardViewState($scope, location, {});
     });
 
     it('should update querystring and view state', () => {
@@ -55,7 +55,7 @@ describe('when updating view state', () => {
 
   describe('to fullscreen false', () => {
     beforeEach(() => {
-      viewState = new DashboardViewState($scope, location, {}, $rootScope);
+      viewState = new DashboardViewState($scope, location, {});
     });
     it('should remove params from query string', () => {
       viewState.update({ fullscreen: true, panelId: 1, edit: true });

+ 2 - 2
public/app/features/dashboard/submenu/submenu.ts

@@ -7,13 +7,13 @@ export class SubmenuCtrl {
   dashboard: any;
 
   /** @ngInject */
-  constructor(private $rootScope, private variableSrv, private $location) {
+  constructor(private variableSrv, private $location) {
     this.annotations = this.dashboard.templating.list;
     this.variables = this.variableSrv.variables;
   }
 
   annotationStateChanged() {
-    this.$rootScope.$broadcast('refresh');
+    this.dashboard.startRefresh();
   }
 
   variableUpdated(variable) {

+ 21 - 12
public/app/features/dashboard/time_srv.ts

@@ -1,8 +1,14 @@
+// Libraries
 import moment from 'moment';
 import _ from 'lodash';
-import coreModule from 'app/core/core_module';
+
+// Utils
 import kbn from 'app/core/utils/kbn';
+import coreModule from 'app/core/core_module';
 import * as dateMath from 'app/core/utils/datemath';
+// Types
+
+import { TimeRange } from 'app/types';
 
 export class TimeSrv {
   time: any;
@@ -24,7 +30,6 @@ export class TimeSrv {
     document.addEventListener('visibilitychange', () => {
       if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
         this.autoRefreshBlocked = false;
-
         this.refreshDashboard();
       }
     });
@@ -142,7 +147,7 @@ export class TimeSrv {
   }
 
   refreshDashboard() {
-    this.$rootScope.$broadcast('refresh');
+    this.dashboard.timeRangeUpdated();
   }
 
   private startNextRefreshTimer(afterMs) {
@@ -201,7 +206,7 @@ export class TimeSrv {
     return range;
   }
 
-  timeRange() {
+  timeRange(): TimeRange {
     // make copies if they are moment  (do not want to return out internal moment, because they are mutable!)
     const raw = {
       from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
@@ -223,17 +228,21 @@ export class TimeSrv {
     const timespan = range.to.valueOf() - range.from.valueOf();
     const center = range.to.valueOf() - timespan / 2;
 
-    let to = center + timespan * factor / 2;
-    let from = center - timespan * factor / 2;
-
-    if (to > Date.now() && range.to <= Date.now()) {
-      const offset = to - Date.now();
-      from = from - offset;
-      to = Date.now();
-    }
+    const to = center + timespan * factor / 2;
+    const from = center - timespan * factor / 2;
 
     this.setTime({ from: moment.utc(from), to: moment.utc(to) });
   }
 }
 
+let singleton;
+
+export function setTimeSrv(srv: TimeSrv) {
+  singleton = srv;
+}
+
+export function getTimeSrv(): TimeSrv {
+  return singleton;
+}
+
 coreModule.service('timeSrv', TimeSrv);

+ 1 - 1
public/app/features/dashboard/timepicker/settings.html

@@ -5,7 +5,7 @@
 		<div class="gf-form">
 			<label class="gf-form-label width-10">Timezone</label>
 			<div class="gf-form-select-wrapper">
-				<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>
+				<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'}]"></select>
 			</div>
 		</div>
 

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

@@ -31,9 +31,10 @@ export class TimePickerCtrl {
 
     $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
     $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
-    $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
     $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
 
+    this.dashboard.on('refresh', this.onRefresh.bind(this), $scope);
+
     // init options
     this.panel = this.dashboard.timepicker;
     _.defaults(this.panel, TimePickerCtrl.defaults);

+ 27 - 85
public/app/features/dashboard/view_state_srv.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import config from 'app/core/config';
+import appEvents from 'app/core/app_events';
 import { DashboardModel } from './dashboard_model';
 
 // represents the transient view state
@@ -10,12 +11,11 @@ export class DashboardViewState {
   panelScopes: any;
   $scope: any;
   dashboard: DashboardModel;
-  editStateChanged: any;
   fullscreenPanel: any;
   oldTimeRange: any;
 
   /** @ngInject */
-  constructor($scope, private $location, private $timeout, private $rootScope) {
+  constructor($scope, private $location, private $timeout) {
     const self = this;
     self.state = {};
     self.panelScopes = [];
@@ -33,10 +33,6 @@ export class DashboardViewState {
       self.update(payload);
     });
 
-    $scope.onAppEvent('panel-initialized', (evt, payload) => {
-      self.registerPanel(payload.scope);
-    });
-
     // this marks changes to location during this digest cycle as not to add history item
     // don't want url changes like adding orgId to add browser history
     $location.replace();
@@ -75,9 +71,6 @@ export class DashboardViewState {
       }
     }
 
-    // remember if editStateChanged
-    this.editStateChanged = (state.edit || false) !== (this.state.edit || false);
-
     _.extend(this.state, state);
     this.dashboard.meta.fullscreen = this.state.fullscreen;
 
@@ -124,110 +117,59 @@ export class DashboardViewState {
   }
 
   syncState() {
-    if (this.panelScopes.length === 0) {
-      return;
-    }
-
     if (this.dashboard.meta.fullscreen) {
-      const panelScope = this.getPanelScope(this.state.panelId);
-      if (!panelScope) {
-        return;
-      }
+      const panel = this.dashboard.getPanelById(this.state.panelId);
 
-      if (this.fullscreenPanel) {
-        // if already fullscreen
-        if (this.fullscreenPanel === panelScope && this.editStateChanged === false) {
-          return;
-        } else {
-          this.leaveFullscreen(false);
-        }
-      }
-
-      if (!panelScope.ctrl.editModeInitiated) {
-        panelScope.ctrl.initEditMode();
+      if (!panel) {
+        return;
       }
 
-      if (!panelScope.ctrl.fullscreen) {
-        this.enterFullscreen(panelScope);
+      if (!panel.fullscreen) {
+        this.enterFullscreen(panel);
+      } else {
+        // already in fullscreen view just update the view mode
+        this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
       }
     } else if (this.fullscreenPanel) {
-      this.leaveFullscreen(true);
+      this.leaveFullscreen();
     }
   }
 
-  getPanelScope(id) {
-    return _.find(this.panelScopes, panelScope => {
-      return panelScope.ctrl.panel.id === id;
-    });
-  }
-
-  leaveFullscreen(render) {
-    const self = this;
-    const ctrl = self.fullscreenPanel.ctrl;
-
-    ctrl.editMode = false;
-    ctrl.fullscreen = false;
+  leaveFullscreen() {
+    const panel = this.fullscreenPanel;
 
-    this.dashboard.setViewMode(ctrl.panel, false, false);
-    this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id });
-    this.$scope.appEvent('dash-scroll', { restore: true });
+    this.dashboard.setViewMode(panel, false, false);
 
-    if (!render) {
-      return false;
-    }
+    delete this.fullscreenPanel;
 
     this.$timeout(() => {
-      if (self.oldTimeRange !== ctrl.range) {
-        self.$rootScope.$broadcast('refresh');
+      appEvents.emit('dash-scroll', { restore: true });
+
+      if (this.oldTimeRange !== this.dashboard.time) {
+        this.dashboard.startRefresh();
       } else {
-        self.$rootScope.$broadcast('render');
+        this.dashboard.render();
       }
-      delete self.fullscreenPanel;
     });
-    return true;
   }
 
-  enterFullscreen(panelScope) {
-    const ctrl = panelScope.ctrl;
-
-    ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
-    ctrl.fullscreen = true;
+  enterFullscreen(panel) {
+    const isEditing = this.state.edit && this.dashboard.meta.canEdit;
 
-    this.oldTimeRange = ctrl.range;
-    this.fullscreenPanel = panelScope;
+    this.oldTimeRange = this.dashboard.time;
+    this.fullscreenPanel = panel;
 
     // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
     this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
-    this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
-    this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
-  }
-
-  registerPanel(panelScope) {
-    const self = this;
-    self.panelScopes.push(panelScope);
-
-    if (!self.dashboard.meta.soloMode) {
-      if (self.state.panelId === panelScope.ctrl.panel.id) {
-        if (self.state.edit) {
-          panelScope.ctrl.editPanel();
-        } else {
-          panelScope.ctrl.viewPanel();
-        }
-      }
-    }
-
-    const unbind = panelScope.$on('$destroy', () => {
-      self.panelScopes = _.without(self.panelScopes, panelScope);
-      unbind();
-    });
+    this.dashboard.setViewMode(panel, true, isEditing);
   }
 }
 
 /** @ngInject */
-export function dashboardViewStateSrv($location, $timeout, $rootScope) {
+export function dashboardViewStateSrv($location, $timeout) {
   return {
     create: $scope => {
-      return new DashboardViewState($scope, $location, $timeout, $rootScope);
+      return new DashboardViewState($scope, $location, $timeout);
     },
   };
 }

+ 1 - 28
public/app/features/panel/metrics_panel_ctrl.ts

@@ -7,13 +7,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
 import { getExploreUrl } from 'app/core/utils/explore';
-
 import { metricsTabDirective } from './metrics_tab';
 
 class MetricsPanelCtrl extends PanelCtrl {
   scope: any;
   datasource: any;
-  datasourceName: any;
   $q: any;
   $timeout: any;
   contextSrv: any;
@@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.scope = $scope;
     this.panel.datasource = this.panel.datasource || null;
 
-    if (!this.panel.targets) {
-      this.panel.targets = [{}];
-    }
-
     this.events.on('refresh', this.onMetricsPanelRefresh.bind(this));
     this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this));
     this.events.on('panel-teardown', this.onPanelTearDown.bind(this));
@@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl {
   }
 
   private onInitMetricsPanelEditMode() {
-    this.addEditorTab('Metrics', metricsTabDirective);
+    this.addEditorTab('Metrics', metricsTabDirective, 1, 'fa fa-database');
     this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
   }
 
@@ -291,27 +285,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     });
   }
 
-  setDatasource(datasource) {
-    // switching to mixed
-    if (datasource.meta.mixed) {
-      _.each(this.panel.targets, target => {
-        target.datasource = this.panel.datasource;
-        if (!target.datasource) {
-          target.datasource = config.defaultDatasource;
-        }
-      });
-    } else if (this.datasource && this.datasource.meta.mixed) {
-      _.each(this.panel.targets, target => {
-        delete target.datasource;
-      });
-    }
-
-    this.panel.datasource = datasource.value;
-    this.datasourceName = datasource.name;
-    this.datasource = null;
-    this.refresh();
-  }
-
   getAdditionalMenuItems() {
     const items = [];
     if (

+ 34 - 2
public/app/features/panel/metrics_tab.ts

@@ -1,6 +1,14 @@
-import { DashboardModel } from '../dashboard/dashboard_model';
+// Libraries
+import _ from 'lodash';
 import Remarkable from 'remarkable';
 
+// Services & utils
+import coreModule from 'app/core/core_module';
+import config from 'app/core/config';
+
+// Types
+import { DashboardModel } from '../dashboard/dashboard_model';
+
 export class MetricsTabCtrl {
   dsName: string;
   panel: any;
@@ -24,6 +32,9 @@ export class MetricsTabCtrl {
     $scope.ctrl = this;
 
     this.panel = this.panelCtrl.panel;
+    this.panel.datasource = this.panel.datasource || null;
+    this.panel.targets = this.panel.targets || [{}];
+
     this.dashboard = this.panelCtrl.dashboard;
     this.datasources = datasourceSrv.getMetricSources();
     this.panelDsValue = this.panelCtrl.panel.datasource;
@@ -66,10 +77,29 @@ export class MetricsTabCtrl {
     }
 
     this.datasourceInstance = option.datasource;
-    this.panelCtrl.setDatasource(option.datasource);
+    this.setDatasource(option.datasource);
     this.updateDatasourceOptions();
   }
 
+  setDatasource(datasource) {
+    // switching to mixed
+    if (datasource.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        target.datasource = this.panel.datasource;
+        if (!target.datasource) {
+          target.datasource = config.defaultDatasource;
+        }
+      });
+    } else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) {
+      _.each(this.panel.targets, target => {
+        delete target.datasource;
+      });
+    }
+
+    this.panel.datasource = datasource.value;
+    this.panel.refresh();
+  }
+
   addMixedQuery(option) {
     if (!option) {
       return;
@@ -120,3 +150,5 @@ export function metricsTabDirective() {
     controller: MetricsTabCtrl,
   };
 }
+
+coreModule.directive('metricsTab', metricsTabDirective);

+ 17 - 20
public/app/features/panel/panel_ctrl.ts

@@ -24,10 +24,8 @@ export class PanelCtrl {
   $injector: any;
   $location: any;
   $timeout: any;
-  fullscreen: boolean;
   inspector: any;
   editModeInitiated: boolean;
-  editMode: any;
   height: any;
   containerHeight: any;
   events: Emitter;
@@ -49,7 +47,6 @@ export class PanelCtrl {
       this.pluginName = plugin.name;
     }
 
-    $scope.$on('refresh', () => this.refresh());
     $scope.$on('component-did-mount', () => this.panelDidMount());
 
     $scope.$on('$destroy', () => {
@@ -58,13 +55,9 @@ export class PanelCtrl {
     });
   }
 
-  init() {
-    this.events.emit('panel-initialized');
-    this.publishAppEvent('panel-initialized', { scope: this.$scope });
-  }
-
   panelDidMount() {
     this.events.emit('component-did-mount');
+    this.dashboard.panelInitialized(this.panel);
   }
 
   renderingCompleted() {
@@ -72,7 +65,7 @@ export class PanelCtrl {
   }
 
   refresh() {
-    this.events.emit('refresh', null);
+    this.panel.refresh();
   }
 
   publishAppEvent(evtName, evt) {
@@ -102,6 +95,7 @@ export class PanelCtrl {
   initEditMode() {
     this.editorTabs = [];
     this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
+
     this.editModeInitiated = true;
     this.events.emit('init-edit-mode', null);
 
@@ -122,14 +116,15 @@ export class PanelCtrl {
     route.updateParams();
   }
 
-  addEditorTab(title, directiveFn, index?) {
-    const editorTab = { title, directiveFn };
+  addEditorTab(title, directiveFn, index?, icon?) {
+    const editorTab = { title, directiveFn, icon };
 
     if (_.isString(directiveFn)) {
       editorTab.directiveFn = () => {
         return { templateUrl: directiveFn };
       };
     }
+
     if (index) {
       this.editorTabs.splice(index, 0, editorTab);
     } else {
@@ -190,7 +185,7 @@ export class PanelCtrl {
 
   getExtendedMenu() {
     const menu = [];
-    if (!this.fullscreen && this.dashboard.meta.canEdit) {
+    if (!this.panel.fullscreen && this.dashboard.meta.canEdit) {
       menu.push({
         text: 'Duplicate',
         click: 'ctrl.duplicate()',
@@ -220,15 +215,15 @@ export class PanelCtrl {
   }
 
   otherPanelInFullscreenMode() {
-    return this.dashboard.meta.fullscreen && !this.fullscreen;
+    return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
   }
 
   calculatePanelHeight() {
-    if (this.fullscreen) {
-      const docHeight = $(window).height();
-      const editHeight = Math.floor(docHeight * 0.4);
+    if (this.panel.fullscreen) {
+      const docHeight = $('.react-grid-layout').height();
+      const editHeight = Math.floor(docHeight * 0.35);
       const fullscreenHeight = Math.floor(docHeight * 0.8);
-      this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
+      this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight;
     } else {
       this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
     }
@@ -237,6 +232,11 @@ export class PanelCtrl {
       this.containerHeight = $(window).height();
     }
 
+    // hacky solution
+    if (this.panel.isEditing && !this.editModeInitiated) {
+      this.initEditMode();
+    }
+
     this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT);
   }
 
@@ -247,9 +247,6 @@ export class PanelCtrl {
 
   duplicate() {
     this.dashboard.duplicatePanel(this.panel);
-    this.$timeout(() => {
-      this.$scope.$root.$broadcast('render');
-    });
   }
 
   removePanel() {

+ 43 - 44
public/app/features/panel/panel_directive.ts

@@ -6,48 +6,53 @@ import baron from 'baron';
 const module = angular.module('grafana.directives');
 
 const panelTemplate = `
-  <div class="panel-container">
-    <div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.fullscreen}">
-      <span class="panel-info-corner">
-        <i class="fa"></i>
-        <span class="panel-info-corner-inner"></span>
-      </span>
-
-      <span class="panel-loading" ng-show="ctrl.loading">
-        <i class="fa fa-spinner fa-spin"></i>
-      </span>
-
-      <panel-header class="panel-title-container" panel-ctrl="ctrl"></panel-header>
-    </div>
+  <div ng-class="{'panel-editor-container': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
+    <div ng-class="{'panel-editor-container__panel': ctrl.panel.isEditing, 'panel-height-helper': !ctrl.panel.isEditing}">
+      <div class="panel-container">
+        <div class="panel-header" ng-class="{'grid-drag-handle': !ctrl.panel.fullscreen}">
+          <span class="panel-info-corner">
+            <i class="fa"></i>
+            <span class="panel-info-corner-inner"></span>
+          </span>
+
+          <span class="panel-loading" ng-show="ctrl.loading">
+            <i class="fa fa-spinner fa-spin"></i>
+          </span>
+
+          <panel-header class="panel-title-container" panel-ctrl="ctrl"></panel-header>
+        </div>
 
-    <div class="panel-content">
-      <ng-transclude class="panel-height-helper"></ng-transclude>
+        <div class="panel-content">
+          <ng-transclude class="panel-height-helper"></ng-transclude>
+        </div>
+      </div>
     </div>
-  </div>
 
-  <div class="panel-full-edit" ng-if="ctrl.editMode">
-    <div class="tabbed-view tabbed-view--panel-edit">
-      <div class="tabbed-view-header">
-        <h3 class="tabbed-view-panel-title">
-          {{ctrl.pluginName}}
-        </h3>
-
-        <ul class="gf-tabs">
-          <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
-            <a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
-              {{::tab.title}}
-            </a>
-          </li>
-        </ul>
-
-        <button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
-          <i class="fa fa-remove"></i>
-        </button>
-      </div>
+    <div ng-if="ctrl.panel.isEditing" ng-class="{'panel-editor-container__editor': ctrl.panel.isEditing,
+                                                 'panel-height-helper': !ctrl.panel.isEditing}">
+      <div class="tabbed-view tabbed-view--new">
+        <div class="tabbed-view-header">
+          <h3 class="tabbed-view-panel-title">
+            {{ctrl.pluginName}}
+          </h3>
+
+          <ul class="gf-tabs">
+            <li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
+              <a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
+                {{::tab.title}}
+              </a>
+            </li>
+          </ul>
+
+          <button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
+            <i class="fa fa-remove"></i>
+          </button>
+        </div>
 
-      <div class="tabbed-view-body">
-        <div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index">
-          <panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
+        <div class="tabbed-view-body">
+          <div ng-repeat="tab in ctrl.editorTabs" ng-if="ctrl.editorTabIndex === $index" class="panel-height-helper">
+            <panel-editor-tab editor-tab="tab" ctrl="ctrl" index="$index"></panel-editor-tab>
+          </div>
         </div>
       </div>
     </div>
@@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
         ctrl.dashboard.setPanelFocus(0);
       }
 
-      function panelHeightUpdated() {
-        panelContent.css({ height: ctrl.height + 'px' });
-      }
-
       function resizeScrollableContent() {
         if (panelScrollbar) {
           panelScrollbar.update();
@@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
       ctrl.events.on('panel-size-changed', () => {
         ctrl.calculatePanelHeight();
-        panelHeightUpdated();
         $timeout(() => {
           resizeScrollableContent();
           ctrl.render();
@@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
 
       // set initial height
       ctrl.calculatePanelHeight();
-      panelHeightUpdated();
 
       ctrl.events.on('render', () => {
         if (transparentLastState !== ctrl.panel.transparent) {

+ 18 - 10
public/app/features/panel/panel_editor_tab.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
 
 const directiveModule = angular.module('grafana.directives');
+const directiveCache = {};
 
 /** @ngInject */
 function panelEditorTab(dynamicDirectiveSrv) {
@@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) {
     },
     directive: scope => {
       const pluginId = scope.ctrl.pluginId;
-      const tabIndex = scope.index;
-      // create a wrapper for directiveFn
-      // required for metrics tab directive
-      // that is the same for many panels but
-      // given different names in this function
-      const fn = () => scope.editorTab.directiveFn();
+      const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-');
 
-      return Promise.resolve({
-        name: `panel-editor-tab-${pluginId}${tabIndex}`,
-        fn: fn,
-      });
+      if (directiveCache[pluginId]) {
+        if (directiveCache[pluginId][tabName]) {
+          return directiveCache[pluginId][tabName];
+        }
+      } else {
+        directiveCache[pluginId] = [];
+      }
+
+      const result = {
+        fn: () => scope.editorTab.directiveFn(),
+        name: `panel-editor-tab-${pluginId}${tabName}`,
+      };
+
+      directiveCache[pluginId][tabName] = result;
+
+      return result;
     },
   });
 }

+ 0 - 15
public/app/features/panel/panel_header.ts

@@ -8,21 +8,6 @@ const template = `
   <span class="panel-menu-container dropdown">
     <span class="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown"></span>
     <ul class="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
-      <li>
-        <a ng-click="ctrl.addDataQuery(datasource);">
-          <i class="fa fa-cog"></i> Edit <span class="dropdown-menu-item-shortcut">e</span>
-        </a>
-      </li>
-      <li class="dropdown-submenu">
-        <a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-cube"></i> Actions</a>
-        <ul class="dropdown-menu panel-menu">
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-flash"></i> Add Annotation</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-bullseye"></i> Toggle Legend</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-download"></i> Export to CSV</a></li>
-          <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-eye"></i> View JSON</a></li>
-        </ul>
-      </li>
-      <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
     </ul>
   </span>
   <span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>

+ 0 - 4
public/app/features/panel/partials/metrics_tab.html

@@ -1,11 +1,7 @@
 <div class="gf-form-group">
   <div class="gf-form-inline">
     <div class="gf-form">
-			<label class="gf-form-label gf-query-ds-label">
-				<i class="icon-gf icon-gf-datasources"></i>
-			</label>
       <label class="gf-form-label">Data Source</label>
-
       <gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
                         lookup-text="true"
                         get-options="ctrl.getOptions(true)"

+ 73 - 0
public/app/features/panel/viz_tab.ts

@@ -0,0 +1,73 @@
+import coreModule from 'app/core/core_module';
+import { DashboardModel } from '../dashboard/dashboard_model';
+import { VizTypePicker } from '../dashboard/dashgrid/VizTypePicker';
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { PanelPlugin } from 'app/types/plugins';
+
+export class VizTabCtrl {
+  panelCtrl: any;
+  dashboard: DashboardModel;
+
+  /** @ngInject */
+  constructor($scope) {
+    this.panelCtrl = $scope.ctrl;
+    this.dashboard = this.panelCtrl.dashboard;
+
+    $scope.ctrl = this;
+  }
+
+  onTypeChanged = (plugin: PanelPlugin) => {
+    this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id);
+  };
+}
+
+const template = `
+<div class="gf-form-group ">
+  <div class="gf-form-query">
+    <div class="gf-form">
+      <label class="gf-form-label">
+        <img src="public/app/plugins/panel/graph/img/icn-graph-panel.svg" style="width: 16px; height: 16px" />
+        Graph
+        <i class="fa fa-caret-down" />
+      </label>
+		</div>
+
+		<div class="gf-form gf-form--grow">
+			<label class="gf-form-label gf-form-label--grow"></label>
+		</div>
+	</div>
+
+	<br />
+	<br />
+
+  <div class="query-editor-rows gf-form-group">
+	  <div ng-repeat="tab in ctrl.panelCtrl.optionTabs">
+	    <div class="gf-form-query">
+		    <div class="gf-form gf-form-query-letter-cell">
+			    <label class="gf-form-label">
+				    <span class="gf-form-query-letter-cell-carret">
+					    <i class="fa fa-caret-down"></i>
+				    </span>
+				    <span class="gf-form-query-letter-cell-letter">{{tab.title}}</span>
+          </label>
+			  </div>
+        <div class="gf-form gf-form--grow">
+			    <label class="gf-form-label gf-form-label--grow"></label>
+		    </div>
+			</div>
+		</div>
+	</div>
+</div>`;
+
+/** @ngInject */
+export function vizTabDirective() {
+  'use strict';
+  return {
+    restrict: 'E',
+    template: template,
+    controller: VizTabCtrl,
+  };
+}
+
+react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]);
+coreModule.directive('vizTab', vizTabDirective);

+ 4 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -14,6 +14,8 @@ import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
 import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
 
 import * as textPanel from 'app/plugins/panel/text/module';
+import * as text2Panel from 'app/plugins/panel/text2/module';
+import * as graph2Panel from 'app/plugins/panel/graph2/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
 import * as dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
@@ -40,6 +42,8 @@ const builtInPlugins = {
   'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
 
   'app/plugins/panel/text/module': textPanel,
+  'app/plugins/panel/text2/module': text2Panel,
+  'app/plugins/panel/graph2/module': graph2Panel,
   'app/plugins/panel/graph/module': graphPanel,
   'app/plugins/panel/dashlist/module': dashListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,

+ 17 - 1
public/app/features/plugins/datasource_srv.ts

@@ -1,8 +1,14 @@
+// Libraries
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
+
+// Utils
 import config from 'app/core/config';
 import { importPluginModule } from './plugin_loader';
 
+// Types
+import { DataSourceApi } from 'app/types/series';
+
 export class DatasourceSrv {
   datasources: any;
 
@@ -15,7 +21,7 @@ export class DatasourceSrv {
     this.datasources = {};
   }
 
-  get(name?) {
+  get(name?): Promise<DataSourceApi> {
     if (!name) {
       return this.get(config.defaultDatasource);
     }
@@ -162,5 +168,15 @@ export class DatasourceSrv {
   }
 }
 
+let singleton: DatasourceSrv;
+
+export function setDatasourceSrv(srv: DatasourceSrv) {
+  singleton = srv;
+}
+
+export function getDatasourceSrv(): DatasourceSrv {
+  return singleton;
+}
+
 coreModule.service('datasourceSrv', DatasourceSrv);
 export default DatasourceSrv;

+ 8 - 6
public/app/features/plugins/plugin_component.ts

@@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader';
 import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
 
 /** @ngInject */
-function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
+function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
   function getTemplate(component) {
     if (component.template) {
       return $q.when(component.template);
@@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
       PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
         PanelCtrl.templateUrl = null;
-        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`;
+        PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-editor-container">${template}</grafana-panel>`;
         return componentInfo;
       });
 
@@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
 
     // let a binding digest cycle complete before adding to dom
     setTimeout(() => {
-      elem.append(child);
       scope.$applyAsync(() => {
-        scope.$broadcast('component-did-mount');
-        scope.$broadcast('refresh');
+        elem.append(child);
+        setTimeout(() => {
+          scope.$applyAsync(() => {
+            scope.$broadcast('component-did-mount');
+          });
+        });
       });
     });
   }
@@ -245,7 +248,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
           registerPluginComponent(scope, elem, attrs, componentInfo);
         })
         .catch(err => {
-          $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
           console.log('Plugin component error', err);
         });
     },

+ 3 - 1
public/app/features/plugins/plugin_loader.ts

@@ -18,6 +18,7 @@ import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
 import { coreModule, appEvents, contextSrv } from 'app/core/core';
+import { PluginExports } from 'app/types/plugins';
 import * as datemath from 'app/core/utils/datemath';
 import * as fileExport from 'app/core/utils/file_export';
 import * as flatten from 'app/core/utils/flatten';
@@ -140,11 +141,12 @@ const flotDeps = [
   'jquery.flot.events',
   'jquery.flot.gauge',
 ];
+
 for (const flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
 }
 
-export function importPluginModule(path: string): Promise<any> {
+export function importPluginModule(path: string): Promise<PluginExports> {
   const builtIn = builtInPlugins[path];
   if (builtIn) {
     return Promise.resolve(builtIn);

+ 7 - 4
public/app/features/templating/specs/variable_srv.test.ts

@@ -1,5 +1,6 @@
 import '../all';
 import { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import moment from 'moment';
 import $q from 'q';
 
@@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) {
           return getVarMockConstructor(ctr, model, ctx);
         };
 
-        ctx.variableSrv.init({
-          templating: { list: [] },
-          updateSubmenuVisibility: () => {},
-        });
+        ctx.variableSrv.init(
+          new DashboardModel({
+            templating: { list: [] },
+            updateSubmenuVisibility: () => {},
+          })
+        );
 
         scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel);
         ctx.variableSrv.addVariable(scenario.variable);

+ 3 - 2
public/app/features/templating/specs/variable_srv_init.test.ts

@@ -2,6 +2,7 @@ import '../all';
 
 import _ from 'lodash';
 import { VariableSrv } from '../variable_srv';
+import { DashboardModel } from '../../dashboard/dashboard_model';
 import $q from 'q';
 
 describe('VariableSrv init', function(this: any) {
@@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) {
         ctx.variableSrv.datasourceSrv = ctx.datasourceSrv;
 
         ctx.variableSrv.$location.search = () => scenario.urlParams;
-        ctx.variableSrv.dashboard = {
+        ctx.variableSrv.dashboard = new DashboardModel({
           templating: { list: scenario.variables },
-        };
+        });
 
         await ctx.variableSrv.init(ctx.variableSrv.dashboard);
 

+ 9 - 9
public/app/features/templating/variable_srv.ts

@@ -1,5 +1,8 @@
+// Libaries
 import angular from 'angular';
 import _ from 'lodash';
+
+// Utils & Services
 import coreModule from 'app/core/core_module';
 import { variableTypes } from './variable';
 import { Graph } from 'app/core/utils/dag';
@@ -10,13 +13,12 @@ export class VariableSrv {
 
   /** @ngInject */
   constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
-    // update time variant variables
-    $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope);
     $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
   }
 
   init(dashboard) {
     this.dashboard = dashboard;
+    this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
 
     // create working class models representing variables
     this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
@@ -39,11 +41,7 @@ export class VariableSrv {
       });
   }
 
-  onDashboardRefresh(evt, payload) {
-    if (payload && payload.fromVariableValueUpdated) {
-      return Promise.resolve({});
-    }
-
+  onTimeRangeUpdated() {
     const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
       const previousOptions = variable.options.slice();
 
@@ -54,7 +52,9 @@ export class VariableSrv {
       });
     });
 
-    return this.$q.all(promises);
+    return this.$q.all(promises).then(() => {
+      this.dashboard.startRefresh();
+    });
   }
 
   processVariable(variable, queryParams) {
@@ -133,7 +133,7 @@ export class VariableSrv {
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
         this.$rootScope.$emit('template-variable-value-updated');
-        this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true });
+        this.dashboard.startRefresh();
       }
     });
   }

+ 2 - 3
public/app/partials/dashboard.html

@@ -7,12 +7,11 @@
                         class="dashboard-settings">
     </dashboard-settings>
 
-    <div class="dashboard-container">
+		<div class="dashboard-container" ng-class="{'dashboard-container--has-submenu': ctrl.dashboard.meta.submenuEnabled}">
       <dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
       </dashboard-submenu>
 
-      <dashboard-grid get-panel-container="ctrl.getPanelContainer">
-      </dashboard-grid>
+      <dashboard-grid dashboard="ctrl.dashboard"></dashboard-grid>
     </div>
   </div>
 </div>

+ 3 - 2
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts

@@ -1,4 +1,5 @@
 import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 
 export class CloudWatchQueryParameter {
@@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl {
   }
 }
 
-angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
-angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);
+coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter);
+coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl);

+ 3 - 4
public/app/plugins/datasource/elasticsearch/bucket_agg.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import * as queryDef from './query_def';
 
@@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl {
   }
 }
 
-const module = angular.module('grafana.directives');
-module.directive('elasticBucketAgg', elasticBucketAgg);
-module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);
+coreModule.directive('elasticBucketAgg', elasticBucketAgg);
+coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl);

+ 3 - 4
public/app/plugins/datasource/elasticsearch/metric_agg.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import * as queryDef from './query_def';
 
@@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl {
   }
 }
 
-const module = angular.module('grafana.directives');
-module.directive('elasticMetricAgg', elasticMetricAgg);
-module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);
+coreModule.directive('elasticMetricAgg', elasticMetricAgg);
+coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl);

+ 2 - 2
public/app/plugins/datasource/graphite/add_graphite_func.ts

@@ -1,8 +1,8 @@
-import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 import rst2html from 'rst2html';
 import Drop from 'tether-drop';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 export function graphiteAddFunc($compile) {
@@ -130,7 +130,7 @@ export function graphiteAddFunc($compile) {
   };
 }
 
-angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc);
+coreModule.directive('graphiteAddFunc', graphiteAddFunc);
 
 function createFunctionDropDownMenu(funcDefs) {
   const categories = {};

+ 2 - 2
public/app/plugins/datasource/graphite/func_editor.ts

@@ -1,7 +1,7 @@
-import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 import rst2html from 'rst2html';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
@@ -315,4 +315,4 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
   };
 }
 
-angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor);
+coreModule.directive('graphiteFuncEditor', graphiteFuncEditor);

+ 3 - 3
public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import * as options from './constants';
 import kbn from 'app/core/utils/kbn';
@@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl {
   }
 }
 
-angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation);
-angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);
+coreModule.directive('stackdriverAggregation', StackdriverAggregation);
+coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);

+ 3 - 3
public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts

@@ -1,4 +1,4 @@
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 import _ from 'lodash';
 import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
 import appEvents from 'app/core/app_events';
@@ -281,5 +281,5 @@ export class StackdriverFilterCtrl {
   }
 }
 
-angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter);
-angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl);
+coreModule.directive('stackdriverFilter', StackdriverFilter);
+coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl);

+ 0 - 1
public/app/plugins/datasource/testdata/datasource.ts

@@ -62,7 +62,6 @@ class TestDataDatasource {
           });
         }
 
-        console.log(res);
         return { data: data };
       });
   }

+ 2 - 4
public/app/plugins/panel/graph/legend.ts

@@ -1,11 +1,9 @@
-import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 import baron from 'baron';
+import coreModule from 'app/core/core_module';
 
-const module = angular.module('grafana.directives');
-
-module.directive('graphLegend', (popoverSrv, $timeout) => {
+coreModule.directive('graphLegend', (popoverSrv, $timeout) => {
   return {
     link: (scope, elem) => {
       let firstRender = true;

+ 1 - 1
public/app/plugins/panel/graph/module.ts

@@ -134,9 +134,9 @@ class GraphCtrl extends MetricsPanelCtrl {
   }
 
   onInitEditMode() {
+    this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
     this.addEditorTab('Axes', axesEditorComponent, 2);
     this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3);
-    this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
 
     if (config.alertingEnabled) {
       this.addEditorTab('Alert', alertTab, 5);

+ 2 - 2
public/app/plugins/panel/graph/series_overrides_ctrl.ts

@@ -1,5 +1,5 @@
 import _ from 'lodash';
-import angular from 'angular';
+import coreModule from 'app/core/core_module';
 
 /** @ngInject */
 export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
@@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
   $scope.updateCurrentOverrides();
 }
 
-angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);
+coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl);

+ 5 - 0
public/app/plugins/panel/graph2/README.md

@@ -0,0 +1,5 @@
+# Text Panel -  Native Plugin
+
+The Text Panel is **included** with Grafana.
+
+The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more.

+ 26 - 0
public/app/plugins/panel/graph2/img/icn-text-panel.svg

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<rect style="opacity:0.2;fill:#414042;" width="100" height="100"/>
+<g>
+	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="50" y1="88.2189" x2="50" y2="11.7811">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0.0595" style="stop-color:#FFE029"/>
+		<stop  offset="0.1303" style="stop-color:#FFD218"/>
+		<stop  offset="0.2032" style="stop-color:#FEC90F"/>
+		<stop  offset="0.2809" style="stop-color:#FDC70C"/>
+		<stop  offset="0.6685" style="stop-color:#F3903F"/>
+		<stop  offset="0.8876" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_1_);" d="M15.107,30.157h-2.593l0.395-18.376h74.183l0.395,18.376h-2.424
+		c-0.865-5.035-2.049-8.671-3.551-10.908c-1.504-2.235-3.12-3.607-4.848-4.115c-1.729-0.507-4.679-0.761-8.85-0.761H55.524V68.32
+		c0,5.975,0.141,9.903,0.423,11.781c0.282,1.88,1.043,3.27,2.283,4.171c1.24,0.902,3.57,1.353,6.99,1.353h3.72v2.593H30.834v-2.593
+		h3.946c3.269,0,5.533-0.413,6.793-1.24c1.258-0.826,2.066-2.114,2.424-3.861c0.357-1.747,0.535-5.815,0.535-12.204V14.374h-11.33
+		c-4.924,0-8.23,0.235-9.921,0.704c-1.691,0.471-3.279,1.888-4.764,4.256C17.032,21.702,15.896,25.31,15.107,30.157z"/>
+</g>
+<g>
+	<path style="fill:#898989;" d="M99,1v98H1V1H99 M100,0H0v100h100V0L100,0z"/>
+</g>
+</svg>

+ 43 - 0
public/app/plugins/panel/graph2/module.tsx

@@ -0,0 +1,43 @@
+// Libraries
+import _ from 'lodash';
+import React, { PureComponent } from 'react';
+
+// Components
+import Graph from 'app/viz/Graph';
+import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
+
+// Types
+import { PanelProps, NullValueMode } from 'app/types';
+
+interface Options {
+  showBars: boolean;
+}
+
+interface Props extends PanelProps {
+  options: Options;
+}
+
+export class Graph2 extends PureComponent<Props> {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    const { timeSeries, timeRange } = this.props;
+
+    const vmSeries = getTimeSeriesVMs({
+      timeSeries: timeSeries,
+      nullValueMode: NullValueMode.Ignore,
+    });
+
+    return <Graph timeSeries={vmSeries} timeRange={timeRange} />;
+  }
+}
+
+export class TextOptions extends PureComponent<any> {
+  render() {
+    return <p>Text2 Options component</p>;
+  }
+}
+
+export { Graph2 as PanelComponent, TextOptions as PanelOptions };

+ 17 - 0
public/app/plugins/panel/graph2/plugin.json

@@ -0,0 +1,17 @@
+{
+  "type": "panel",
+  "name": "React Graph",
+  "id": "graph2",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-text-panel.svg",
+      "large": "img/icn-text-panel.svg"
+    }
+  }
+}
+

+ 3 - 5
public/app/plugins/panel/heatmap/color_legend.ts

@@ -1,12 +1,10 @@
-import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
 import * as d3 from 'd3';
 import { contextSrv } from 'app/core/core';
 import { tickStep } from 'app/core/utils/ticks';
 import { getColorScale, getOpacityScale } from './color_scale';
-
-const module = angular.module('grafana.directives');
+import coreModule from 'app/core/core_module';
 
 const LEGEND_HEIGHT_PX = 6;
 const LEGEND_WIDTH_PX = 100;
@@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0;
 /**
  * Color legend for heatmap editor.
  */
-module.directive('colorLegend', () => {
+coreModule.directive('colorLegend', () => {
   return {
     restrict: 'E',
     template: '<div class="heatmap-color-legend"><svg width="16.5rem" height="24px"></svg></div>',
@@ -52,7 +50,7 @@ module.directive('colorLegend', () => {
 /**
  * Heatmap legend with scale values.
  */
-module.directive('heatmapLegend', () => {
+coreModule.directive('heatmapLegend', () => {
   return {
     restrict: 'E',
     template: `<div class="heatmap-color-legend"><svg width="${LEGEND_WIDTH_PX}px" height="${LEGEND_HEIGHT_PX}px"></svg></div>`,

+ 5 - 0
public/app/plugins/panel/text2/README.md

@@ -0,0 +1,5 @@
+# Text Panel -  Native Plugin
+
+The Text Panel is **included** with Grafana.
+
+The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more.

+ 186 - 0
public/app/plugins/panel/text2/img/icn-graph-panel.svg

@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<polyline style="fill:none;stroke:#898989;stroke-width:2;stroke-miterlimit:10;" points="4.734,34.349 36.05,19.26 64.876,36.751 
+	96.308,6.946 "/>
+<circle style="fill:#898989;" cx="4.885" cy="33.929" r="4.885"/>
+<circle style="fill:#898989;" cx="35.95" cy="19.545" r="4.885"/>
+<circle style="fill:#898989;" cx="65.047" cy="36.046" r="4.885"/>
+<circle style="fill:#898989;" cx="94.955" cy="7.135" r="4.885"/>
+<g>
+	<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="5" y1="103.7019" x2="5" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_1_);" d="M9.001,48.173H0.999C0.447,48.173,0,48.62,0,49.172V100h10V49.172
+		C10,48.62,9.553,48.173,9.001,48.173z"/>
+	<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5" y1="98.9423" x2="5" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_2_);" d="M0,69.173v30.563h10V69.173"/>
+	<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="5" y1="99.4343" x2="5" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_3_);" d="M0,83.166v16.701h10V83.166"/>
+</g>
+<g>
+	<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="20" y1="103.7019" x2="20" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_4_);" d="M24.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
+		C25,41.216,24.553,40.769,24.001,40.769z"/>
+	<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="20" y1="98.9423" x2="20" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_5_);" d="M15,64.716v35.02h10v-35.02"/>
+	<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="20" y1="99.4343" x2="20" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_6_);" d="M15,80.731v19.137h10V80.731"/>
+</g>
+<g>
+	<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="35" y1="103.7019" x2="35" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_7_);" d="M39.001,34.423h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V35.422
+		C40,34.87,39.553,34.423,39.001,34.423z"/>
+	<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="35" y1="98.9423" x2="35" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_8_);" d="M30,60.895v38.84h10v-38.84"/>
+	<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="35" y1="99.4343" x2="35" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_9_);" d="M30,78.643v21.225h10V78.643"/>
+</g>
+<g>
+	<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="50" y1="103.7019" x2="50" y2="32.0424">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_10_);" d="M54.001,41.827h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V42.826
+		C55,42.274,54.553,41.827,54.001,41.827z"/>
+	<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="50" y1="98.9423" x2="50" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_11_);" d="M45,65.352v34.383h10V65.352"/>
+	<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="50" y1="99.4343" x2="50" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_12_);" d="M45,81.079v18.789h10V81.079"/>
+</g>
+<g>
+	<linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="65" y1="103.8575" x2="65" y2="29.1875">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_13_);" d="M69.001,50.404h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V51.403
+		C70,50.851,69.553,50.404,69.001,50.404z"/>
+	<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="65" y1="98.8979" x2="65" y2="51.2298">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_14_);" d="M60,70.531v29.193h10V70.531"/>
+	<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="65" y1="99.4105" x2="65" y2="73.3619">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_15_);" d="M60,83.909v15.953h10V83.909"/>
+</g>
+<g>
+	<linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="80" y1="104.4108" x2="80" y2="19.0293">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_16_);" d="M84.001,40.769h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V41.768
+		C85,41.216,84.553,40.769,84.001,40.769z"/>
+	<linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="80" y1="98.9423" x2="80" y2="53.1961">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_17_);" d="M75,64.716v35.02h10v-35.02"/>
+	<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="80" y1="99.4343" x2="80" y2="74.4359">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_18_);" d="M75,80.731v19.137h10V80.731"/>
+</g>
+<g>
+	<linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="95" y1="103.5838" x2="95" y2="34.2115">
+		<stop  offset="0" style="stop-color:#FFF33B"/>
+		<stop  offset="0" style="stop-color:#FFD53F"/>
+		<stop  offset="0" style="stop-color:#FBBC40"/>
+		<stop  offset="0" style="stop-color:#F7A840"/>
+		<stop  offset="0" style="stop-color:#F59B40"/>
+		<stop  offset="0" style="stop-color:#F3933F"/>
+		<stop  offset="0" style="stop-color:#F3903F"/>
+		<stop  offset="0.8423" style="stop-color:#ED683C"/>
+		<stop  offset="1" style="stop-color:#E93E3A"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_19_);" d="M99.001,21.157h-8.002c-0.552,0-0.999,0.447-0.999,0.999V100h10V22.156
+		C100,21.604,99.553,21.157,99.001,21.157z"/>
+	<linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="95" y1="98.9761" x2="95" y2="54.69">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#F99B1C"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_20_);" d="M90,52.898v46.846h10V52.898"/>
+	<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="95" y1="99.4524" x2="95" y2="75.2518">
+		<stop  offset="0" style="stop-color:#FEBC11"/>
+		<stop  offset="1" style="stop-color:#FFDE17"/>
+	</linearGradient>
+	<path style="fill:url(#SVGID_21_);" d="M90,74.272v25.6h10v-25.6"/>
+</g>
+</svg>

+ 14 - 0
public/app/plugins/panel/text2/module.tsx

@@ -0,0 +1,14 @@
+import React, { PureComponent } from 'react';
+import { PanelProps } from 'app/types';
+
+export class Text2 extends PureComponent<PanelProps> {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    return <h2>Text Panel!</h2>;
+  }
+}
+
+export { Text2 as PanelComponent };

+ 19 - 0
public/app/plugins/panel/text2/plugin.json

@@ -0,0 +1,19 @@
+{
+  "type": "panel",
+  "name": "Text v2",
+  "id": "text2",
+
+  "state": "alpha",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-graph-panel.svg",
+      "large": "img/icn-graph-panel.svg"
+    }
+  }
+}
+

+ 7 - 3
public/app/core/components/grafana_app.ts → public/app/routes/GrafanaCtrl.ts

@@ -8,9 +8,10 @@ import appEvents from 'app/core/app_events';
 import Drop from 'tether-drop';
 import colors from 'app/core/utils/colors';
 import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
-import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
-import { configureStore } from 'app/store/configureStore';
+import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
+import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
+import { configureStore } from 'app/store/configureStore';
 
 export class GrafanaCtrl {
   /** @ngInject */
@@ -23,12 +24,15 @@ export class GrafanaCtrl {
     contextSrv,
     bridgeSrv,
     backendSrv: BackendSrv,
+    timeSrv: TimeSrv,
     datasourceSrv: DatasourceSrv,
     angularLoader: AngularLoader
   ) {
-    // sets singleston instances for angular services so react components can access them
+    // make angular loader service available to react components
     setAngularLoader(angularLoader);
     setBackendSrv(backendSrv);
+    setDatasourceSrv(datasourceSrv);
+    setTimeSrv(timeSrv);
     configureStore();
 
     $scope.init = () => {

+ 24 - 0
public/app/types/index.ts

@@ -8,6 +8,19 @@ import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { Invitee, OrgUser, User, UsersState } from './user';
 import { DataSource, DataSourcesState } from './datasources';
+import {
+  TimeRange,
+  LoadingState,
+  TimeSeries,
+  TimeSeriesVM,
+  TimeSeriesVMs,
+  TimeSeriesStats,
+  NullValueMode,
+  DataQuery,
+  DataQueryResponse,
+  DataQueryOptions,
+} from './series';
+import { PanelProps } from './panel';
 import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
 
 export {
@@ -45,6 +58,17 @@ export {
   OrgUser,
   User,
   UsersState,
+  TimeRange,
+  LoadingState,
+  PanelProps,
+  TimeSeries,
+  TimeSeriesVM,
+  TimeSeriesVMs,
+  NullValueMode,
+  TimeSeriesStats,
+  DataQuery,
+  DataQueryResponse,
+  DataQueryOptions,
   PluginDashboard,
 };
 

+ 1 - 0
public/app/types/location.ts

@@ -2,6 +2,7 @@ export interface LocationUpdate {
   path?: string;
   query?: UrlQueryMap;
   routeParams?: UrlQueryMap;
+  partial?: boolean;
 }
 
 export interface LocationState {

+ 7 - 0
public/app/types/panel.ts

@@ -0,0 +1,7 @@
+import { LoadingState, TimeSeries, TimeRange } from './series';
+
+export interface PanelProps {
+  timeSeries: TimeSeries[];
+  timeRange: TimeRange;
+  loading: LoadingState;
+}

+ 22 - 0
public/app/types/plugins.ts

@@ -1,3 +1,25 @@
+export interface PluginExports {
+  PanelCtrl?;
+  PanelComponent?: any;
+  Datasource?: any;
+  QueryCtrl?: any;
+  ConfigCtrl?: any;
+  AnnotationsQueryCtrl?: any;
+  PanelOptions?: any;
+}
+
+export interface PanelPlugin {
+  id: string;
+  name: string;
+  meta: any;
+  hideFromList: boolean;
+  module: string;
+  baseUrl: string;
+  info: any;
+  sort: number;
+  exports?: PluginExports;
+}
+
 export interface PluginMeta {
   id: string;
   name: string;

+ 91 - 0
public/app/types/series.ts

@@ -0,0 +1,91 @@
+import { Moment } from 'moment';
+
+export enum LoadingState {
+  NotStarted = 'NotStarted',
+  Loading = 'Loading',
+  Done = 'Done',
+  Error = 'Error',
+}
+
+export interface RawTimeRange {
+  from: Moment | string;
+  to: Moment | string;
+}
+
+export interface TimeRange {
+  from: Moment;
+  to: Moment;
+  raw: RawTimeRange;
+}
+
+export type TimeSeriesValue = string | number | null;
+
+export type TimeSeriesPoints = TimeSeriesValue[][];
+
+export interface TimeSeries {
+  target: string;
+  datapoints: TimeSeriesPoints;
+  unit?: string;
+}
+
+/** View model projection of a time series */
+export interface TimeSeriesVM {
+  label: string;
+  color: string;
+  data: TimeSeriesValue[][];
+  stats: TimeSeriesStats;
+}
+
+export interface TimeSeriesStats {
+  total: number;
+  max: number;
+  min: number;
+  logmin: number;
+  avg: number | null;
+  current: number | null;
+  first: number | null;
+  delta: number;
+  diff: number | null;
+  range: number | null;
+  timeStep: number;
+  count: number;
+  allIsNull: boolean;
+  allIsZero: boolean;
+}
+
+export enum NullValueMode {
+  Null = 'null',
+  Ignore = 'connected',
+  AsZero = 'null as zero',
+}
+
+/** View model projection of many time series */
+export interface TimeSeriesVMs {
+  [index: number]: TimeSeriesVM;
+}
+
+export interface DataQueryResponse {
+  data: TimeSeries[];
+}
+
+export interface DataQuery {
+  refId: string;
+}
+
+export interface DataQueryOptions {
+  timezone: string;
+  range: TimeRange;
+  rangeRaw: RawTimeRange;
+  targets: DataQuery[];
+  panelId: number;
+  dashboardId: number;
+  cacheTimeout?: string;
+  interval: string;
+  intervalMs: number;
+  maxDataPoints: number;
+  scopedVars: object;
+}
+
+export interface DataSourceApi {
+  query(options: DataQueryOptions): Promise<DataQueryResponse>;
+}

+ 124 - 0
public/app/viz/Graph.tsx

@@ -0,0 +1,124 @@
+// Libraries
+import $ from 'jquery';
+import React, { PureComponent } from 'react';
+import { withSize } from 'react-sizeme';
+import 'vendor/flot/jquery.flot';
+import 'vendor/flot/jquery.flot.time';
+
+// Types
+import { TimeRange, TimeSeriesVMs } from 'app/types';
+
+// Copied from graph.ts
+function time_format(ticks, min, max) {
+  if (min && max && ticks) {
+    const range = max - min;
+    const secPerTick = range / ticks / 1000;
+    const oneDay = 86400000;
+    const oneYear = 31536000000;
+
+    if (secPerTick <= 45) {
+      return '%H:%M:%S';
+    }
+    if (secPerTick <= 7200 || range <= oneDay) {
+      return '%H:%M';
+    }
+    if (secPerTick <= 80000) {
+      return '%m/%d %H:%M';
+    }
+    if (secPerTick <= 2419200 || range <= oneYear) {
+      return '%m/%d';
+    }
+    return '%Y-%m';
+  }
+
+  return '%H:%M';
+}
+
+const FLOT_OPTIONS = {
+  legend: {
+    show: false,
+  },
+  series: {
+    lines: {
+      linewidth: 1,
+      zero: false,
+    },
+    shadowSize: 0,
+  },
+  grid: {
+    minBorderMargin: 0,
+    markings: [],
+    backgroundColor: null,
+    borderWidth: 0,
+    // hoverable: true,
+    clickable: true,
+    color: '#a1a1a1',
+    margin: { left: 0, right: 0 },
+    labelMarginX: 0,
+  },
+};
+
+interface GraphProps {
+  timeSeries: TimeSeriesVMs;
+  timeRange: TimeRange;
+  size?: { width: number; height: number };
+}
+
+export class Graph extends PureComponent<GraphProps> {
+  element: any;
+
+  componentDidUpdate(prevProps: GraphProps) {
+    if (
+      prevProps.timeSeries !== this.props.timeSeries ||
+      prevProps.timeRange !== this.props.timeRange ||
+      prevProps.size !== this.props.size
+    ) {
+      this.draw();
+    }
+  }
+
+  componentDidMount() {
+    this.draw();
+  }
+
+  draw() {
+    const { size, timeSeries, timeRange } = this.props;
+
+    if (!size) {
+      return;
+    }
+
+    const ticks = (size.width || 0) / 100;
+    const min = timeRange.from.valueOf();
+    const max = timeRange.to.valueOf();
+
+    const dynamicOptions = {
+      xaxis: {
+        mode: 'time',
+        min: min,
+        max: max,
+        label: 'Datetime',
+        ticks: ticks,
+        timeformat: time_format(ticks, min, max),
+      },
+    };
+
+    const options = {
+      ...FLOT_OPTIONS,
+      ...dynamicOptions,
+    };
+
+    console.log('plot', timeSeries, options);
+    $.plot(this.element, timeSeries, options);
+  }
+
+  render() {
+    return (
+      <div className="graph-panel">
+        <div className="graph-panel__chart" ref={e => (this.element = e)} />
+      </div>
+    );
+  }
+}
+
+export default withSize()(Graph);

+ 168 - 0
public/app/viz/state/timeSeries.ts

@@ -0,0 +1,168 @@
+// Libraries
+import _ from 'lodash';
+
+// Utils
+import colors from 'app/core/utils/colors';
+
+// Types
+import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types';
+
+interface Options {
+  timeSeries: TimeSeries[];
+  nullValueMode: NullValueMode;
+}
+
+export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
+  const vmSeries = timeSeries.map((item, index) => {
+    const colorIndex = index % colors.length;
+    const label = item.target;
+    const result = [];
+
+    // stat defaults
+    let total = 0;
+    let max = -Number.MAX_VALUE;
+    let min = Number.MAX_VALUE;
+    let logmin = Number.MAX_VALUE;
+    let avg = null;
+    let current = null;
+    let first = null;
+    let delta = 0;
+    let diff = null;
+    let range = null;
+    let timeStep = Number.MAX_VALUE;
+    let allIsNull = true;
+    let allIsZero = true;
+
+    const ignoreNulls = nullValueMode === NullValueMode.Ignore;
+    const nullAsZero = nullValueMode === NullValueMode.AsZero;
+
+    let currentTime;
+    let currentValue;
+    let nonNulls = 0;
+    let previousTime;
+    let previousValue = 0;
+    let previousDeltaUp = true;
+
+    for (let i = 0; i < item.datapoints.length; i++) {
+      currentValue = item.datapoints[i][0];
+      currentTime = item.datapoints[i][1];
+
+      // Due to missing values we could have different timeStep all along the series
+      // so we have to find the minimum one (could occur with aggregators such as ZimSum)
+      if (previousTime !== undefined) {
+        const currentStep = currentTime - previousTime;
+        if (currentStep < timeStep) {
+          timeStep = currentStep;
+        }
+      }
+
+      previousTime = currentTime;
+
+      if (currentValue === null) {
+        if (ignoreNulls) {
+          continue;
+        }
+        if (nullAsZero) {
+          currentValue = 0;
+        }
+      }
+
+      if (currentValue !== null) {
+        if (_.isNumber(currentValue)) {
+          total += currentValue;
+          allIsNull = false;
+          nonNulls++;
+        }
+
+        if (currentValue > max) {
+          max = currentValue;
+        }
+
+        if (currentValue < min) {
+          min = currentValue;
+        }
+
+        if (first === null) {
+          first = currentValue;
+        } else {
+          if (previousValue > currentValue) {
+            // counter reset
+            previousDeltaUp = false;
+            if (i === item.datapoints.length - 1) {
+              // reset on last
+              delta += currentValue;
+            }
+          } else {
+            if (previousDeltaUp) {
+              delta += currentValue - previousValue; // normal increment
+            } else {
+              delta += currentValue; // account for counter reset
+            }
+            previousDeltaUp = true;
+          }
+        }
+        previousValue = currentValue;
+
+        if (currentValue < logmin && currentValue > 0) {
+          logmin = currentValue;
+        }
+
+        if (currentValue !== 0) {
+          allIsZero = false;
+        }
+      }
+
+      result.push([currentTime, currentValue]);
+    }
+
+    if (max === -Number.MAX_VALUE) {
+      max = null;
+    }
+
+    if (min === Number.MAX_VALUE) {
+      min = null;
+    }
+
+    if (result.length && !allIsNull) {
+      avg = total / nonNulls;
+      current = result[result.length - 1][1];
+      if (current === null && result.length > 1) {
+        current = result[result.length - 2][1];
+      }
+    }
+
+    if (max !== null && min !== null) {
+      range = max - min;
+    }
+
+    if (current !== null && first !== null) {
+      diff = current - first;
+    }
+
+    const count = result.length;
+
+    return {
+      data: result,
+      label: label,
+      color: colors[colorIndex],
+      stats: {
+        total,
+        min,
+        max,
+        current,
+        logmin,
+        avg,
+        diff,
+        delta,
+        timeStep,
+        range,
+        count,
+        first,
+        allIsZero,
+        allIsNull,
+      },
+    };
+  });
+
+  return vmSeries;
+}

+ 1 - 0
public/sass/_grafana.scss

@@ -97,6 +97,7 @@
 @import 'components/form_select_box';
 @import 'components/user-picker';
 @import 'components/description-picker';
+@import 'components/viz_editor';
 @import 'components/delete_button';
 @import 'components/add_data_source.scss';
 @import 'components/page_loader';

+ 5 - 1
public/sass/components/_dashboard_grid.scss

@@ -20,7 +20,6 @@
   }
 
   // Disable grid interaction indicators in fullscreen panels
-
   .panel-header:hover {
     background-color: inherit;
   }
@@ -32,6 +31,11 @@
   .react-resizable-handle {
     display: none;
   }
+
+  // the react-grid has a height transition
+  .react-grid-layout {
+    transition-property: none;
+  }
 }
 
 @include media-breakpoint-down(sm) {

+ 0 - 4
public/sass/components/_panel_add_panel.scss

@@ -85,10 +85,6 @@
   height: calc(100% - 15px);
 }
 
-.add-panel__item-icon {
-  padding: 2px;
-}
-
 .add-panel__searchbar {
   width: 100%;
   margin-bottom: 10px;

+ 5 - 0
public/sass/components/_scrollbar.scss

@@ -307,6 +307,7 @@
   .view {
     display: flex;
     flex-grow: 1;
+    flex-direction: column;
   }
 
   .track-vertical {
@@ -337,3 +338,7 @@
     border-radius: 6px;
   }
 }
+
+.scroll-margin-helper {
+  margin-right: 12px;
+}

+ 10 - 11
public/sass/components/_tabbed_view.scss

@@ -1,19 +1,15 @@
 .tabbed-view {
-  padding: $spacer*3;
-  margin-bottom: $dashboard-padding;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
 
-  &.tabbed-view--panel-edit {
-    padding: 0;
-
-    .tabbed-view-header {
-      padding: 0px 25px;
-      background: none;
-    }
+  &.tabbed-view--new {
+    padding: 25px 0 0 0;
+    height: 100%;
   }
 }
 
 .tabbed-view-header {
-  background: $page-header-bg;
   box-shadow: $page-header-shadow;
   border-bottom: 1px solid $page-header-border-color;
   @include clearfix();
@@ -48,7 +44,10 @@
 }
 
 .tabbed-view-body {
-  padding: $spacer*2 $spacer;
+  padding: $spacer*2 $spacer $spacer $spacer;
+  display: flex;
+  flex-direction: column;
+  flex: 1;
 
   &--small {
     min-height: 0px;

+ 81 - 0
public/sass/components/_viz_editor.scss

@@ -0,0 +1,81 @@
+.viz-editor {
+  display: flex;
+  height: 100%;
+}
+
+.viz-editor-col1 {
+  width: 210px;
+  height: 100%;
+  margin-right: 40px;
+}
+
+.viz-editor-col2 {
+  flex-grow: 1;
+}
+
+.viz-picker {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.viz-picker__search {
+  flex-grow: 0;
+}
+
+.viz-picker__items {
+  flex-grow: 1;
+  height: calc(100% - 50px);
+}
+
+.viz-picker__item {
+  background: $card-background;
+  box-shadow: $card-shadow;
+
+  border-radius: 3px;
+  padding: $spacer;
+  width: 100%;
+  height: 60px;
+  text-align: center;
+  margin-bottom: 6px;
+  cursor: pointer;
+  display: flex;
+  flex-shrink: 0;
+  border: 1px solid transparent;
+  @include left-brand-border;
+
+  &:hover {
+    background: $card-background-hover;
+  }
+
+  &--selected {
+    // border: 1px solid $orange;
+    @include left-brand-border-gradient();
+
+    .viz-picker__item-name {
+      color: $text-color;
+    }
+
+    .viz-picker__item-img {
+      filter: saturate(100%);
+    }
+  }
+}
+
+.viz-picker__item-name {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  font-size: $font-size-h5;
+  display: flex;
+  flex-direction: column;
+  align-self: center;
+  padding-left: $spacer;
+  font-size: $font-size-md;
+  color: $text-muted;
+}
+
+.viz-picker__item-img {
+  height: 100%;
+  filter: saturate(30%);
+}

+ 34 - 10
public/sass/pages/_dashboard.scss

@@ -1,7 +1,12 @@
 .dashboard-container {
   padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding;
   width: 100%;
-  min-height: 100%;
+  height: 100%;
+  box-sizing: border-box;
+
+  &--has-submenu {
+    height: calc(100% - 50px);
+  }
 }
 
 .template-variable {
@@ -29,16 +34,43 @@ div.flot-text {
   height: 100%;
 }
 
+.panel-editor-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.panel-editor-container__panel {
+  height: 35%;
+}
+
+.panel-editor-container__editor {
+  height: 65%;
+}
+
 .panel-container {
   background-color: $panel-bg;
   border: $panel-border;
   position: relative;
   border-radius: 3px;
+  height: 100%;
 
   &.panel-transparent {
     background-color: transparent;
     border: none;
   }
+
+  &:hover {
+    .panel-menu-toggle {
+      visibility: visible;
+      transition: opacity 0.1s ease-in 0.2s;
+      opacity: 1;
+    }
+  }
+
+  &--is-editing {
+    height: auto;
+  }
 }
 
 .panel-content {
@@ -199,14 +231,6 @@ div.flot-text {
   }
 }
 
-.panel-hover-highlight {
-  .panel-menu-toggle {
-    visibility: visible;
-    transition: opacity 0.1s ease-in 0.2s;
-    opacity: 1;
-  }
-}
-
 .panel-time-info {
   font-weight: bold;
   float: right;
@@ -233,5 +257,5 @@ div.flot-text {
 }
 
 .panel-full-edit {
-  margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding);
+  padding-top: $dashboard-padding;
 }