Explorar o código

Merge branch 'develop' into 12759-timeshift

Torkel Ödegaard %!s(int64=7) %!d(string=hai) anos
pai
achega
81a9a3a3c1
Modificáronse 80 ficheiros con 2313 adicións e 885 borrados
  1. 1 0
      .gitignore
  2. 37 0
      public/app/core/components/Animations/FadeIn.tsx
  3. 1 1
      public/app/core/components/Animations/SlideDown.tsx
  4. 6 3
      public/app/core/directives/dash_class.ts
  5. 1 0
      public/app/core/reducers/location.ts
  6. 4 0
      public/app/core/services/AngularLoader.ts
  7. 1 1
      public/app/core/services/bridge_srv.ts
  8. 1 1
      public/app/core/utils/connectWithReduxStore.tsx
  9. 7 1
      public/app/features/dashboard/dashgrid/DashboardGrid.tsx
  10. 34 34
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  11. 88 0
      public/app/features/dashboard/dashgrid/DataSourcePicker.tsx
  12. 96 0
      public/app/features/dashboard/dashgrid/EditorTabBody.tsx
  13. 31 49
      public/app/features/dashboard/dashgrid/PanelEditor.tsx
  14. 64 0
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  15. 24 4
      public/app/features/dashboard/dashgrid/QueriesTab.tsx
  16. 57 0
      public/app/features/dashboard/dashgrid/VisualizationTab.tsx
  17. 42 19
      public/app/features/dashboard/dashgrid/VizTypePicker.tsx
  18. 10 76
      public/app/features/dashboard/specs/AddPanelPanel.test.tsx
  19. 1 1
      public/app/features/dashboard/utils/getPanelMenu.ts
  20. 0 125
      public/app/features/datasources/DataSourceSettings.tsx
  21. 3 0
      public/app/features/datasources/__mocks__/dataSourcesMocks.ts
  22. 20 0
      public/app/features/datasources/settings/BasicSettings.test.tsx
  23. 34 0
      public/app/features/datasources/settings/BasicSettings.tsx
  24. 31 0
      public/app/features/datasources/settings/ButtonRow.test.tsx
  25. 25 0
      public/app/features/datasources/settings/ButtonRow.tsx
  26. 63 0
      public/app/features/datasources/settings/DataSourceSettings.test.tsx
  27. 245 0
      public/app/features/datasources/settings/DataSourceSettings.tsx
  28. 63 0
      public/app/features/datasources/settings/PluginSettings.tsx
  29. 25 0
      public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap
  30. 59 0
      public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap
  31. 395 0
      public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap
  32. 54 13
      public/app/features/datasources/state/actions.ts
  33. 4 1
      public/app/features/datasources/state/navModel.ts
  34. 4 1
      public/app/features/datasources/state/reducers.ts
  35. 9 1
      public/app/features/datasources/state/selectors.ts
  36. 2 2
      public/app/features/panel/panel_directive.ts
  37. 30 3
      public/app/features/plugins/__mocks__/pluginMocks.ts
  38. 36 6
      public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap
  39. 0 1
      public/app/features/plugins/all.ts
  40. 2 0
      public/app/features/plugins/built_in_plugins.ts
  41. 3 6
      public/app/features/plugins/datasource_srv.ts
  42. 1 1
      public/app/features/plugins/ds_dashboards_ctrl.ts
  43. 1 1
      public/app/features/plugins/ds_edit_ctrl.ts
  44. 0 72
      public/app/features/plugins/partials/ds_edit.html
  45. 9 3
      public/app/features/plugins/plugin_component.ts
  46. 0 179
      public/app/features/plugins/plugin_edit_ctrl.ts
  47. 3 8
      public/app/plugins/datasource/cloudwatch/datasource.ts
  48. 23 0
      public/app/plugins/panel/gauge/module.tsx
  49. 18 0
      public/app/plugins/panel/gauge/plugin.json
  50. 20 5
      public/app/plugins/panel/graph2/module.tsx
  51. 0 5
      public/app/plugins/panel/unknown/module.html
  52. 0 10
      public/app/plugins/panel/unknown/module.ts
  53. 1 1
      public/app/routes/ReactContainer.tsx
  54. 6 4
      public/app/routes/routes.ts
  55. 3 4
      public/app/store/configureStore.ts
  56. 5 0
      public/app/store/store.ts
  57. 10 0
      public/app/types/datasources.ts
  58. 4 2
      public/app/types/index.ts
  59. 5 5
      public/app/types/plugins.ts
  60. 1 0
      public/app/types/series.ts
  61. 133 0
      public/app/viz/Gauge.tsx
  62. 16 0
      public/app/viz/GaugeOptions.tsx
  63. 2 1
      public/sass/_grafana.scss
  64. 8 2
      public/sass/_variables.dark.scss
  65. 6 0
      public/sass/_variables.light.scss
  66. 1 1
      public/sass/components/_buttons.scss
  67. 5 1
      public/sass/components/_dashboard_grid.scss
  68. 21 1
      public/sass/components/_gf-form.scss
  69. 2 6
      public/sass/components/_navbar.scss
  70. 208 0
      public/sass/components/_panel_editor.scss
  71. 71 71
      public/sass/components/_scrollbar.scss
  72. 1 2
      public/sass/components/_submenu.scss
  73. 4 2
      public/sass/components/_tabbed_view.scss
  74. 1 0
      public/sass/components/_tabs.scss
  75. 1 0
      public/sass/components/_timepicker.scss
  76. 59 0
      public/sass/components/_toolbar.scss
  77. 0 81
      public/sass/components/_viz_editor.scss
  78. 0 14
      public/sass/pages/_dashboard.scss
  79. 19 18
      public/vendor/flot/jquery.flot.gauge.js
  80. 32 36
      scripts/webpack/webpack.hot.js

+ 1 - 0
.gitignore

@@ -76,3 +76,4 @@ debug.test
 /devenv/bulk_alerting_dashboards/*.json
 /devenv/bulk_alerting_dashboards/*.json
 
 
 /scripts/build/release_publisher/release_publisher
 /scripts/build/release_publisher/release_publisher
+*.patch

+ 37 - 0
public/app/core/components/Animations/FadeIn.tsx

@@ -0,0 +1,37 @@
+import React, { SFC } from 'react';
+import Transition from 'react-transition-group/Transition';
+
+interface Props {
+  duration: number;
+  children: JSX.Element;
+  in: boolean;
+}
+
+export const FadeIn: SFC<Props> = props => {
+  const defaultStyle = {
+    transition: `opacity ${props.duration}ms linear`,
+    opacity: 0,
+  };
+
+  const transitionStyles = {
+    exited: { opacity: 0, display: 'none' },
+    entering: { opacity: 0 },
+    entered: { opacity: 1 },
+    exiting: { opacity: 0 },
+  };
+
+  return (
+    <Transition in={props.in} timeout={props.duration}>
+      {state => (
+        <div
+          style={{
+            ...defaultStyle,
+            ...transitionStyles[state],
+          }}
+        >
+          {props.children}
+        </div>
+      )}
+    </Transition>
+  );
+};

+ 1 - 1
public/app/core/components/Animations/SlideDown.tsx

@@ -23,7 +23,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
   const transitionStyles = {
   const transitionStyles = {
     exited: { maxHeight: 0 },
     exited: { maxHeight: 0 },
     entering: { maxHeight: maxHeight },
     entering: { maxHeight: maxHeight },
-    entered: { maxHeight: maxHeight, overflow: 'visible' },
+    entered: { maxHeight: 'unset', overflow: 'visible' },
     exiting: { maxHeight: 0 },
     exiting: { maxHeight: 0 },
   };
   };
 
 

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

@@ -1,3 +1,4 @@
+import $ from 'jquery';
 import _ from 'lodash';
 import _ from 'lodash';
 import coreModule from '../core_module';
 import coreModule from '../core_module';
 
 
@@ -5,18 +6,20 @@ import coreModule from '../core_module';
 function dashClass($timeout) {
 function dashClass($timeout) {
   return {
   return {
     link: ($scope, elem) => {
     link: ($scope, elem) => {
+      const body = $('body');
+
       $scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
       $scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
         console.log('view-mode-changed', panel.fullscreen);
         console.log('view-mode-changed', panel.fullscreen);
         if (panel.fullscreen) {
         if (panel.fullscreen) {
-          elem.addClass('panel-in-fullscreen');
+          body.addClass('panel-in-fullscreen');
         } else {
         } else {
           $timeout(() => {
           $timeout(() => {
-            elem.removeClass('panel-in-fullscreen');
+            body.removeClass('panel-in-fullscreen');
           });
           });
         }
         }
       });
       });
 
 
-      elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
+      body.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
 
 
       $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
       $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
         if (newValue) {
         if (newValue) {

+ 1 - 0
public/app/core/reducers/location.ts

@@ -18,6 +18,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
 
 
       if (action.payload.partial) {
       if (action.payload.partial) {
         query = _.defaults(query, state.query);
         query = _.defaults(query, state.query);
+        query = _.omitBy(query, _.isNull);
       }
       }
 
 
       return {
       return {

+ 4 - 0
public/app/core/services/AngularLoader.ts

@@ -4,6 +4,7 @@ import _ from 'lodash';
 
 
 export interface AngularComponent {
 export interface AngularComponent {
   destroy();
   destroy();
+  digest();
 }
 }
 
 
 export class AngularLoader {
 export class AngularLoader {
@@ -24,6 +25,9 @@ export class AngularLoader {
         scope.$destroy();
         scope.$destroy();
         compiledElem.remove();
         compiledElem.remove();
       },
       },
+      digest: () => {
+        scope.$digest();
+      },
     };
     };
   }
   }
 }
 }

+ 1 - 1
public/app/core/services/bridge_srv.ts

@@ -1,6 +1,6 @@
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import locationUtil from 'app/core/utils/location_util';
 import locationUtil from 'app/core/utils/location_util';
 import { updateLocation } from 'app/core/actions';
 import { updateLocation } from 'app/core/actions';
 
 

+ 1 - 1
public/app/core/utils/connectWithReduxStore.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import React from 'react';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { store } from '../../store/configureStore';
+import { store } from '../../store/store';
 
 
 export function connectWithStore(WrappedComponent, ...args) {
 export function connectWithStore(WrappedComponent, ...args) {
   const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
   const ConnectedWrappedComponent = connect(...args)(WrappedComponent);

+ 7 - 1
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -8,6 +8,7 @@ import classNames from 'classnames';
 import sizeMe from 'react-sizeme';
 import sizeMe from 'react-sizeme';
 
 
 let lastGridWidth = 1200;
 let lastGridWidth = 1200;
+let ignoreNextWidthChange = false;
 
 
 function GridWrapper({
 function GridWrapper({
   size,
   size,
@@ -24,8 +25,12 @@ function GridWrapper({
   isFullscreen,
   isFullscreen,
 }) {
 }) {
   const width = size.width > 0 ? size.width : lastGridWidth;
   const width = size.width > 0 ? size.width : lastGridWidth;
+
+  // logic to ignore width changes (optimization)
   if (width !== lastGridWidth) {
   if (width !== lastGridWidth) {
-    if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
+    if (ignoreNextWidthChange) {
+      ignoreNextWidthChange = false;
+    } else if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) {
       onWidthChange();
       onWidthChange();
       lastGridWidth = width;
       lastGridWidth = width;
     }
     }
@@ -138,6 +143,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
   }
   }
 
 
   onViewModeChanged(payload) {
   onViewModeChanged(payload) {
+    ignoreNextWidthChange = true;
     this.setState({ animated: !payload.fullscreen });
     this.setState({ animated: !payload.fullscreen });
   }
   }
 
 

+ 34 - 34
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -1,15 +1,19 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import config from 'app/core/config';
 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 { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
-import { DashboardRow } from './DashboardRow';
-import { AddPanelPanel } from './AddPanelPanel';
 import { importPluginModule } from 'app/features/plugins/plugin_loader';
 import { importPluginModule } from 'app/features/plugins/plugin_loader';
-import { PluginExports, PanelPlugin } from 'app/types/plugins';
+
+import { AddPanelPanel } from './AddPanelPanel';
+import { getPanelPluginNotFound } from './PanelPluginNotFound';
+import { DashboardRow } from './DashboardRow';
 import { PanelChrome } from './PanelChrome';
 import { PanelChrome } from './PanelChrome';
 import { PanelEditor } from './PanelEditor';
 import { PanelEditor } from './PanelEditor';
 
 
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { PanelPlugin } from 'app/types';
+
 export interface Props {
 export interface Props {
   panel: PanelModel;
   panel: PanelModel;
   dashboard: DashboardModel;
   dashboard: DashboardModel;
@@ -18,20 +22,19 @@ export interface Props {
 }
 }
 
 
 export interface State {
 export interface State {
-  pluginExports: PluginExports;
+  plugin: PanelPlugin;
 }
 }
 
 
 export class DashboardPanel extends PureComponent<Props, State> {
 export class DashboardPanel extends PureComponent<Props, State> {
   element: any;
   element: any;
   angularPanel: AngularComponent;
   angularPanel: AngularComponent;
-  pluginInfo: any;
   specialPanels = {};
   specialPanels = {};
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
-      pluginExports: null,
+      plugin: null,
     };
     };
 
 
     this.specialPanels['row'] = this.renderRow.bind(this);
     this.specialPanels['row'] = this.renderRow.bind(this);
@@ -64,20 +67,22 @@ export class DashboardPanel extends PureComponent<Props, State> {
       return;
       return;
     }
     }
 
 
+    const { panel } = this.props;
+
     // handle plugin loading & changing of plugin type
     // 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.state.plugin || this.state.plugin.id !== panel.type) {
+      const plugin = config.panels[panel.type] || getPanelPluginNotFound(panel.type);
 
 
-      if (this.pluginInfo.exports) {
+      if (plugin.exports) {
         this.cleanUpAngularPanel();
         this.cleanUpAngularPanel();
-        this.setState({ pluginExports: this.pluginInfo.exports });
+        this.setState({ plugin: plugin });
       } else {
       } else {
-        importPluginModule(this.pluginInfo.module).then(pluginExports => {
+        importPluginModule(plugin.module).then(pluginExports => {
           this.cleanUpAngularPanel();
           this.cleanUpAngularPanel();
           // cache plugin exports (saves a promise async cycle next time)
           // cache plugin exports (saves a promise async cycle next time)
-          this.pluginInfo.exports = pluginExports;
+          plugin.exports = pluginExports;
           // update panel state
           // update panel state
-          this.setState({ pluginExports: pluginExports });
+          this.setState({ plugin: plugin });
         });
         });
       }
       }
     }
     }
@@ -113,7 +118,9 @@ export class DashboardPanel extends PureComponent<Props, State> {
   }
   }
 
 
   renderReactPanel() {
   renderReactPanel() {
-    const { pluginExports } = this.state;
+    const { dashboard, panel } = this.props;
+    const { plugin } = this.state;
+
     const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper';
     const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper';
     const panelWrapperClass = this.props.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
     const panelWrapperClass = this.props.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
     // this might look strange with these classes that change when edit, but
     // this might look strange with these classes that change when edit, but
@@ -121,37 +128,30 @@ export class DashboardPanel extends PureComponent<Props, State> {
     return (
     return (
       <div className={containerClass}>
       <div className={containerClass}>
         <div className={panelWrapperClass}>
         <div className={panelWrapperClass}>
-          <PanelChrome
-            component={pluginExports.PanelComponent}
-            panel={this.props.panel}
-            dashboard={this.props.dashboard}
-          />
+          <PanelChrome component={plugin.exports.PanelComponent} panel={panel} dashboard={dashboard} />
         </div>
         </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>
+        {panel.isEditing && (
+          <PanelEditor panel={panel} plugin={plugin} dashboard={dashboard} onTypeChanged={this.onPluginTypeChanged} />
         )}
         )}
       </div>
       </div>
     );
     );
   }
   }
 
 
   render() {
   render() {
+    const { panel } = this.props;
+    const { plugin } = this.state;
+
     if (this.isSpecial()) {
     if (this.isSpecial()) {
-      return this.specialPanels[this.props.panel.type]();
+      return this.specialPanels[panel.type]();
     }
     }
 
 
-    if (!this.state.pluginExports) {
+    // if we have not loaded plugin exports yet, wait
+    if (!plugin || !plugin.exports) {
       return null;
       return null;
     }
     }
 
 
-    if (this.state.pluginExports.PanelComponent) {
+    // if exporting PanelComponent it must be a react panel
+    if (plugin.exports.PanelComponent) {
       return this.renderReactPanel();
       return this.renderReactPanel();
     }
     }
 
 

+ 88 - 0
public/app/features/dashboard/dashgrid/DataSourcePicker.tsx

@@ -0,0 +1,88 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import _ from 'lodash';
+
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { DataSourceSelectItem } from 'app/types';
+
+interface Props {}
+
+interface State {
+  datasources: DataSourceSelectItem[];
+  searchQuery: string;
+}
+
+export class DataSourcePicker extends PureComponent<Props, State> {
+  searchInput: HTMLElement;
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      datasources: getDatasourceSrv().getMetricSources(),
+      searchQuery: '',
+    };
+  }
+
+  getDataSources() {
+    const { datasources, searchQuery } = this.state;
+    const regex = new RegExp(searchQuery, 'i');
+
+    const filtered = datasources.filter(item => {
+      return regex.test(item.name) || regex.test(item.meta.name);
+    });
+
+    return _.sortBy(filtered, 'sort');
+  }
+
+  renderDataSource = (ds: DataSourceSelectItem, index) => {
+    const cssClass = classNames({
+      'ds-picker-list__item': true,
+    });
+
+    return (
+      <div key={index} className={cssClass} title={ds.name}>
+        <img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
+        <div className="ds-picker-list__name">{ds.name}</div>
+      </div>
+    );
+  };
+
+  componentDidMount() {
+    setTimeout(() => {
+      this.searchInput.focus();
+    }, 300);
+  }
+
+  renderFilters() {
+    return (
+      <>
+        <label className="gf-form--has-input-icon">
+          <input
+            type="text"
+            className="gf-form-input width-13"
+            placeholder=""
+            ref={elem => (this.searchInput = elem)}
+          />
+          <i className="gf-form-input-icon fa fa-search" />
+        </label>
+        <div className="p-l-1">
+          <button className="btn toggle-btn gf-form-btn active">All</button>
+          <button className="btn toggle-btn gf-form-btn">Favorites</button>
+        </div>
+      </>
+    );
+  }
+
+  render() {
+    return (
+      <>
+        <div className="cta-form__bar">
+          {this.renderFilters()}
+          <div className="gf-form--grow" />
+        </div>
+        <div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
+      </>
+    );
+  }
+}

+ 96 - 0
public/app/features/dashboard/dashgrid/EditorTabBody.tsx

@@ -0,0 +1,96 @@
+import React, { PureComponent } from 'react';
+import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
+import { FadeIn } from 'app/core/components/Animations/FadeIn';
+
+interface Props {
+  children: JSX.Element;
+  main: EditorToolBarView;
+  toolbarItems: EditorToolBarView[];
+}
+
+export interface EditorToolBarView {
+  title: string;
+  imgSrc?: string;
+  icon?: string;
+  render: () => JSX.Element;
+}
+
+interface State {
+  openView?: EditorToolBarView;
+}
+
+export class EditorTabBody extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      openView: null,
+    };
+  }
+
+  onToggleToolBarView = (item: EditorToolBarView) => {
+    this.setState({
+      openView: item === this.state.openView ? null : item,
+    });
+  };
+
+  onCloseOpenView = () => {
+    this.setState({ openView: null });
+  };
+
+  renderMainSelection(view: EditorToolBarView) {
+    return (
+      <div className="toolbar__main" onClick={() => this.onToggleToolBarView(view)} key={view.title}>
+        <img className="toolbar__main-image" src={view.imgSrc} />
+        <div className="toolbar__main-name">{view.title}</div>
+        <i className="fa fa-caret-down" />
+      </div>
+    );
+  }
+
+  renderButton(view: EditorToolBarView) {
+    return (
+      <div className="nav-buttons" key={view.title}>
+        <button className="btn navbar-button" onClick={() => this.onToggleToolBarView(view)}>
+          {view.icon && <i className={view.icon} />} {view.title}
+        </button>
+      </div>
+    );
+  }
+
+  renderOpenView(view: EditorToolBarView) {
+    return (
+      <div className="toolbar-subview">
+        <button className="toolbar-subview__close" onClick={this.onCloseOpenView}>
+          <i className="fa fa-chevron-up" />
+        </button>
+        {view.render()}
+      </div>
+    );
+  }
+
+  render() {
+    const { children, toolbarItems, main } = this.props;
+    const { openView } = this.state;
+
+    return (
+      <>
+        <div className="toolbar">
+          {this.renderMainSelection(main)}
+          <div className="gf-form--grow" />
+          {toolbarItems.map(item => this.renderButton(item))}
+        </div>
+        <div className="panel-editor__scroll">
+          <CustomScrollbar autoHide={false}>
+            <div className="panel-editor__content">
+              <FadeIn in={openView !== null} duration={200}>
+                {openView && this.renderOpenView(openView)}
+              </FadeIn>
+              {children}
+            </div>
+          </CustomScrollbar>
+        </div>
+      </>
+    );
+  }
+}

+ 31 - 49
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -2,20 +2,19 @@ import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 
 
 import { QueriesTab } from './QueriesTab';
 import { QueriesTab } from './QueriesTab';
-import { VizTypePicker } from './VizTypePicker';
+import { VisualizationTab } from './VisualizationTab';
 
 
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import { updateLocation } from 'app/core/actions';
 import { updateLocation } from 'app/core/actions';
 
 
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
-import { PanelPlugin, PluginExports } from 'app/types/plugins';
+import { PanelPlugin } from 'app/types/plugins';
 
 
 interface PanelEditorProps {
 interface PanelEditorProps {
   panel: PanelModel;
   panel: PanelModel;
   dashboard: DashboardModel;
   dashboard: DashboardModel;
-  panelType: string;
-  pluginExports: PluginExports;
+  plugin: PanelPlugin;
   onTypeChanged: (newType: PanelPlugin) => void;
   onTypeChanged: (newType: PanelPlugin) => void;
 }
 }
 
 
@@ -34,43 +33,10 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
     this.tabs = [
     this.tabs = [
       { id: 'queries', text: 'Queries', icon: 'fa fa-database' },
       { id: 'queries', text: 'Queries', icon: 'fa fa-database' },
       { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
       { id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
+      { id: 'alert', text: 'Alert', icon: 'gicon gicon-alert' },
     ];
     ];
   }
   }
 
 
-  renderQueriesTab() {
-    return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
-  }
-
-  renderPanelOptions() {
-    const { pluginExports, panel } = this.props;
-
-    if (pluginExports.PanelOptionsComponent) {
-      const OptionsComponent = pluginExports.PanelOptionsComponent;
-      return <OptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
-    } else {
-      return <p>Visualization has no options</p>;
-    }
-  }
-
-  onPanelOptionsChanged = (options: any) => {
-    this.props.panel.updateOptions(options);
-    this.forceUpdate();
-  };
-
-  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) => {
   onChangeTab = (tab: PanelEditorTab) => {
     store.dispatch(
     store.dispatch(
       updateLocation({
       updateLocation({
@@ -81,28 +47,44 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
     this.forceUpdate();
     this.forceUpdate();
   };
   };
 
 
+  onClose = () => {
+    store.dispatch(
+      updateLocation({
+        query: { tab: null, fullscreen: null, edit: null },
+        partial: true,
+      })
+    );
+  };
+
   render() {
   render() {
+    const { panel, dashboard, onTypeChanged, plugin } = this.props;
     const { location } = store.getState();
     const { location } = store.getState();
     const activeTab = location.query.tab || 'queries';
     const activeTab = location.query.tab || 'queries';
 
 
     return (
     return (
-      <div className="tabbed-view tabbed-view--new">
-        <div className="tabbed-view-header">
+      <div className="panel-editor-container__editor">
+        <div className="panel-editor-resizer">
+          <div className="panel-editor-resizer__handle">
+            <div className="panel-editor-resizer__handle-dots" />
+          </div>
+        </div>
+
+        <div className="panel-editor-tabs">
           <ul className="gf-tabs">
           <ul className="gf-tabs">
             {this.tabs.map(tab => {
             {this.tabs.map(tab => {
               return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
               return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
             })}
             })}
           </ul>
           </ul>
 
 
-          <button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
-            <i className="fa fa-remove" />
+          <button className="panel-editor-tabs__close" onClick={this.onClose}>
+            <i className="fa fa-reply" />
           </button>
           </button>
         </div>
         </div>
 
 
-        <div className="tabbed-view-body">
-          {activeTab === 'queries' && this.renderQueriesTab()}
-          {activeTab === 'visualization' && this.renderVizTab()}
-        </div>
+        {activeTab === 'queries' && <QueriesTab panel={panel} dashboard={dashboard} />}
+        {activeTab === 'visualization' && (
+          <VisualizationTab panel={panel} dashboard={dashboard} plugin={plugin} onTypeChanged={onTypeChanged} />
+        )}
       </div>
       </div>
     );
     );
   }
   }
@@ -121,8 +103,8 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
   });
   });
 
 
   return (
   return (
-    <li className="gf-tabs-item" key={tab.id}>
-      <a className={tabClasses} onClick={() => onClick(tab)}>
+    <li className="gf-tabs-item" onClick={() => onClick(tab)}>
+      <a className={tabClasses}>
         <i className={tab.icon} /> {tab.text}
         <i className={tab.icon} /> {tab.text}
       </a>
       </a>
     </li>
     </li>

+ 64 - 0
public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx

@@ -0,0 +1,64 @@
+import _ from 'lodash';
+import React, { PureComponent } from 'react';
+import { PanelPlugin, PanelProps } from 'app/types';
+
+interface Props {
+  pluginId: string;
+}
+
+class PanelPluginNotFound extends PureComponent<Props> {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    const style = {
+      display: 'flex',
+      'align-items': 'center',
+      'text-align': 'center',
+      height: '100%',
+    };
+
+    return (
+      <div style={style}>
+        <div className="alert alert-error" style={{ margin: '0 auto' }}>
+          Panel plugin with id {this.props.pluginId} could not be found
+        </div>
+      </div>
+    );
+  }
+}
+
+export function getPanelPluginNotFound(id: string): PanelPlugin {
+  const NotFound = class NotFound extends PureComponent<PanelProps> {
+    render() {
+      return <PanelPluginNotFound pluginId={id} />;
+    }
+  };
+
+  return {
+    id: id,
+    name: id,
+    sort: 100,
+    module: '',
+    baseUrl: '',
+    info: {
+      author: {
+        name: '',
+      },
+      description: '',
+      links: [],
+      logos: {
+        large: '',
+        small: '',
+      },
+      screenshots: [],
+      updated: '',
+      version: '',
+    },
+
+    exports: {
+      PanelComponent: NotFound,
+    },
+  };
+}

+ 24 - 4
public/app/features/dashboard/dashgrid/QueriesTab.tsx

@@ -1,10 +1,9 @@
-// Libraries
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-// Services & utils
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
 import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { EditorTabBody } from './EditorTabBody';
+import { DataSourcePicker } from './DataSourcePicker';
 
 
-// Types
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
 
 
@@ -48,6 +47,27 @@ export class QueriesTab extends PureComponent<Props> {
   }
   }
 
 
   render() {
   render() {
-    return <div ref={element => (this.element = element)} className="panel-height-helper" />;
+    const currentDataSource = {
+      title: 'ProductionDB',
+      imgSrc: 'public/app/plugins/datasource/prometheus/img/prometheus_logo.svg',
+      render: () => <DataSourcePicker />,
+    };
+
+    const queryInspector = {
+      title: 'Query Inspector',
+      render: () => <h2>hello</h2>,
+    };
+
+    const dsHelp = {
+      title: '',
+      icon: 'fa fa-question',
+      render: () => <h2>hello</h2>,
+    };
+
+    return (
+      <EditorTabBody main={currentDataSource} toolbarItems={[queryInspector, dsHelp]}>
+        <div ref={element => (this.element = element)} style={{ width: '100%' }} />
+      </EditorTabBody>
+    );
   }
   }
 }
 }

+ 57 - 0
public/app/features/dashboard/dashgrid/VisualizationTab.tsx

@@ -0,0 +1,57 @@
+import React, { PureComponent } from 'react';
+
+import { EditorTabBody } from './EditorTabBody';
+import { VizTypePicker } from './VizTypePicker';
+
+import { PanelModel } from '../panel_model';
+import { DashboardModel } from '../dashboard_model';
+import { PanelPlugin } from 'app/types/plugins';
+
+interface Props {
+  panel: PanelModel;
+  dashboard: DashboardModel;
+  plugin: PanelPlugin;
+  onTypeChanged: (newType: PanelPlugin) => void;
+}
+
+export class VisualizationTab extends PureComponent<Props> {
+  constructor(props) {
+    super(props);
+  }
+
+  renderPanelOptions() {
+    const { plugin, panel } = this.props;
+    const { PanelOptionsComponent } = plugin.exports;
+
+    if (PanelOptionsComponent) {
+      return <PanelOptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
+    } else {
+      return <p>Visualization has no options</p>;
+    }
+  }
+
+  onPanelOptionsChanged = (options: any) => {
+    this.props.panel.updateOptions(options);
+    this.forceUpdate();
+  };
+
+  render() {
+    const { plugin } = this.props;
+
+    const panelSelection = {
+      title: plugin.name,
+      imgSrc: plugin.info.logos.small,
+      render: () => {
+        // the needs to be scoped inside this closure
+        const { plugin, onTypeChanged } = this.props;
+        return <VizTypePicker current={plugin} onTypeChanged={onTypeChanged} />;
+      },
+    };
+
+    return (
+      <EditorTabBody main={panelSelection} toolbarItems={[]}>
+        {this.renderPanelOptions()}
+      </EditorTabBody>
+    );
+  }
+}

+ 42 - 19
public/app/features/dashboard/dashgrid/VizTypePicker.tsx

@@ -1,12 +1,12 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
+import _ from 'lodash';
+
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { PanelPlugin } from 'app/types/plugins';
 import { PanelPlugin } from 'app/types/plugins';
-import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
-import _ from 'lodash';
 
 
 interface Props {
 interface Props {
-  currentType: string;
+  current: PanelPlugin;
   onTypeChanged: (newType: PanelPlugin) => void;
   onTypeChanged: (newType: PanelPlugin) => void;
 }
 }
 
 
@@ -15,6 +15,8 @@ interface State {
 }
 }
 
 
 export class VizTypePicker extends PureComponent<Props, State> {
 export class VizTypePicker extends PureComponent<Props, State> {
+  searchInput: HTMLElement;
+
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
@@ -36,34 +38,55 @@ export class VizTypePicker extends PureComponent<Props, State> {
   renderVizPlugin = (plugin, index) => {
   renderVizPlugin = (plugin, index) => {
     const cssClass = classNames({
     const cssClass = classNames({
       'viz-picker__item': true,
       'viz-picker__item': true,
-      'viz-picker__item--selected': plugin.id === this.props.currentType,
+      'viz-picker__item--selected': plugin.id === this.props.current.id,
     });
     });
 
 
     return (
     return (
       <div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
       <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 className="viz-picker__item-name">{plugin.name}</div>
+        <img className="viz-picker__item-img" src={plugin.info.logos.small} />
       </div>
       </div>
     );
     );
   };
   };
 
 
-  render() {
+  componentDidMount() {
+    setTimeout(() => {
+      this.searchInput.focus();
+    }, 300);
+  }
+
+  renderFilters() {
     return (
     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>
+      <>
+        <label className="gf-form--has-input-icon">
+          <input
+            type="text"
+            className="gf-form-input width-13"
+            placeholder=""
+            ref={elem => (this.searchInput = elem)}
+          />
+          <i className="gf-form-input-icon fa fa-search" />
+        </label>
+        <div className="p-l-1">
+          <button className="btn toggle-btn gf-form-btn active">Basic Types</button>
+          <button className="btn toggle-btn gf-form-btn">Master Types</button>
         </div>
         </div>
-        <div className="viz-picker__items">
-          <CustomScrollbar>
-            <div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
-          </CustomScrollbar>
+      </>
+    );
+  }
+
+  render() {
+    const { pluginList } = this.state;
+
+    return (
+      <>
+        <div className="cta-form__bar">
+          {this.renderFilters()}
+          <div className="gf-form--grow" />
         </div>
         </div>
-      </div>
+
+        <div className="viz-picker">{pluginList.map(this.renderVizPlugin)}</div>
+      </>
     );
     );
   }
   }
 }
 }

+ 10 - 76
public/app/features/dashboard/specs/AddPanelPanel.test.tsx

@@ -3,6 +3,7 @@ import { AddPanelPanel } from './../dashgrid/AddPanelPanel';
 import { PanelModel } from '../panel_model';
 import { PanelModel } from '../panel_model';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 import config from '../../../core/config';
 import config from '../../../core/config';
+import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks';
 
 
 jest.mock('app/core/store', () => ({
 jest.mock('app/core/store', () => ({
   get: key => {
   get: key => {
@@ -18,76 +19,11 @@ describe('AddPanelPanel', () => {
 
 
   beforeEach(() => {
   beforeEach(() => {
     config.panels = [
     config.panels = [
-      {
-        id: 'singlestat',
-        hideFromList: false,
-        name: 'Singlestat',
-        sort: 2,
-        module: '',
-        baseUrl: '',
-        meta: {},
-        info: {
-          logos: {
-            small: '',
-          },
-        },
-      },
-      {
-        id: 'hidden',
-        hideFromList: true,
-        name: 'Hidden',
-        sort: 100,
-        meta: {},
-        module: '',
-        baseUrl: '',
-        info: {
-          logos: {
-            small: '',
-          },
-        },
-      },
-      {
-        id: 'graph',
-        hideFromList: false,
-        name: 'Graph',
-        sort: 1,
-        meta: {},
-        module: '',
-        baseUrl: '',
-        info: {
-          logos: {
-            small: '',
-          },
-        },
-      },
-      {
-        id: 'alexander_zabbix',
-        hideFromList: false,
-        name: 'Zabbix',
-        sort: 100,
-        meta: {},
-        module: '',
-        baseUrl: '',
-        info: {
-          logos: {
-            small: '',
-          },
-        },
-      },
-      {
-        id: 'piechart',
-        hideFromList: false,
-        name: 'Piechart',
-        sort: 100,
-        meta: {},
-        module: '',
-        baseUrl: '',
-        info: {
-          logos: {
-            small: '',
-          },
-        },
-      },
+      getPanelPlugin({ id: 'singlestat', sort: 2 }),
+      getPanelPlugin({ id: 'hidden', sort: 100, hideFromList: true }),
+      getPanelPlugin({ id: 'graph', sort: 1 }),
+      getPanelPlugin({ id: 'alexander_zabbix', sort: 100 }),
+      getPanelPlugin({ id: 'piechart', sort: 100 }),
     ];
     ];
 
 
     dashboardMock = { toggleRow: jest.fn() };
     dashboardMock = { toggleRow: jest.fn() };
@@ -97,16 +33,14 @@ describe('AddPanelPanel', () => {
   });
   });
 
 
   it('should fetch all panels sorted with core plugins first', () => {
   it('should fetch all panels sorted with core plugins first', () => {
-    //console.log(wrapper.debug());
-    //console.log(wrapper.find('.add-panel__item').get(0).props.title);
-    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Singlestat');
-    expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('Piechart');
+    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('singlestat');
+    expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('piechart');
   });
   });
 
 
   it('should filter', () => {
   it('should filter', () => {
     wrapper.find('input').simulate('change', { target: { value: 'p' } });
     wrapper.find('input').simulate('change', { target: { value: 'p' } });
 
 
-    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Piechart');
-    expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('Graph');
+    expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('piechart');
+    expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('graph');
   });
   });
 });
 });

+ 1 - 1
public/app/features/dashboard/utils/getPanelMenu.ts

@@ -1,5 +1,5 @@
 import { updateLocation } from 'app/core/actions';
 import { updateLocation } from 'app/core/actions';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 
 
 import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
 import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
 import { PanelModel } from 'app/features/dashboard/panel_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';

+ 0 - 125
public/app/features/datasources/DataSourceSettings.tsx

@@ -1,125 +0,0 @@
-import React, { PureComponent } from 'react';
-import { connect } from 'react-redux';
-import { DataSource, Plugin } from 'app/types';
-
-export interface Props {
-  dataSource: DataSource;
-  dataSourceMeta: Plugin;
-}
-interface State {
-  name: string;
-}
-
-enum DataSourceStates {
-  Alpha = 'alpha',
-  Beta = 'beta',
-}
-
-export class DataSourceSettings extends PureComponent<Props, State> {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      name: props.dataSource.name,
-    };
-  }
-
-  onNameChange = event => {
-    this.setState({
-      name: event.target.value,
-    });
-  };
-
-  onSubmit = event => {
-    event.preventDefault();
-    console.log(event);
-  };
-
-  onDelete = event => {
-    console.log(event);
-  };
-
-  isReadyOnly() {
-    return this.props.dataSource.readOnly === true;
-  }
-
-  shouldRenderInfoBox() {
-    const { state } = this.props.dataSourceMeta;
-
-    return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
-  }
-
-  getInfoText() {
-    const { dataSourceMeta } = this.props;
-
-    switch (dataSourceMeta.state) {
-      case DataSourceStates.Alpha:
-        return (
-          'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
-          ' will include breaking changes.'
-        );
-
-      case DataSourceStates.Beta:
-        return (
-          'This plugin is marked as being in a beta development state. This means it is in currently in active' +
-          ' development and could be missing important features.'
-        );
-    }
-
-    return null;
-  }
-
-  render() {
-    const { name } = this.state;
-
-    return (
-      <div>
-        <h3 className="page-sub-heading">Settings</h3>
-        <form onSubmit={this.onSubmit}>
-          <div className="gf-form-group">
-            <div className="gf-form-inline">
-              <div className="gf-form max-width-30">
-                <span className="gf-form-label width-10">Name</span>
-                <input
-                  className="gf-form-input max-width-23"
-                  type="text"
-                  value={name}
-                  placeholder="name"
-                  onChange={this.onNameChange}
-                  required
-                />
-              </div>
-            </div>
-          </div>
-          {this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
-          {this.isReadyOnly() && (
-            <div className="grafana-info-box span8">
-              This datasource was added by config and cannot be modified using the UI. Please contact your server admin
-              to update this datasource.
-            </div>
-          )}
-          <div className="gf-form-button-row">
-            <button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
-              Save &amp; Test
-            </button>
-            <button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
-              Delete
-            </button>
-            <a className="btn btn-inverse" href="datasources">
-              Back
-            </a>
-          </div>
-        </form>
-      </div>
-    );
-  }
-}
-
-function mapStateToProps(state) {
-  return {
-    dataSource: state.dataSources.dataSource,
-    dataSourceMeta: state.dataSources.dataSourceMeta,
-  };
-}
-
-export default connect(mapStateToProps)(DataSourceSettings);

+ 3 - 0
public/app/features/datasources/__mocks__/dataSourcesMocks.ts

@@ -29,6 +29,9 @@ export const getMockDataSource = (): DataSource => {
   return {
   return {
     access: '',
     access: '',
     basicAuth: false,
     basicAuth: false,
+    basicAuthUser: '',
+    basicAuthPassword: '',
+    withCredentials: false,
     database: '',
     database: '',
     id: 13,
     id: 13,
     isDefault: false,
     isDefault: false,

+ 20 - 0
public/app/features/datasources/settings/BasicSettings.test.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import BasicSettings, { Props } from './BasicSettings';
+
+const setup = () => {
+  const props: Props = {
+    dataSourceName: 'Graphite',
+    onChange: jest.fn(),
+  };
+
+  return shallow(<BasicSettings {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 34 - 0
public/app/features/datasources/settings/BasicSettings.tsx

@@ -0,0 +1,34 @@
+import React, { SFC } from 'react';
+import { Label } from 'app/core/components/Label/Label';
+
+export interface Props {
+  dataSourceName: string;
+  onChange: (name: string) => void;
+}
+
+const BasicSettings: SFC<Props> = ({ dataSourceName, onChange }) => {
+  return (
+    <div className="gf-form-group">
+      <div className="gf-form max-width-30">
+        <Label
+          tooltip={
+            'The name is used when you select the data source in panels. The Default data source is' +
+            'preselected in new panels.'
+          }
+        >
+          Name
+        </Label>
+        <input
+          className="gf-form-input max-width-23"
+          type="text"
+          value={dataSourceName}
+          placeholder="Name"
+          onChange={event => onChange(event.target.value)}
+          required
+        />
+      </div>
+    </div>
+  );
+};
+
+export default BasicSettings;

+ 31 - 0
public/app/features/datasources/settings/ButtonRow.test.tsx

@@ -0,0 +1,31 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import ButtonRow, { Props } from './ButtonRow';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    isReadOnly: true,
+    onSubmit: jest.fn(),
+    onDelete: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<ButtonRow {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render with buttons enabled', () => {
+    const wrapper = setup({
+      isReadOnly: false,
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 25 - 0
public/app/features/datasources/settings/ButtonRow.tsx

@@ -0,0 +1,25 @@
+import React, { SFC } from 'react';
+
+export interface Props {
+  isReadOnly: boolean;
+  onDelete: () => void;
+  onSubmit: (event) => void;
+}
+
+const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
+  return (
+    <div className="gf-form-button-row">
+      <button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>
+        Save &amp; Test
+      </button>
+      <button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
+        Delete
+      </button>
+      <a className="btn btn-inverse" href="/datasources">
+        Back
+      </a>
+    </div>
+  );
+};
+
+export default ButtonRow;

+ 63 - 0
public/app/features/datasources/settings/DataSourceSettings.test.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DataSourceSettings, Props } from './DataSourceSettings';
+import { DataSource, NavModel } from '../../../types';
+import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
+import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    navModel: {} as NavModel,
+    dataSource: getMockDataSource(),
+    dataSourceMeta: getMockPlugin(),
+    pageId: 1,
+    deleteDataSource: jest.fn(),
+    loadDataSource: jest.fn(),
+    setDataSourceName: jest.fn(),
+    updateDataSource: jest.fn(),
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<DataSourceSettings {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render loader', () => {
+    const wrapper = setup({
+      dataSource: {} as DataSource,
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render beta info text', () => {
+    const wrapper = setup({
+      dataSourceMeta: { ...getMockPlugin(), state: 'beta' },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render alpha info text', () => {
+    const wrapper = setup({
+      dataSourceMeta: { ...getMockPlugin(), state: 'alpha' },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render is ready only message', () => {
+    const wrapper = setup({
+      dataSource: { ...getMockDataSource(), readOnly: true },
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 245 - 0
public/app/features/datasources/settings/DataSourceSettings.tsx

@@ -0,0 +1,245 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import PageLoader from 'app/core/components/PageLoader/PageLoader';
+import PluginSettings from './PluginSettings';
+import BasicSettings from './BasicSettings';
+import ButtonRow from './ButtonRow';
+
+import appEvents from 'app/core/app_events';
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+
+import { getDataSource, getDataSourceMeta } from '../state/selectors';
+import { deleteDataSource, loadDataSource, setDataSourceName, updateDataSource } from '../state/actions';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { getRouteParamsId } from 'app/core/selectors/location';
+
+import { DataSource, NavModel, Plugin } from 'app/types/';
+import { getDataSourceLoadingNav } from '../state/navModel';
+
+export interface Props {
+  navModel: NavModel;
+  dataSource: DataSource;
+  dataSourceMeta: Plugin;
+  pageId: number;
+  deleteDataSource: typeof deleteDataSource;
+  loadDataSource: typeof loadDataSource;
+  setDataSourceName: typeof setDataSourceName;
+  updateDataSource: typeof updateDataSource;
+}
+
+interface State {
+  dataSource: DataSource;
+  isTesting?: boolean;
+  testingMessage?: string;
+  testingStatus?: string;
+}
+
+enum DataSourceStates {
+  Alpha = 'alpha',
+  Beta = 'beta',
+}
+
+export class DataSourceSettings extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      dataSource: {} as DataSource,
+    };
+  }
+
+  async componentDidMount() {
+    const { loadDataSource, pageId } = this.props;
+
+    await loadDataSource(pageId);
+  }
+
+  onSubmit = async event => {
+    event.preventDefault();
+
+    await this.props.updateDataSource({ ...this.state.dataSource, name: this.props.dataSource.name });
+
+    this.testDataSource();
+  };
+
+  onDelete = () => {
+    appEvents.emit('confirm-modal', {
+      title: 'Delete',
+      text: 'Are you sure you want to delete this data source?',
+      yesText: 'Delete',
+      icon: 'fa-trash',
+      onConfirm: () => {
+        this.confirmDelete();
+      },
+    });
+  };
+
+  confirmDelete = () => {
+    this.props.deleteDataSource();
+  };
+
+  onModelChange = dataSource => {
+    this.setState({
+      dataSource: dataSource,
+    });
+  };
+
+  isReadOnly() {
+    return this.props.dataSource.readOnly === true;
+  }
+
+  shouldRenderInfoBox() {
+    const { state } = this.props.dataSourceMeta;
+
+    return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
+  }
+
+  getInfoText() {
+    const { dataSourceMeta } = this.props;
+
+    switch (dataSourceMeta.state) {
+      case DataSourceStates.Alpha:
+        return (
+          'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
+          ' will include breaking changes.'
+        );
+
+      case DataSourceStates.Beta:
+        return (
+          'This plugin is marked as being in a beta development state. This means it is in currently in active' +
+          ' development and could be missing important features.'
+        );
+    }
+
+    return null;
+  }
+
+  renderIsReadOnlyMessage() {
+    return (
+      <div className="grafana-info-box span8">
+        This datasource was added by config and cannot be modified using the UI. Please contact your server admin to
+        update this datasource.
+      </div>
+    );
+  }
+
+  async testDataSource() {
+    const dsApi = await getDatasourceSrv().get(this.state.dataSource.name);
+
+    if (!dsApi.testDatasource) {
+      return;
+    }
+
+    this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
+
+    getBackendSrv().withNoBackendCache(async () => {
+      try {
+        const result = await dsApi.testDatasource();
+
+        this.setState({
+          isTesting: false,
+          testingStatus: result.status,
+          testingMessage: result.message,
+        });
+      } catch (err) {
+        let message = '';
+
+        if (err.statusText) {
+          message = 'HTTP Error ' + err.statusText;
+        } else {
+          message = err.message;
+        }
+
+        this.setState({
+          isTesting: false,
+          testingStatus: 'error',
+          testingMessage: message,
+        });
+      }
+    });
+  }
+
+  render() {
+    const { dataSource, dataSourceMeta, navModel } = this.props;
+    const { testingMessage, testingStatus } = this.state;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        {Object.keys(dataSource).length === 0 ? (
+          <PageLoader pageName="Data source settings" />
+        ) : (
+          <div className="page-container page-body">
+            <div>
+              <form onSubmit={this.onSubmit}>
+                <BasicSettings
+                  dataSourceName={this.props.dataSource.name}
+                  onChange={name => this.props.setDataSourceName(name)}
+                />
+
+                {this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
+
+                {this.isReadOnly() && this.renderIsReadOnlyMessage()}
+                {dataSourceMeta.module && (
+                  <PluginSettings
+                    dataSource={dataSource}
+                    dataSourceMeta={dataSourceMeta}
+                    onModelChange={this.onModelChange}
+                  />
+                )}
+
+                <div className="gf-form-group section">
+                  {testingMessage && (
+                    <div className={`alert-${testingStatus} alert`}>
+                      <div className="alert-icon">
+                        {testingStatus === 'error' ? (
+                          <i className="fa fa-exclamation-triangle" />
+                        ) : (
+                          <i className="fa fa-check" />
+                        )}
+                      </div>
+                      <div className="alert-body">
+                        <div className="alert-title">{testingMessage}</div>
+                      </div>
+                    </div>
+                  )}
+                </div>
+
+                <ButtonRow
+                  onSubmit={event => this.onSubmit(event)}
+                  isReadOnly={this.isReadOnly()}
+                  onDelete={this.onDelete}
+                />
+              </form>
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  const pageId = getRouteParamsId(state.location);
+  const dataSource = getDataSource(state.dataSources, pageId);
+
+  return {
+    navModel: getNavModel(state.navIndex, `datasource-settings-${pageId}`, getDataSourceLoadingNav('settings')),
+    dataSource: getDataSource(state.dataSources, pageId),
+    dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type),
+    pageId: pageId,
+  };
+}
+
+const mapDispatchToProps = {
+  deleteDataSource,
+  loadDataSource,
+  setDataSourceName,
+  updateDataSource,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettings));

+ 63 - 0
public/app/features/datasources/settings/PluginSettings.tsx

@@ -0,0 +1,63 @@
+import React, { PureComponent } from 'react';
+import _ from 'lodash';
+import { DataSource, Plugin } from 'app/types/';
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+
+export interface Props {
+  dataSource: DataSource;
+  dataSourceMeta: Plugin;
+  onModelChange: (dataSource: DataSource) => void;
+}
+
+export class PluginSettings extends PureComponent<Props> {
+  element: any;
+  component: AngularComponent;
+  scopeProps: {
+    ctrl: { datasourceMeta: Plugin; current: DataSource };
+    onModelChanged: (dataSource: DataSource) => void;
+  };
+
+  constructor(props) {
+    super(props);
+
+    this.scopeProps = {
+      ctrl: { datasourceMeta: props.dataSourceMeta, current: _.cloneDeep(props.dataSource) },
+      onModelChanged: this.onModelChanged,
+    };
+  }
+
+  componentDidMount() {
+    if (!this.element) {
+      return;
+    }
+
+    const loader = getAngularLoader();
+    const template = '<plugin-component type="datasource-config-ctrl" />';
+
+    this.component = loader.load(this.element, this.scopeProps, template);
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.dataSource !== prevProps.dataSource) {
+      this.scopeProps.ctrl.current = _.cloneDeep(this.props.dataSource);
+
+      this.component.digest();
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.component) {
+      this.component.destroy();
+    }
+  }
+
+  onModelChanged = (dataSource: DataSource) => {
+    this.props.onModelChange(dataSource);
+  };
+
+  render() {
+    return <div ref={element => (this.element = element)} />;
+  }
+}
+
+export default PluginSettings;

+ 25 - 0
public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap

@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="gf-form-group"
+>
+  <div
+    className="gf-form max-width-30"
+  >
+    <Component
+      tooltip="The name is used when you select the data source in panels. The Default data source ispreselected in new panels."
+    >
+      Name
+    </Component>
+    <input
+      className="gf-form-input max-width-23"
+      onChange={[Function]}
+      placeholder="Name"
+      required={true}
+      type="text"
+      value="Graphite"
+    />
+  </div>
+</div>
+`;

+ 59 - 0
public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap

@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="gf-form-button-row"
+>
+  <button
+    className="btn btn-success"
+    disabled={true}
+    onClick={[Function]}
+    type="submit"
+  >
+    Save & Test
+  </button>
+  <button
+    className="btn btn-danger"
+    disabled={true}
+    onClick={[MockFunction]}
+    type="submit"
+  >
+    Delete
+  </button>
+  <a
+    className="btn btn-inverse"
+    href="/datasources"
+  >
+    Back
+  </a>
+</div>
+`;
+
+exports[`Render should render with buttons enabled 1`] = `
+<div
+  className="gf-form-button-row"
+>
+  <button
+    className="btn btn-success"
+    disabled={false}
+    onClick={[Function]}
+    type="submit"
+  >
+    Save & Test
+  </button>
+  <button
+    className="btn btn-danger"
+    disabled={false}
+    onClick={[MockFunction]}
+    type="submit"
+  >
+    Delete
+  </button>
+  <a
+    className="btn btn-inverse"
+    href="/datasources"
+  >
+    Back
+  </a>
+</div>
+`;

+ 395 - 0
public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap

@@ -0,0 +1,395 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render alpha info text 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <div>
+      <form
+        onSubmit={[Function]}
+      >
+        <BasicSettings
+          dataSourceName="gdev-cloudwatch"
+          onChange={[Function]}
+        />
+        <div
+          className="grafana-info-box"
+        >
+          This plugin is marked as being in alpha state, which means it is in early development phase and updates will include breaking changes.
+        </div>
+        <PluginSettings
+          dataSource={
+            Object {
+              "access": "",
+              "basicAuth": false,
+              "basicAuthPassword": "",
+              "basicAuthUser": "",
+              "database": "",
+              "id": 13,
+              "isDefault": false,
+              "jsonData": Object {
+                "authType": "credentials",
+                "defaultRegion": "eu-west-2",
+              },
+              "name": "gdev-cloudwatch",
+              "orgId": 1,
+              "password": "",
+              "readOnly": false,
+              "type": "cloudwatch",
+              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+              "url": "",
+              "user": "",
+              "withCredentials": false,
+            }
+          }
+          dataSourceMeta={
+            Object {
+              "defaultNavUrl": "some/url",
+              "enabled": false,
+              "hasUpdate": false,
+              "id": "1",
+              "info": Object {
+                "author": Object {
+                  "name": "Grafana Labs",
+                  "url": "url/to/GrafanaLabs",
+                },
+                "description": "pretty decent plugin",
+                "links": Array [
+                  "one link",
+                ],
+                "logos": Object {
+                  "large": "large/logo",
+                  "small": "small/logo",
+                },
+                "screenshots": Array [
+                  Object {
+                    "path": "screenshot",
+                  },
+                ],
+                "updated": "2018-09-26",
+                "version": "1",
+              },
+              "latestVersion": "1",
+              "module": Object {},
+              "name": "pretty cool plugin 1",
+              "pinned": false,
+              "state": "alpha",
+              "type": "",
+            }
+          }
+          onModelChange={[Function]}
+        />
+        <div
+          className="gf-form-group section"
+        />
+        <ButtonRow
+          isReadOnly={false}
+          onDelete={[Function]}
+          onSubmit={[Function]}
+        />
+      </form>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render beta info text 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <div>
+      <form
+        onSubmit={[Function]}
+      >
+        <BasicSettings
+          dataSourceName="gdev-cloudwatch"
+          onChange={[Function]}
+        />
+        <div
+          className="grafana-info-box"
+        >
+          This plugin is marked as being in a beta development state. This means it is in currently in active development and could be missing important features.
+        </div>
+        <PluginSettings
+          dataSource={
+            Object {
+              "access": "",
+              "basicAuth": false,
+              "basicAuthPassword": "",
+              "basicAuthUser": "",
+              "database": "",
+              "id": 13,
+              "isDefault": false,
+              "jsonData": Object {
+                "authType": "credentials",
+                "defaultRegion": "eu-west-2",
+              },
+              "name": "gdev-cloudwatch",
+              "orgId": 1,
+              "password": "",
+              "readOnly": false,
+              "type": "cloudwatch",
+              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+              "url": "",
+              "user": "",
+              "withCredentials": false,
+            }
+          }
+          dataSourceMeta={
+            Object {
+              "defaultNavUrl": "some/url",
+              "enabled": false,
+              "hasUpdate": false,
+              "id": "1",
+              "info": Object {
+                "author": Object {
+                  "name": "Grafana Labs",
+                  "url": "url/to/GrafanaLabs",
+                },
+                "description": "pretty decent plugin",
+                "links": Array [
+                  "one link",
+                ],
+                "logos": Object {
+                  "large": "large/logo",
+                  "small": "small/logo",
+                },
+                "screenshots": Array [
+                  Object {
+                    "path": "screenshot",
+                  },
+                ],
+                "updated": "2018-09-26",
+                "version": "1",
+              },
+              "latestVersion": "1",
+              "module": Object {},
+              "name": "pretty cool plugin 1",
+              "pinned": false,
+              "state": "beta",
+              "type": "",
+            }
+          }
+          onModelChange={[Function]}
+        />
+        <div
+          className="gf-form-group section"
+        />
+        <ButtonRow
+          isReadOnly={false}
+          onDelete={[Function]}
+          onSubmit={[Function]}
+        />
+      </form>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <div>
+      <form
+        onSubmit={[Function]}
+      >
+        <BasicSettings
+          dataSourceName="gdev-cloudwatch"
+          onChange={[Function]}
+        />
+        <PluginSettings
+          dataSource={
+            Object {
+              "access": "",
+              "basicAuth": false,
+              "basicAuthPassword": "",
+              "basicAuthUser": "",
+              "database": "",
+              "id": 13,
+              "isDefault": false,
+              "jsonData": Object {
+                "authType": "credentials",
+                "defaultRegion": "eu-west-2",
+              },
+              "name": "gdev-cloudwatch",
+              "orgId": 1,
+              "password": "",
+              "readOnly": false,
+              "type": "cloudwatch",
+              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+              "url": "",
+              "user": "",
+              "withCredentials": false,
+            }
+          }
+          dataSourceMeta={
+            Object {
+              "defaultNavUrl": "some/url",
+              "enabled": false,
+              "hasUpdate": false,
+              "id": "1",
+              "info": Object {
+                "author": Object {
+                  "name": "Grafana Labs",
+                  "url": "url/to/GrafanaLabs",
+                },
+                "description": "pretty decent plugin",
+                "links": Array [
+                  "one link",
+                ],
+                "logos": Object {
+                  "large": "large/logo",
+                  "small": "small/logo",
+                },
+                "screenshots": Array [
+                  Object {
+                    "path": "screenshot",
+                  },
+                ],
+                "updated": "2018-09-26",
+                "version": "1",
+              },
+              "latestVersion": "1",
+              "module": Object {},
+              "name": "pretty cool plugin 1",
+              "pinned": false,
+              "state": "",
+              "type": "",
+            }
+          }
+          onModelChange={[Function]}
+        />
+        <div
+          className="gf-form-group section"
+        />
+        <ButtonRow
+          isReadOnly={false}
+          onDelete={[Function]}
+          onSubmit={[Function]}
+        />
+      </form>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render is ready only message 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <div>
+      <form
+        onSubmit={[Function]}
+      >
+        <BasicSettings
+          dataSourceName="gdev-cloudwatch"
+          onChange={[Function]}
+        />
+        <div
+          className="grafana-info-box span8"
+        >
+          This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
+        </div>
+        <PluginSettings
+          dataSource={
+            Object {
+              "access": "",
+              "basicAuth": false,
+              "basicAuthPassword": "",
+              "basicAuthUser": "",
+              "database": "",
+              "id": 13,
+              "isDefault": false,
+              "jsonData": Object {
+                "authType": "credentials",
+                "defaultRegion": "eu-west-2",
+              },
+              "name": "gdev-cloudwatch",
+              "orgId": 1,
+              "password": "",
+              "readOnly": true,
+              "type": "cloudwatch",
+              "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+              "url": "",
+              "user": "",
+              "withCredentials": false,
+            }
+          }
+          dataSourceMeta={
+            Object {
+              "defaultNavUrl": "some/url",
+              "enabled": false,
+              "hasUpdate": false,
+              "id": "1",
+              "info": Object {
+                "author": Object {
+                  "name": "Grafana Labs",
+                  "url": "url/to/GrafanaLabs",
+                },
+                "description": "pretty decent plugin",
+                "links": Array [
+                  "one link",
+                ],
+                "logos": Object {
+                  "large": "large/logo",
+                  "small": "small/logo",
+                },
+                "screenshots": Array [
+                  Object {
+                    "path": "screenshot",
+                  },
+                ],
+                "updated": "2018-09-26",
+                "version": "1",
+              },
+              "latestVersion": "1",
+              "module": Object {},
+              "name": "pretty cool plugin 1",
+              "pinned": false,
+              "state": "",
+              "type": "",
+            }
+          }
+          onModelChange={[Function]}
+        />
+        <div
+          className="gf-form-group section"
+        />
+        <ButtonRow
+          isReadOnly={true}
+          onDelete={[Function]}
+          onSubmit={[Function]}
+        />
+      </form>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Render should render loader 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <PageLoader
+    pageName="Data source settings"
+  />
+</div>
+`;

+ 54 - 13
public/app/features/datasources/state/actions.ts

@@ -1,10 +1,12 @@
 import { ThunkAction } from 'redux-thunk';
 import { ThunkAction } from 'redux-thunk';
-import { DataSource, Plugin, StoreState } from 'app/types';
-import { getBackendSrv } from '../../../core/services/backend_srv';
-import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
-import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
-import { UpdateLocationAction } from '../../../core/actions/location';
+import config from '../../../core/config';
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
+import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
+import { UpdateLocationAction } from 'app/core/actions/location';
 import { buildNavModel } from './navModel';
 import { buildNavModel } from './navModel';
+import { DataSource, Plugin, StoreState } from 'app/types';
 
 
 export enum ActionTypes {
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
   LoadDataSources = 'LOAD_DATA_SOURCES',
@@ -14,43 +16,49 @@ export enum ActionTypes {
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
   SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
   SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
   SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
   SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
+  SetDataSourceName = 'SET_DATA_SOURCE_NAME',
 }
 }
 
 
-export interface LoadDataSourcesAction {
+interface LoadDataSourcesAction {
   type: ActionTypes.LoadDataSources;
   type: ActionTypes.LoadDataSources;
   payload: DataSource[];
   payload: DataSource[];
 }
 }
 
 
-export interface SetDataSourcesSearchQueryAction {
+interface SetDataSourcesSearchQueryAction {
   type: ActionTypes.SetDataSourcesSearchQuery;
   type: ActionTypes.SetDataSourcesSearchQuery;
   payload: string;
   payload: string;
 }
 }
 
 
-export interface SetDataSourcesLayoutModeAction {
+interface SetDataSourcesLayoutModeAction {
   type: ActionTypes.SetDataSourcesLayoutMode;
   type: ActionTypes.SetDataSourcesLayoutMode;
   payload: LayoutMode;
   payload: LayoutMode;
 }
 }
 
 
-export interface LoadDataSourceTypesAction {
+interface LoadDataSourceTypesAction {
   type: ActionTypes.LoadDataSourceTypes;
   type: ActionTypes.LoadDataSourceTypes;
   payload: Plugin[];
   payload: Plugin[];
 }
 }
 
 
-export interface SetDataSourceTypeSearchQueryAction {
+interface SetDataSourceTypeSearchQueryAction {
   type: ActionTypes.SetDataSourceTypeSearchQuery;
   type: ActionTypes.SetDataSourceTypeSearchQuery;
   payload: string;
   payload: string;
 }
 }
 
 
-export interface LoadDataSourceAction {
+interface LoadDataSourceAction {
   type: ActionTypes.LoadDataSource;
   type: ActionTypes.LoadDataSource;
   payload: DataSource;
   payload: DataSource;
 }
 }
 
 
-export interface LoadDataSourceMetaAction {
+interface LoadDataSourceMetaAction {
   type: ActionTypes.LoadDataSourceMeta;
   type: ActionTypes.LoadDataSourceMeta;
   payload: Plugin;
   payload: Plugin;
 }
 }
 
 
+interface SetDataSourceNameAction {
+  type: ActionTypes.SetDataSourceName;
+  payload: string;
+}
+
 const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
 const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
   type: ActionTypes.LoadDataSources,
   type: ActionTypes.LoadDataSources,
   payload: dataSources,
   payload: dataSources,
@@ -86,6 +94,11 @@ export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSe
   payload: query,
   payload: query,
 });
 });
 
 
+export const setDataSourceName = (name: string) => ({
+  type: ActionTypes.SetDataSourceName,
+  payload: name,
+});
+
 export type Action =
 export type Action =
   | LoadDataSourcesAction
   | LoadDataSourcesAction
   | SetDataSourcesSearchQueryAction
   | SetDataSourcesSearchQueryAction
@@ -95,7 +108,8 @@ export type Action =
   | SetDataSourceTypeSearchQueryAction
   | SetDataSourceTypeSearchQueryAction
   | LoadDataSourceAction
   | LoadDataSourceAction
   | UpdateNavIndexAction
   | UpdateNavIndexAction
-  | LoadDataSourceMetaAction;
+  | LoadDataSourceMetaAction
+  | SetDataSourceNameAction;
 
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
 
@@ -145,6 +159,23 @@ export function loadDataSourceTypes(): ThunkResult<void> {
   };
   };
 }
 }
 
 
+export function updateDataSource(dataSource: DataSource): ThunkResult<void> {
+  return async dispatch => {
+    await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
+    await updateFrontendSettings();
+    return dispatch(loadDataSource(dataSource.id));
+  };
+}
+
+export function deleteDataSource(): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const dataSource = getStore().dataSources.dataSource;
+
+    await getBackendSrv().delete(`/api/datasources/${dataSource.id}`);
+    dispatch(updateLocation({ path: '/datasources' }));
+  };
+}
+
 export function nameExits(dataSources, name) {
 export function nameExits(dataSources, name) {
   return (
   return (
     dataSources.filter(dataSource => {
     dataSources.filter(dataSource => {
@@ -173,6 +204,16 @@ export function findNewName(dataSources, name) {
   return name;
   return name;
 }
 }
 
 
+function updateFrontendSettings() {
+  return getBackendSrv()
+    .get('/api/frontend/settings')
+    .then(settings => {
+      config.datasources = settings.datasources;
+      config.defaultDatasource = settings.defaultDatasource;
+      getDatasourceSrv().init();
+    });
+}
+
 function nameHasSuffix(name) {
 function nameHasSuffix(name) {
   return name.endsWith('-', name.length - 1);
   return name.endsWith('-', name.length - 1);
 }
 }

+ 4 - 1
public/app/features/datasources/state/navModel.ts

@@ -48,6 +48,9 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
     {
     {
       access: '',
       access: '',
       basicAuth: false,
       basicAuth: false,
+      basicAuthUser: '',
+      basicAuthPassword: '',
+      withCredentials: false,
       database: '',
       database: '',
       id: 1,
       id: 1,
       isDefault: false,
       isDefault: false,
@@ -75,7 +78,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
           large: '',
           large: '',
           small: '',
           small: '',
         },
         },
-        screenshots: '',
+        screenshots: [],
         updated: '',
         updated: '',
         version: '',
         version: '',
       },
       },

+ 4 - 1
public/app/features/datasources/state/reducers.ts

@@ -10,8 +10,8 @@ const initialState: DataSourcesState = {
   dataSourcesCount: 0,
   dataSourcesCount: 0,
   dataSourceTypes: [] as Plugin[],
   dataSourceTypes: [] as Plugin[],
   dataSourceTypeSearchQuery: '',
   dataSourceTypeSearchQuery: '',
-  dataSourceMeta: {} as Plugin,
   hasFetched: false,
   hasFetched: false,
+  dataSourceMeta: {} as Plugin,
 };
 };
 
 
 export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
 export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
@@ -36,6 +36,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
 
 
     case ActionTypes.LoadDataSourceMeta:
     case ActionTypes.LoadDataSourceMeta:
       return { ...state, dataSourceMeta: action.payload };
       return { ...state, dataSourceMeta: action.payload };
+
+    case ActionTypes.SetDataSourceName:
+      return { ...state, dataSource: { ...state.dataSource, name: action.payload } };
   }
   }
 
 
   return state;
   return state;

+ 9 - 1
public/app/features/datasources/state/selectors.ts

@@ -20,7 +20,15 @@ export const getDataSource = (state, dataSourceId): DataSource | null => {
   if (state.dataSource.id === parseInt(dataSourceId, 10)) {
   if (state.dataSource.id === parseInt(dataSourceId, 10)) {
     return state.dataSource;
     return state.dataSource;
   }
   }
-  return null;
+  return {} as DataSource;
+};
+
+export const getDataSourceMeta = (state, type): Plugin => {
+  if (state.dataSourceMeta.id === type) {
+    return state.dataSourceMeta;
+  }
+
+  return {} as Plugin;
 };
 };
 
 
 export const getDataSourcesSearchQuery = state => state.searchQuery;
 export const getDataSourcesSearchQuery = state => state.searchQuery;

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

@@ -44,8 +44,8 @@ const panelTemplate = `
             </li>
             </li>
           </ul>
           </ul>
 
 
-          <button class="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
-            <i class="fa fa-remove"></i>
+          <button class="panel-editor-tabs__close" ng-click="ctrl.exitFullscreen();">
+            <i class="fa fa-reply"></i>
           </button>
           </button>
         </div>
         </div>
 
 

+ 30 - 3
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -1,4 +1,4 @@
-import { Plugin } from 'app/types';
+import { Plugin, PanelPlugin } from 'app/types';
 
 
 export const getMockPlugins = (amount: number): Plugin[] => {
 export const getMockPlugins = (amount: number): Plugin[] => {
   const plugins = [];
   const plugins = [];
@@ -17,7 +17,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
         description: 'pretty decent plugin',
         description: 'pretty decent plugin',
         links: ['one link'],
         links: ['one link'],
         logos: { small: 'small/logo', large: 'large/logo' },
         logos: { small: 'small/logo', large: 'large/logo' },
-        screenshots: `screenshot/${i}`,
+        screenshots: [{ path: `screenshot/${i}` }],
         updated: '2018-09-26',
         updated: '2018-09-26',
         version: '1',
         version: '1',
       },
       },
@@ -26,12 +26,38 @@ export const getMockPlugins = (amount: number): Plugin[] => {
       pinned: false,
       pinned: false,
       state: '',
       state: '',
       type: '',
       type: '',
+      module: {},
     });
     });
   }
   }
 
 
   return plugins;
   return plugins;
 };
 };
 
 
+export const getPanelPlugin = (options: { id: string; sort?: number; hideFromList?: boolean }): PanelPlugin => {
+  return {
+    id: options.id,
+    name: options.id,
+    sort: options.sort || 1,
+    info: {
+      author: {
+        name: options.id + 'name',
+      },
+      description: '',
+      links: [],
+      logos: {
+        large: '',
+        small: '',
+      },
+      screenshots: [],
+      updated: '',
+      version: '',
+    },
+    hideFromList: options.hideFromList === true,
+    module: '',
+    baseUrl: '',
+  };
+};
+
 export const getMockPlugin = () => {
 export const getMockPlugin = () => {
   return {
   return {
     defaultNavUrl: 'some/url',
     defaultNavUrl: 'some/url',
@@ -46,7 +72,7 @@ export const getMockPlugin = () => {
       description: 'pretty decent plugin',
       description: 'pretty decent plugin',
       links: ['one link'],
       links: ['one link'],
       logos: { small: 'small/logo', large: 'large/logo' },
       logos: { small: 'small/logo', large: 'large/logo' },
-      screenshots: 'screenshot/1',
+      screenshots: [{ path: `screenshot` }],
       updated: '2018-09-26',
       updated: '2018-09-26',
       version: '1',
       version: '1',
     },
     },
@@ -55,5 +81,6 @@ export const getMockPlugin = () => {
     pinned: false,
     pinned: false,
     state: '',
     state: '',
     type: '',
     type: '',
+    module: {},
   };
   };
 };
 };

+ 36 - 6
public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap

@@ -28,11 +28,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/0",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/0",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.0",
           "latestVersion": "1.0",
+          "module": Object {},
           "name": "pretty cool plugin-0",
           "name": "pretty cool plugin-0",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",
@@ -61,11 +66,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/1",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/1",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.1",
           "latestVersion": "1.1",
+          "module": Object {},
           "name": "pretty cool plugin-1",
           "name": "pretty cool plugin-1",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",
@@ -94,11 +104,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/2",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/2",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.2",
           "latestVersion": "1.2",
+          "module": Object {},
           "name": "pretty cool plugin-2",
           "name": "pretty cool plugin-2",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",
@@ -127,11 +142,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/3",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/3",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.3",
           "latestVersion": "1.3",
+          "module": Object {},
           "name": "pretty cool plugin-3",
           "name": "pretty cool plugin-3",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",
@@ -160,11 +180,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/4",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/4",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.4",
           "latestVersion": "1.4",
+          "module": Object {},
           "name": "pretty cool plugin-4",
           "name": "pretty cool plugin-4",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",
@@ -193,11 +218,16 @@ exports[`Render should render component 1`] = `
               "large": "large/logo",
               "large": "large/logo",
               "small": "small/logo",
               "small": "small/logo",
             },
             },
-            "screenshots": "screenshot/5",
+            "screenshots": Array [
+              Object {
+                "path": "screenshot/5",
+              },
+            ],
             "updated": "2018-09-26",
             "updated": "2018-09-26",
             "version": "1",
             "version": "1",
           },
           },
           "latestVersion": "1.5",
           "latestVersion": "1.5",
+          "module": Object {},
           "name": "pretty cool plugin-5",
           "name": "pretty cool plugin-5",
           "pinned": false,
           "pinned": false,
           "state": "",
           "state": "",

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

@@ -1,4 +1,3 @@
-import './plugin_edit_ctrl';
 import './plugin_page_ctrl';
 import './plugin_page_ctrl';
 import './import_list/import_list';
 import './import_list/import_list';
 import './ds_edit_ctrl';
 import './ds_edit_ctrl';

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

@@ -24,6 +24,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
+import * as gaugePanel from 'app/plugins/panel/gauge/module';
 
 
 const builtInPlugins = {
 const builtInPlugins = {
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/graphite/module': graphitePlugin,
@@ -52,6 +53,7 @@ const builtInPlugins = {
   'app/plugins/panel/table/module': tablePanel,
   'app/plugins/panel/table/module': tablePanel,
   'app/plugins/panel/singlestat/module': singlestatPanel,
   'app/plugins/panel/singlestat/module': singlestatPanel,
   'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
   'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
+  'app/plugins/panel/gauge/module': gaugePanel,
 };
 };
 
 
 export default builtInPlugins;
 export default builtInPlugins;

+ 3 - 6
public/app/features/plugins/datasource_srv.ts

@@ -1,14 +1,11 @@
-// Libraries
 import _ from 'lodash';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 
 
-// Utils
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { importPluginModule } from './plugin_loader';
 import { importPluginModule } from './plugin_loader';
 
 
-// Types
 import { DataSourceApi } from 'app/types/series';
 import { DataSourceApi } from 'app/types/series';
-import { DataSource } from 'app/types';
+import { DataSource, DataSourceSelectItem } from 'app/types';
 
 
 export class DatasourceSrv {
 export class DatasourceSrv {
   datasources: { [name: string]: DataSource };
   datasources: { [name: string]: DataSource };
@@ -102,8 +99,8 @@ export class DatasourceSrv {
     return _.sortBy(es, ['name']);
     return _.sortBy(es, ['name']);
   }
   }
 
 
-  getMetricSources(options) {
-    const metricSources = [];
+  getMetricSources(options?) {
+    const metricSources: DataSourceSelectItem[] = [];
 
 
     _.each(config.datasources, (value, key) => {
     _.each(config.datasources, (value, key) => {
       if (value.meta && value.meta.metrics) {
       if (value.meta && value.meta.metrics) {

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

@@ -1,5 +1,5 @@
 import { coreModule } from 'app/core/core';
 import { coreModule } from 'app/core/core';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { buildNavModel } from './state/navModel';
 import { buildNavModel } from './state/navModel';
 
 

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

@@ -1,7 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import config from 'app/core/config';
 import config from 'app/core/config';
 import { coreModule, appEvents } from 'app/core/core';
 import { coreModule, appEvents } from 'app/core/core';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { buildNavModel } from './state/navModel';
 import { buildNavModel } from './state/navModel';
 
 

+ 0 - 72
public/app/features/plugins/partials/ds_edit.html

@@ -1,72 +0,0 @@
-<page-header model="ctrl.navModel"></page-header>
-
-<div class="page-container page-body">
-  <h3 class="page-sub-heading">Settings</h3>
-
-  <form name="ctrl.editForm" ng-if="ctrl.current">
-    <div class="gf-form-group">
-      <div class="gf-form-inline">
-        <div class="gf-form max-width-30">
-          <span class="gf-form-label width-10">Name</span>
-          <input class="gf-form-input max-width-23" type="text" ng-model="ctrl.current.name" placeholder="name" required>
-          <info-popover offset="0px -135px" mode="right-absolute">
-            The name is used when you select the data source in panels.
-            The <em>Default</em> data source is preselected in new
-            panels.
-          </info-popover>
-        </div>
-        <gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
-      </div>
-    </div>
-
-    <div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'alpha'">
-      This plugin is marked as being in alpha state, which means it is in early development phase and
-      updates will include breaking changes.
-    </div>
-
-		<div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'beta'">
-      This plugin is marked as being in a beta development state. This means it is in currently in active development and could be
-      missing important features.
-    </div>
-
-    <rebuild-on-change property="ctrl.datasourceMeta.id">
-      <plugin-component type="datasource-config-ctrl">
-      </plugin-component>
-    </rebuild-on-change>
-
-    <div ng-if="ctrl.hasDashboards">
-      <h3 class="section-heading">Bundled Plugin Dashboards</h3>
-      <div class="section">
-        <dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
-      </div>
-    </div>
-
-    <div ng-if="ctrl.testing" class="gf-form-group section">
-      <h5 ng-show="!ctrl.testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
-      <div class="alert-{{ctrl.testing.status}} alert" ng-show="ctrl.testing.done">
-        <div class="alert-icon">
-          <i class="fa fa-exclamation-triangle" ng-show="ctrl.testing.status === 'error'"></i>
-          <i class="fa fa-check" ng-show="ctrl.testing.status !== 'error'"></i>
-        </div>
-        <div class="alert-body">
-          <div class="alert-title">{{ctrl.testing.message}}</div>
-        </div>
-      </div>
-    </div>
-
-		<div class="grafana-info-box span8" ng-if="ctrl.current.readOnly">
-			This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
-		</div>
-
-		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-success" ng-disabled="ctrl.current.readOnly"  ng-click="ctrl.saveChanges()">Save &amp; Test</button>
-			<button type="submit" class="btn btn-danger" ng-disabled="ctrl.current.readOnly"  ng-show="!ctrl.isNew" ng-click="ctrl.delete()">Delete</button>
-			<a class="btn btn-inverse" href="datasources">Back</a>
-		</div>
-
-		<br />
-		<br />
-		<br />
-
-	</form>
-</div>

+ 9 - 3
public/app/features/plugins/plugin_component.ts

@@ -5,8 +5,6 @@ import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { importPluginModule } from './plugin_loader';
 import { importPluginModule } from './plugin_loader';
 
 
-import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
-
 /** @ngInject */
 /** @ngInject */
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) {
   function getTemplate(component) {
   function getTemplate(component) {
@@ -69,7 +67,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
     };
     };
 
 
     const panelInfo = config.panels[scope.panel.type];
     const panelInfo = config.panels[scope.panel.type];
-    let panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
+    let panelCtrlPromise = Promise.resolve(null);
     if (panelInfo) {
     if (panelInfo) {
       panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => {
       panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => {
         return panelModule.PanelCtrl;
         return panelModule.PanelCtrl;
@@ -149,6 +147,14 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
             return { notFound: true };
             return { notFound: true };
           }
           }
 
 
+          scope.$watch(
+            'ctrl.current',
+            () => {
+              scope.onModelChanged(scope.ctrl.current);
+            },
+            true
+          );
+
           return {
           return {
             baseUrl: dsMeta.baseUrl,
             baseUrl: dsMeta.baseUrl,
             name: 'ds-config-' + dsMeta.id,
             name: 'ds-config-' + dsMeta.id,

+ 0 - 179
public/app/features/plugins/plugin_edit_ctrl.ts

@@ -1,179 +0,0 @@
-import angular from 'angular';
-import _ from 'lodash';
-import Remarkable from 'remarkable';
-
-export class PluginEditCtrl {
-  model: any;
-  pluginIcon: string;
-  pluginId: any;
-  includes: any;
-  readmeHtml: any;
-  includedDatasources: any;
-  tab: string;
-  navModel: any;
-  hasDashboards: any;
-  preUpdateHook: () => any;
-  postUpdateHook: () => any;
-
-  /** @ngInject */
-  constructor(private $scope, private $rootScope, private backendSrv, private $sce, private $routeParams, navModelSrv) {
-    this.pluginId = $routeParams.pluginId;
-    this.preUpdateHook = () => Promise.resolve();
-    this.postUpdateHook = () => Promise.resolve();
-
-    this.init();
-  }
-
-  setNavModel(model) {
-    let defaultTab = 'readme';
-
-    this.navModel = {
-      main: {
-        img: model.info.logos.large,
-        subTitle: model.info.author.name,
-        url: '',
-        text: model.name,
-        breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
-        children: [
-          {
-            icon: 'fa fa-fw fa-file-text-o',
-            id: 'readme',
-            text: 'Readme',
-            url: `plugins/${this.model.id}/edit?tab=readme`,
-          },
-        ],
-      },
-    };
-
-    if (model.type === 'app') {
-      this.navModel.main.children.push({
-        icon: 'gicon gicon-cog',
-        id: 'config',
-        text: 'Config',
-        url: `plugins/${this.model.id}/edit?tab=config`,
-      });
-
-      const hasDashboards = _.find(model.includes, { type: 'dashboard' });
-
-      if (hasDashboards) {
-        this.navModel.main.children.push({
-          icon: 'gicon gicon-dashboard',
-          id: 'dashboards',
-          text: 'Dashboards',
-          url: `plugins/${this.model.id}/edit?tab=dashboards`,
-        });
-      }
-
-      defaultTab = 'config';
-    }
-
-    this.tab = this.$routeParams.tab || defaultTab;
-
-    for (const tab of this.navModel.main.children) {
-      if (tab.id === this.tab) {
-        tab.active = true;
-      }
-    }
-  }
-
-  init() {
-    return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => {
-      this.model = result;
-      this.pluginIcon = this.getPluginIcon(this.model.type);
-
-      this.model.dependencies.plugins.forEach(plug => {
-        plug.icon = this.getPluginIcon(plug.type);
-      });
-
-      this.includes = _.map(result.includes, plug => {
-        plug.icon = this.getPluginIcon(plug.type);
-        return plug;
-      });
-
-      this.setNavModel(this.model);
-      return this.initReadme();
-    });
-  }
-
-  initReadme() {
-    return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
-      const md = new Remarkable({
-        linkify: true,
-      });
-      this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
-    });
-  }
-
-  getPluginIcon(type) {
-    switch (type) {
-      case 'datasource':
-        return 'icon-gf icon-gf-datasources';
-      case 'panel':
-        return 'icon-gf icon-gf-panel';
-      case 'app':
-        return 'icon-gf icon-gf-apps';
-      case 'page':
-        return 'icon-gf icon-gf-endpoint-tiny';
-      case 'dashboard':
-        return 'icon-gf icon-gf-dashboard';
-      default:
-        return 'icon-gf icon-gf-apps';
-    }
-  }
-
-  update() {
-    this.preUpdateHook()
-      .then(() => {
-        const updateCmd = _.extend(
-          {
-            enabled: this.model.enabled,
-            pinned: this.model.pinned,
-            jsonData: this.model.jsonData,
-            secureJsonData: this.model.secureJsonData,
-          },
-          {}
-        );
-        return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
-      })
-      .then(this.postUpdateHook)
-      .then(res => {
-        window.location.href = window.location.href;
-      });
-  }
-
-  importDashboards() {
-    return Promise.resolve();
-  }
-
-  setPreUpdateHook(callback: () => any) {
-    this.preUpdateHook = callback;
-  }
-
-  setPostUpdateHook(callback: () => any) {
-    this.postUpdateHook = callback;
-  }
-
-  updateAvailable() {
-    const modalScope = this.$scope.$new(true);
-    modalScope.plugin = this.model;
-
-    this.$rootScope.appEvent('show-modal', {
-      src: 'public/app/features/plugins/partials/update_instructions.html',
-      scope: modalScope,
-    });
-  }
-
-  enable() {
-    this.model.enabled = true;
-    this.model.pinned = true;
-    this.update();
-  }
-
-  disable() {
-    this.model.enabled = false;
-    this.model.pinned = false;
-    this.update();
-  }
-}
-
-angular.module('grafana.controllers').controller('PluginEditCtrl', PluginEditCtrl);

+ 3 - 8
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -362,14 +362,9 @@ export default class CloudWatchDatasource {
     const metricName = 'EstimatedCharges';
     const metricName = 'EstimatedCharges';
     const dimensions = {};
     const dimensions = {};
 
 
-    return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(
-      () => {
-        return { status: 'success', message: 'Data source is working' };
-      },
-      err => {
-        return { status: 'error', message: err.message };
-      }
-    );
+    return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(() => {
+      return { status: 'success', message: 'Data source is working' };
+    });
   }
   }
 
 
   awsRequest(url, data) {
   awsRequest(url, data) {

+ 23 - 0
public/app/plugins/panel/gauge/module.tsx

@@ -0,0 +1,23 @@
+import React, { PureComponent } from 'react';
+import Gauge from 'app/viz/Gauge';
+import { NullValueMode, PanelProps } from 'app/types';
+import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
+
+export interface Options {}
+
+interface Props extends PanelProps<Options> {}
+
+export class GaugePanel extends PureComponent<Props> {
+  render() {
+    const { timeSeries } = this.props;
+
+    const vmSeries = getTimeSeriesVMs({
+      timeSeries: timeSeries,
+      nullValueMode: NullValueMode.Ignore,
+    });
+
+    return <Gauge maxValue={100} minValue={0} timeSeries={vmSeries} thresholds={[0, 100]} />;
+  }
+}
+
+export { GaugePanel as PanelComponent };

+ 18 - 0
public/app/plugins/panel/gauge/plugin.json

@@ -0,0 +1,18 @@
+{
+  "type": "panel",
+  "name": "Gauge",
+  "id": "gauge",
+
+  "state": "alpha",
+
+  "info": {
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+
+    }
+  }
+}
+

+ 20 - 5
public/app/plugins/panel/graph2/module.tsx

@@ -61,11 +61,26 @@ export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
 
 
     return (
     return (
       <div>
       <div>
-        <div className="section gf-form-group">
-          <h5 className="page-heading">Draw Modes</h5>
-          <Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
-          <Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
-          <Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
+        <div className="form-option-box">
+          <div className="form-option-box__header">Display Options</div>
+          <div className="section gf-form-group">
+            <h5 className="section-heading">Draw Modes</h5>
+            <Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
+            <Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
+            <Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
+          </div>
+          <div className="section gf-form-group">
+            <h5 className="section-heading">Test Options</h5>
+            <Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
+            <Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
+            <Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
+          </div>
+        </div>
+        <div className="form-option-box">
+          <div className="form-option-box__header">Axes</div>
+        </div>
+        <div className="form-option-box">
+          <div className="form-option-box__header">Thresholds</div>
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 0 - 5
public/app/plugins/panel/unknown/module.html

@@ -1,5 +0,0 @@
-<div class="text-center" style="padding-top: 2rem">
-	Unknown panel type: <strong>{{ctrl.panel.type}}</strong>
-</div>
-
-

+ 0 - 10
public/app/plugins/panel/unknown/module.ts

@@ -1,10 +0,0 @@
-import { PanelCtrl } from 'app/features/panel/panel_ctrl';
-
-export class UnknownPanelCtrl extends PanelCtrl {
-  static templateUrl = 'public/app/plugins/panel/unknown/module.html';
-
-  /** @ngInject */
-  constructor($scope, $injector) {
-    super($scope, $injector);
-  }
-}

+ 1 - 1
public/app/routes/ReactContainer.tsx

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
 
 
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { ContextSrv } from 'app/core/services/context_srv';
 import { ContextSrv } from 'app/core/services/context_srv';

+ 6 - 4
public/app/routes/routes.ts

@@ -14,6 +14,7 @@ import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
 import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import UsersListPage from 'app/features/users/UsersListPage';
 import UsersListPage from 'app/features/users/UsersListPage';
 import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
 import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
+import DataSourceSettings from '../features/datasources/settings/DataSourceSettings';
 import OrgDetailsPage from '../features/org/OrgDetailsPage';
 import OrgDetailsPage from '../features/org/OrgDetailsPage';
 
 
 /** @ngInject */
 /** @ngInject */
@@ -74,10 +75,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
         component: () => DataSourcesListPage,
         component: () => DataSourcesListPage,
       },
       },
     })
     })
-    .when('/datasources/edit/:id', {
-      templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
-      controller: 'DataSourceEditCtrl',
-      controllerAs: 'ctrl',
+    .when('/datasources/edit/:id/', {
+      template: '<react-container />',
+      resolve: {
+        component: () => DataSourceSettings,
+      },
     })
     })
     .when('/datasources/edit/:id/dashboards', {
     .when('/datasources/edit/:id/dashboards', {
       template: '<react-container />',
       template: '<react-container />',

+ 3 - 4
public/app/store/configureStore.ts

@@ -11,6 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers';
 import dataSourcesReducers from 'app/features/datasources/state/reducers';
 import dataSourcesReducers from 'app/features/datasources/state/reducers';
 import usersReducers from 'app/features/users/state/reducers';
 import usersReducers from 'app/features/users/state/reducers';
 import organizationReducers from 'app/features/org/state/reducers';
 import organizationReducers from 'app/features/org/state/reducers';
+import { setStore } from './store';
 
 
 const rootReducers = {
 const rootReducers = {
   ...sharedReducers,
   ...sharedReducers,
@@ -25,8 +26,6 @@ const rootReducers = {
   ...organizationReducers,
   ...organizationReducers,
 };
 };
 
 
-export let store;
-
 export function addRootReducer(reducers) {
 export function addRootReducer(reducers) {
   Object.assign(rootReducers, ...reducers);
   Object.assign(rootReducers, ...reducers);
 }
 }
@@ -38,8 +37,8 @@ export function configureStore() {
 
 
   if (process.env.NODE_ENV !== 'production') {
   if (process.env.NODE_ENV !== 'production') {
     // DEV builds we had the logger middleware
     // DEV builds we had the logger middleware
-    store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
+    setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
   } else {
   } else {
-    store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)));
+    setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
   }
   }
 }
 }

+ 5 - 0
public/app/store/store.ts

@@ -0,0 +1,5 @@
+export let store;
+
+export function setStore(newStore) {
+  store = newStore;
+}

+ 10 - 0
public/app/types/datasources.ts

@@ -13,15 +13,25 @@ export interface DataSource {
   user: string;
   user: string;
   database: string;
   database: string;
   basicAuth: boolean;
   basicAuth: boolean;
+  basicAuthPassword: string;
+  basicAuthUser: string;
   isDefault: boolean;
   isDefault: boolean;
   jsonData: { authType: string; defaultRegion: string };
   jsonData: { authType: string; defaultRegion: string };
   readOnly: boolean;
   readOnly: boolean;
+  withCredentials: boolean;
   meta?: PluginMeta;
   meta?: PluginMeta;
   pluginExports?: PluginExports;
   pluginExports?: PluginExports;
   init?: () => void;
   init?: () => void;
   testDatasource?: () => Promise<any>;
   testDatasource?: () => Promise<any>;
 }
 }
 
 
+export interface DataSourceSelectItem {
+  name: string;
+  value: string | null;
+  meta: PluginMeta;
+  sort: string;
+}
+
 export interface DataSourcesState {
 export interface DataSourcesState {
   dataSources: DataSource[];
   dataSources: DataSource[];
   searchQuery: string;
   searchQuery: string;

+ 4 - 2
public/app/types/index.ts

@@ -7,7 +7,7 @@ import { DashboardState } from './dashboard';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { Invitee, OrgUser, User, UsersState, UserState } from './user';
 import { Invitee, OrgUser, User, UsersState, UserState } from './user';
-import { DataSource, DataSourcesState } from './datasources';
+import { DataSource, DataSourceSelectItem, DataSourcesState } from './datasources';
 import {
 import {
   TimeRange,
   TimeRange,
   LoadingState,
   LoadingState,
@@ -21,7 +21,7 @@ import {
   DataQueryOptions,
   DataQueryOptions,
 } from './series';
 } from './series';
 import { PanelProps, PanelOptionsProps } from './panel';
 import { PanelProps, PanelOptionsProps } from './panel';
-import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
+import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins';
 import { Organization, OrganizationPreferences, OrganizationState } from './organization';
 import { Organization, OrganizationPreferences, OrganizationState } from './organization';
 import {
 import {
   AppNotification,
   AppNotification,
@@ -55,6 +55,7 @@ export {
   OrgRole,
   OrgRole,
   PermissionLevel,
   PermissionLevel,
   DataSource,
   DataSource,
+  DataSourceSelectItem,
   PluginMeta,
   PluginMeta,
   ApiKey,
   ApiKey,
   ApiKeysState,
   ApiKeysState,
@@ -68,6 +69,7 @@ export {
   UsersState,
   UsersState,
   TimeRange,
   TimeRange,
   LoadingState,
   LoadingState,
+  PanelPlugin,
   PanelProps,
   PanelProps,
   PanelOptionsProps,
   PanelOptionsProps,
   TimeSeries,
   TimeSeries,

+ 5 - 5
public/app/types/plugins.ts

@@ -12,14 +12,13 @@ export interface PluginExports {
   // Panel plugin
   // Panel plugin
   PanelCtrl?;
   PanelCtrl?;
   PanelComponent?: ComponentClass<PanelProps>;
   PanelComponent?: ComponentClass<PanelProps>;
-  PanelOptionsComponent: ComponentClass<PanelOptionsProps>;
+  PanelOptionsComponent?: ComponentClass<PanelOptionsProps>;
 }
 }
 
 
 export interface PanelPlugin {
 export interface PanelPlugin {
   id: string;
   id: string;
   name: string;
   name: string;
-  meta: any;
-  hideFromList: boolean;
+  hideFromList?: boolean;
   module: string;
   module: string;
   baseUrl: string;
   baseUrl: string;
   info: any;
   info: any;
@@ -49,7 +48,7 @@ export interface PluginInclude {
 export interface PluginMetaInfo {
 export interface PluginMetaInfo {
   author: {
   author: {
     name: string;
     name: string;
-    url: string;
+    url?: string;
   };
   };
   description: string;
   description: string;
   links: string[];
   links: string[];
@@ -57,7 +56,7 @@ export interface PluginMetaInfo {
     large: string;
     large: string;
     small: string;
     small: string;
   };
   };
-  screenshots: string;
+  screenshots: any[];
   updated: string;
   updated: string;
   version: string;
   version: string;
 }
 }
@@ -73,6 +72,7 @@ export interface Plugin {
   pinned: boolean;
   pinned: boolean;
   state: string;
   state: string;
   type: string;
   type: string;
+  module: any;
 }
 }
 
 
 export interface PluginDashboard {
 export interface PluginDashboard {

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

@@ -88,4 +88,5 @@ export interface DataQueryOptions {
 
 
 export interface DataSourceApi {
 export interface DataSourceApi {
   query(options: DataQueryOptions): Promise<DataQueryResponse>;
   query(options: DataQueryOptions): Promise<DataQueryResponse>;
+  testDatasource(): Promise<any>;
 }
 }

+ 133 - 0
public/app/viz/Gauge.tsx

@@ -0,0 +1,133 @@
+import React, { PureComponent } from 'react';
+import $ from 'jquery';
+import { withSize } from 'react-sizeme';
+import { TimeSeriesVMs } from 'app/types';
+import config from '../core/config';
+
+interface Props {
+  timeSeries: TimeSeriesVMs;
+  minValue: number;
+  maxValue: number;
+  showThresholdMarkers?: boolean;
+  thresholds?: number[];
+  showThresholdLables?: boolean;
+  size?: { width: number; height: number };
+}
+
+const colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
+
+export class Gauge extends PureComponent<Props> {
+  parentElement: any;
+  canvasElement: any;
+
+  static defaultProps = {
+    minValue: 0,
+    maxValue: 100,
+    showThresholdMarkers: true,
+    showThresholdLables: false,
+    thresholds: [],
+  };
+
+  componentDidMount() {
+    this.draw();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    this.draw();
+  }
+
+  draw() {
+    const { maxValue, minValue, showThresholdLables, size, showThresholdMarkers, timeSeries, thresholds } = this.props;
+
+    const width = size.width;
+    const height = size.height;
+    const dimension = Math.min(width, height * 1.3);
+
+    const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
+    const fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)';
+    const fontScale = parseInt('80', 10) / 100;
+    const fontSize = Math.min(dimension / 5, 100) * fontScale;
+    const gaugeWidth = Math.min(dimension / 6, 60);
+    const thresholdMarkersWidth = gaugeWidth / 5;
+    const thresholdLabelFontSize = fontSize / 2.5;
+
+    const formattedThresholds = [];
+
+    thresholds.forEach((threshold, index) => {
+      formattedThresholds.push({
+        value: threshold,
+        color: colors[index],
+      });
+    });
+
+    const options = {
+      series: {
+        gauges: {
+          gauge: {
+            min: minValue,
+            max: maxValue,
+            background: { color: backgroundColor },
+            border: { color: null },
+            shadow: { show: false },
+            width: gaugeWidth,
+          },
+          frame: { show: false },
+          label: { show: false },
+          layout: { margin: 0, thresholdWidth: 0 },
+          cell: { border: { width: 0 } },
+          threshold: {
+            values: formattedThresholds,
+            label: {
+              show: showThresholdLables,
+              margin: thresholdMarkersWidth + 1,
+              font: { size: thresholdLabelFontSize },
+            },
+            show: showThresholdMarkers,
+            width: thresholdMarkersWidth,
+          },
+          value: {
+            color: fontColor,
+            formatter: () => {
+              return Math.round(timeSeries[0].stats.avg);
+            },
+            font: {
+              size: fontSize,
+              family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
+            },
+          },
+          show: true,
+        },
+      },
+    };
+
+    const plotSeries = {
+      data: [[0, timeSeries[0].stats.avg]],
+    };
+
+    try {
+      $.plot(this.canvasElement, [plotSeries], options);
+    } catch (err) {
+      console.log('Gauge rendering error', err, options, timeSeries);
+    }
+  }
+
+  render() {
+    const { height, width } = this.props.size;
+
+    return (
+      <div className="singlestat-panel" ref={element => (this.parentElement = element)}>
+        <div
+          style={{
+            height: `${height * 0.9}px`,
+            width: `${Math.min(width, height * 1.3)}px`,
+            top: '10px',
+            margin: 'auto',
+          }}
+          ref={element => (this.canvasElement = element)}
+        />
+      </div>
+    );
+  }
+}
+
+export default withSize({ monitorHeight: true })(Gauge);

+ 16 - 0
public/app/viz/GaugeOptions.tsx

@@ -0,0 +1,16 @@
+import React, { PureComponent } from 'react';
+import { PanelOptionsProps } from '../types';
+
+interface Props {}
+
+export class GaugeOptions extends PureComponent<PanelOptionsProps<Props>> {
+  render() {
+    return (
+      <div>
+        <div className="section gf-form-group">
+          <h5 className="page-heading">Draw Modes</h5>
+        </div>
+      </div>
+    );
+  }
+}

+ 2 - 1
public/sass/_grafana.scss

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

+ 8 - 2
public/sass/_variables.dark.scss

@@ -77,6 +77,7 @@ $brand-gradient: linear-gradient(
   rgba(255, 68, 0, 0.7) 99%,
   rgba(255, 68, 0, 0.7) 99%,
   rgba(255, 68, 0, 0.7) 100%
   rgba(255, 68, 0, 0.7) 100%
 );
 );
+
 $page-gradient: linear-gradient(180deg, #222426 10px, rgb(22, 23, 25) 100px);
 $page-gradient: linear-gradient(180deg, #222426 10px, rgb(22, 23, 25) 100px);
 
 
 // Links
 // Links
@@ -110,7 +111,6 @@ $divider-border-color: #555;
 
 
 // Graphite Target Editor
 // Graphite Target Editor
 $tight-form-bg: $dark-3;
 $tight-form-bg: $dark-3;
-
 $tight-form-func-bg: #333334;
 $tight-form-func-bg: #333334;
 $tight-form-func-highlight-bg: #444445;
 $tight-form-func-highlight-bg: #444445;
 
 
@@ -128,6 +128,7 @@ $list-item-bg: $card-background;
 $list-item-hover-bg: lighten($gray-blue, 2%);
 $list-item-hover-bg: lighten($gray-blue, 2%);
 $list-item-link-color: $text-color;
 $list-item-link-color: $text-color;
 $list-item-shadow: $card-shadow;
 $list-item-shadow: $card-shadow;
+
 $empty-list-cta-bg: $gray-blue;
 $empty-list-cta-bg: $gray-blue;
 
 
 // Scrollbars
 // Scrollbars
@@ -152,8 +153,8 @@ $table-bg-hover: $dark-3;
 $btn-primary-bg: #ff6600;
 $btn-primary-bg: #ff6600;
 $btn-primary-bg-hl: #bc3e06;
 $btn-primary-bg-hl: #bc3e06;
 
 
-$btn-secondary-bg: $blue-dark;
 $btn-secondary-bg-hl: lighten($blue-dark, 5%);
 $btn-secondary-bg-hl: lighten($blue-dark, 5%);
+$btn-secondary-bg: $blue-dark;
 
 
 $btn-success-bg: $green;
 $btn-success-bg: $green;
 $btn-success-bg-hl: darken($green, 6%);
 $btn-success-bg-hl: darken($green, 6%);
@@ -266,6 +267,11 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 // -------------------------
 // -------------------------
 $tab-border-color: $dark-4;
 $tab-border-color: $dark-4;
 
 
+// Toolbar
+$toolbar-bg: $page-header-bg;
+$toolbar-shadow: 0 0 20px black;
+$toolbar-tab-bg: $gray-blue;
+
 // Pagination
 // Pagination
 // -------------------------
 // -------------------------
 
 

+ 6 - 0
public/sass/_variables.light.scss

@@ -31,6 +31,7 @@ $white: #fff;
 // Accent colors
 // Accent colors
 // -------------------------
 // -------------------------
 $blue: #0083b3;
 $blue: #0083b3;
+$blue-dark: #005f81;
 $blue-light: #00a8e6;
 $blue-light: #00a8e6;
 $green: #3aa655;
 $green: #3aa655;
 $red: #d44939;
 $red: #d44939;
@@ -213,6 +214,11 @@ $menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
 // -------------------------
 // -------------------------
 $tab-border-color: $gray-5;
 $tab-border-color: $gray-5;
 
 
+// Toolbar
+$toolbar-bg: linear-gradient(90deg, #ffffff, #e6eef9);
+$toolbar-shadow: 1px 1px 3px #c7d0d8;
+$toolbar-tab-bg: $white;
+
 // search
 // search
 $search-shadow: 0 5px 30px 0 $gray-4;
 $search-shadow: 0 5px 30px 0 $gray-4;
 $search-filter-box-bg: $gray-7;
 $search-filter-box-bg: $gray-7;

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

@@ -120,8 +120,8 @@
 // Info appears as a neutral blue
 // Info appears as a neutral blue
 .btn-secondary {
 .btn-secondary {
   @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl);
   @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl);
+  // Inverse appears as dark gray
 }
 }
-// Inverse appears as dark gray
 .btn-inverse {
 .btn-inverse {
   @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow);
   @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow);
   //background: $card-background;
   //background: $card-background;

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

@@ -3,7 +3,7 @@
 
 
 .panel-in-fullscreen {
 .panel-in-fullscreen {
   .react-grid-layout {
   .react-grid-layout {
-    height: 100% !important;
+    height: calc(100% - 20px) !important;
   }
   }
 
 
   .react-grid-item {
   .react-grid-item {
@@ -19,6 +19,10 @@
     transform: translate(0px, 0px) !important;
     transform: translate(0px, 0px) !important;
   }
   }
 
 
+  .panel {
+    margin: 0 !important;
+  }
+
   // Disable grid interaction indicators in fullscreen panels
   // Disable grid interaction indicators in fullscreen panels
   .panel-header:hover {
   .panel-header:hover {
     background-color: inherit;
     background-color: inherit;

+ 21 - 1
public/sass/components/_gf-form.scss

@@ -415,7 +415,27 @@ select.gf-form-input ~ .gf-form-help-icon {
 }
 }
 
 
 .cta-form__close {
 .cta-form__close {
+  background: transparent;
+  padding: 4px 8px 4px 9px;
+  border: none;
   position: absolute;
   position: absolute;
   right: 0;
   right: 0;
-  top: 0;
+  top: -2px;
+  font-size: $font-size-md;
+
+  &:hover {
+    color: $text-color-strong;
+  }
+}
+
+.cta-form__bar {
+  display: flex;
+  align-items: center;
+  align-content: center;
+  margin-bottom: 20px;
+}
+
+.cta-form__bar-header {
+  font-size: $font-size-h4;
+  padding-right: 20px;
 }
 }

+ 2 - 6
public/sass/components/_navbar.scss

@@ -41,7 +41,7 @@
 
 
 .panel-in-fullscreen {
 .panel-in-fullscreen {
   .navbar {
   .navbar {
-    @include navbar-alt-look();
+    padding-left: 15px;
   }
   }
 
 
   .navbar-button--add-panel,
   .navbar-button--add-panel,
@@ -50,10 +50,6 @@
   .navbar-page-btn .fa-caret-down {
   .navbar-page-btn .fa-caret-down {
     display: none;
     display: none;
   }
   }
-
-  .navbar-buttons--close {
-    display: flex;
-  }
 }
 }
 
 
 .navbar-page-btn {
 .navbar-page-btn {
@@ -98,7 +94,7 @@
 }
 }
 
 
 .navbar-buttons {
 .navbar-buttons {
-  height: $navbarHeight;
+  // height: $navbarHeight;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: flex-end;
   justify-content: flex-end;

+ 208 - 0
public/sass/components/_panel_editor.scss

@@ -0,0 +1,208 @@
+.panel-editor-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.panel-editor-container__panel {
+  flex: 1 1 0;
+}
+
+.panel-editor-container__editor {
+  margin-top: $panel-margin*2;
+  display: flex;
+  flex-direction: column;
+  height: 65%;
+  position: relative;
+}
+
+.panel-editor__scroll {
+  flex-grow: 1;
+  min-width: 0;
+  display: flex;
+  padding: 0 5px;
+}
+
+.panel-editor__content {
+  padding: 40px 15px;
+}
+
+.panel-in-fullscreen {
+  .sidemenu {
+    display: none;
+  }
+
+  .dashboard-container {
+    padding: 0;
+  }
+
+  .submenu-controls {
+    padding: 0 $dashboard-padding $panel-margin $dashboard-padding;
+  }
+
+  .panel-editor-container__panel {
+    margin: 0 $dashboard-padding;
+  }
+}
+
+.panel-editor-resizer {
+  position: absolute;
+  height: 2px;
+  width: 100%;
+  top: -23px;
+  text-align: center;
+  border-bottom: 2px dashed transparent;
+
+  &:hover {
+    transition: border-color 0.2s ease-in 0.4s;
+    transition-delay: 0.2s;
+    border-color: $text-color-faint;
+  }
+}
+
+.panel-editor-resizer__handle {
+  display: inline-block;
+  width: 180px;
+  position: relative;
+  border-radius: 2px;
+  height: 10px;
+  cursor: grabbing;
+  background: $input-label-bg;
+  top: -8px;
+
+  &:hover {
+    transition: background 0.2s ease-in 0.4s;
+    transition-delay: 0.2s;
+    background: linear-gradient(90deg, $orange, $red);
+    .panel-editor-resizer__handle-dots {
+      transition: opacity 0.2s ease-in;
+      opacity: 0;
+    }
+  }
+}
+
+.panel-editor-resizer__handle-dots {
+  border-top: 2px dashed $text-color-faint;
+  position: relative;
+  top: 4px;
+}
+
+.viz-picker {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 13px;
+}
+
+.viz-picker__item {
+  background: $card-background;
+  box-shadow: $card-shadow;
+
+  border-radius: 3px;
+  height: 90px;
+  width: 150px;
+  flex-shrink: 0;
+  flex-direction: column;
+  text-align: center;
+  cursor: pointer;
+  display: flex;
+  margin-right: 10px;
+  margin-bottom: 10px;
+  border: 1px solid transparent;
+  align-items: center;
+
+  &:hover {
+    background: $card-background-hover;
+  }
+
+  &--selected {
+    box-shadow: 0 0 12px #ff4d00;
+  }
+}
+
+.viz-picker__item-name {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  font-size: $font-size-sm;
+  display: flex;
+  flex-direction: column;
+  align-self: center;
+  height: 23px;
+}
+
+.viz-picker__item-img {
+  height: 55px;
+}
+
+.panel-editor-tabs {
+  position: relative;
+  z-index: 2;
+  box-shadow: $page-header-shadow;
+  border-bottom: 1px solid $page-header-border-color;
+  padding: 0 $dashboard-padding;
+
+  @include clearfix();
+
+  .active.gf-tabs-link {
+    background: $toolbar-tab-bg;
+  }
+}
+
+.panel-editor-tabs__close {
+  padding: 5px 9px;
+  border-radius: $border-radius;
+  float: right;
+  @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
+}
+
+.ds-picker-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: 13px;
+  flex-direction: column;
+}
+
+.ds-picker-list__item {
+  background: $card-background;
+  box-shadow: $card-shadow;
+
+  border-radius: 3px;
+  display: flex;
+  cursor: pointer;
+  margin-bottom: 3px;
+  padding: 5px 15px;
+  align-items: center;
+
+  &:hover {
+    background: $card-background-hover;
+  }
+
+  &--selected {
+    .ds-picker-list__name {
+      color: $text-color;
+    }
+  }
+}
+
+.ds-picker-list__name {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  font-size: $font-size-md;
+  padding-left: 15px;
+}
+
+.ds-picker-list__img {
+  width: 30px;
+}
+
+.form-option-box {
+  margin-bottom: 20px;
+}
+
+.form-option-box__header {
+  border-bottom: 2px solid $dark-4;
+  padding: 5px 0px;
+  font-size: $font-size-md;
+  margin-bottom: 20px;
+}

+ 71 - 71
public/sass/components/_scrollbar.scss

@@ -106,78 +106,78 @@
   opacity: 0.9;
   opacity: 0.9;
 }
 }
 
 
-// Scrollbars
+// // Scrollbars
+// //
+//
+// ::-webkit-scrollbar {
+//   width: 8px;
+//   height: 8px;
+// }
+//
+// ::-webkit-scrollbar:hover {
+//   height: 8px;
+// }
+//
+// ::-webkit-scrollbar-button:start:decrement,
+// ::-webkit-scrollbar-button:end:increment {
+//   display: none;
+// }
+// ::-webkit-scrollbar-button:horizontal:decrement {
+//   display: none;
+// }
+// ::-webkit-scrollbar-button:horizontal:increment {
+//   display: none;
+// }
+// ::-webkit-scrollbar-button:vertical:decrement {
+//   display: none;
+// }
+// ::-webkit-scrollbar-button:vertical:increment {
+//   display: none;
+// }
+// ::-webkit-scrollbar-button:horizontal:decrement:active {
+//   background-image: none;
+// }
+// ::-webkit-scrollbar-button:horizontal:increment:active {
+//   background-image: none;
+// }
+// ::-webkit-scrollbar-button:vertical:decrement:active {
+//   background-image: none;
+// }
+// ::-webkit-scrollbar-button:vertical:increment:active {
+//   background-image: none;
+// }
+// ::-webkit-scrollbar-track-piece {
+//   background-color: transparent;
+// }
+//
+// ::-webkit-scrollbar-thumb:vertical {
+//   height: 50px;
+//   background: -webkit-gradient(
+//     linear,
+//     left top,
+//     right top,
+//     color-stop(0%, $scrollbarBackground),
+//     color-stop(100%, $scrollbarBackground2)
+//   );
+//   border: 1px solid $scrollbarBorder;
+//   border-top: 1px solid $scrollbarBorder;
+//   border-left: 1px solid $scrollbarBorder;
+// }
+//
+// ::-webkit-scrollbar-thumb:horizontal {
+//   width: 50px;
+//   background: -webkit-gradient(
+//     linear,
+//     left top,
+//     left bottom,
+//     color-stop(0%, $scrollbarBackground),
+//     color-stop(100%, $scrollbarBackground2)
+//   );
+//   border: 1px solid $scrollbarBorder;
+//   border-top: 1px solid $scrollbarBorder;
+//   border-left: 1px solid $scrollbarBorder;
+// }
 //
 //
-
-::-webkit-scrollbar {
-  width: 8px;
-  height: 8px;
-}
-
-::-webkit-scrollbar:hover {
-  height: 8px;
-}
-
-::-webkit-scrollbar-button:start:decrement,
-::-webkit-scrollbar-button:end:increment {
-  display: none;
-}
-::-webkit-scrollbar-button:horizontal:decrement {
-  display: none;
-}
-::-webkit-scrollbar-button:horizontal:increment {
-  display: none;
-}
-::-webkit-scrollbar-button:vertical:decrement {
-  display: none;
-}
-::-webkit-scrollbar-button:vertical:increment {
-  display: none;
-}
-::-webkit-scrollbar-button:horizontal:decrement:active {
-  background-image: none;
-}
-::-webkit-scrollbar-button:horizontal:increment:active {
-  background-image: none;
-}
-::-webkit-scrollbar-button:vertical:decrement:active {
-  background-image: none;
-}
-::-webkit-scrollbar-button:vertical:increment:active {
-  background-image: none;
-}
-::-webkit-scrollbar-track-piece {
-  background-color: transparent;
-}
-
-::-webkit-scrollbar-thumb:vertical {
-  height: 50px;
-  background: -webkit-gradient(
-    linear,
-    left top,
-    right top,
-    color-stop(0%, $scrollbarBackground),
-    color-stop(100%, $scrollbarBackground2)
-  );
-  border: 1px solid $scrollbarBorder;
-  border-top: 1px solid $scrollbarBorder;
-  border-left: 1px solid $scrollbarBorder;
-}
-
-::-webkit-scrollbar-thumb:horizontal {
-  width: 50px;
-  background: -webkit-gradient(
-    linear,
-    left top,
-    left bottom,
-    color-stop(0%, $scrollbarBackground),
-    color-stop(100%, $scrollbarBackground2)
-  );
-  border: 1px solid $scrollbarBorder;
-  border-top: 1px solid $scrollbarBorder;
-  border-left: 1px solid $scrollbarBorder;
-}
-
 // Baron styles
 // Baron styles
 
 
 .baron {
 .baron {

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

@@ -4,8 +4,7 @@
   flex-wrap: wrap;
   flex-wrap: wrap;
   align-content: flex-start;
   align-content: flex-start;
   align-items: flex-start;
   align-items: flex-start;
-
-  margin: 0 0 $panel-margin 0;
+  padding: 0 0 $panel-margin 0;
 }
 }
 
 
 .annotation-disabled,
 .annotation-disabled,

+ 4 - 2
public/sass/components/_tabbed_view.scss

@@ -2,9 +2,10 @@
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   height: 100%;
   height: 100%;
+  flex-grow: 1;
 
 
   &.tabbed-view--new {
   &.tabbed-view--new {
-    padding: 25px 0 0 0;
+    padding: 0 0 0 0;
     height: 100%;
     height: 100%;
   }
   }
 }
 }
@@ -12,13 +13,14 @@
 .tabbed-view-header {
 .tabbed-view-header {
   box-shadow: $page-header-shadow;
   box-shadow: $page-header-shadow;
   border-bottom: 1px solid $page-header-border-color;
   border-bottom: 1px solid $page-header-border-color;
+  padding: 0 $dashboard-padding;
   @include clearfix();
   @include clearfix();
 }
 }
 
 
 .tabbed-view-title {
 .tabbed-view-title {
   float: left;
   float: left;
   padding-top: 0.5rem;
   padding-top: 0.5rem;
-  margin: 0 $spacer*3 0 $spacer*1;
+  margin: 0 $spacer*3 0 0;
 }
 }
 
 
 .tabbed-view-panel-title {
 .tabbed-view-panel-title {

+ 1 - 0
public/sass/components/_tabs.scss

@@ -53,6 +53,7 @@
       background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
       background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%);
     }
     }
   }
   }
+
   &.active--panel {
   &.active--panel {
     background: $panel-bg !important;
     background: $panel-bg !important;
   }
   }

+ 1 - 0
public/sass/components/_timepicker.scss

@@ -20,6 +20,7 @@
   background-color: $page-bg;
   background-color: $page-bg;
   border-radius: 0 0 0 4px;
   border-radius: 0 0 0 4px;
   box-shadow: $search-shadow;
   box-shadow: $search-shadow;
+  z-index: $zindex-dropdown;
 }
 }
 
 
 .gf-timepicker-absolute-section {
 .gf-timepicker-absolute-section {

+ 59 - 0
public/sass/components/_toolbar.scss

@@ -0,0 +1,59 @@
+.toolbar {
+  display: flex;
+  align-content: center;
+  align-items: center;
+  background: $toolbar-bg;
+  box-shadow: $toolbar-shadow;
+  padding: 7px 20px 7px 20px;
+  position: relative;
+  z-index: 1;
+  flex: 0 0 auto;
+}
+
+.toolbar__main {
+  padding: $input-padding-y $input-padding-x;
+  font-size: $font-size-md;
+  line-height: $input-line-height;
+  color: $input-color;
+  background-color: $input-bg;
+  border: $input-border;
+  border-radius: $input-border-radius;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+
+  .fa {
+    margin-left: 20px;
+    display: inline-block;
+    position: relative;
+  }
+}
+
+.toolbar__main-image {
+  margin-right: 10px;
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+}
+
+.toolbar-subview {
+  position: relative;
+  padding: 20px 20px;
+  background-color: $empty-list-cta-bg;
+  top: -45px;
+  margin: 0 30px 20px 0px;
+}
+
+.toolbar-subview__close {
+  background: transparent;
+  padding: 4px 8px 4px 9px;
+  border: none;
+  position: absolute;
+  right: 15px;
+  top: 20px;
+  font-size: $font-size-md;
+
+  &:hover {
+    color: $text-color-strong;
+  }
+}

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

@@ -1,81 +0,0 @@
-.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%);
-}

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

@@ -37,20 +37,6 @@ div.flot-text {
   height: 100%;
   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 {
 .panel-container {
   background-color: $panel-bg;
   background-color: $panel-bg;
   border: $panel-border;
   border: $panel-border;

+ 19 - 18
public/vendor/flot/jquery.flot.gauge.js

@@ -583,30 +583,31 @@
          * @param  {Number} [a] the angle of the value drawn
          * @param  {Number} [a] the angle of the value drawn
          */
          */
         function drawText(x, y, id, text, textOptions, a) {
         function drawText(x, y, id, text, textOptions, a) {
-            var span = $("." + id, placeholder);
+            var span = $(placeholder).find("#" + id);
             var exists = span.length;
             var exists = span.length;
             if (!exists) {
             if (!exists) {
                 span = $("<span></span>")
                 span = $("<span></span>")
                 span.attr("id", id);
                 span.attr("id", id);
-                span.css("position", "absolute");
-                span.css("top", y + "px");
-                if (textOptions.font.size) {
-                    span.css("font-size", textOptions.font.size + "px");
-                }
-                if (textOptions.font.family) {
-                    span.css("font-family", textOptions.font.family);
-                }
-                if (textOptions.color) {
-                    span.css("color", textOptions.color);
-                }
-                if (textOptions.background.color) {
-                    span.css("background-color", textOptions.background.color);
-                }
-                if (textOptions.background.opacity) {
-                    span.css("opacity", textOptions.background.opacity);
-                }
                 placeholder.append(span);
                 placeholder.append(span);
             }
             }
+
+            span.css("position", "absolute");
+            span.css("top", y + "px");
+            if (textOptions.font.size) {
+              span.css("font-size", textOptions.font.size + "px");
+            }
+            if (textOptions.font.family) {
+              span.css("font-family", textOptions.font.family);
+            }
+            if (textOptions.color) {
+              span.css("color", textOptions.color);
+            }
+            if (textOptions.background.color) {
+              span.css("background-color", textOptions.background.color);
+            }
+            if (textOptions.background.opacity) {
+              span.css("opacity", textOptions.background.opacity);
+            }
             span.text(text);
             span.text(text);
             // after append, readjust the left position
             // after append, readjust the left position
             span.css("left", x + "px"); // for redraw, resetting the left position is needed here
             span.css("left", x + "px"); // for redraw, resetting the left position is needed here

+ 32 - 36
scripts/webpack/webpack.hot.js

@@ -4,22 +4,19 @@ const merge = require('webpack-merge');
 const common = require('./webpack.common.js');
 const common = require('./webpack.common.js');
 const path = require('path');
 const path = require('path');
 const webpack = require('webpack');
 const webpack = require('webpack');
-const HtmlWebpackPlugin = require("html-webpack-plugin");
+const HtmlWebpackPlugin = require('html-webpack-plugin');
 const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 
 
 module.exports = merge(common, {
 module.exports = merge(common, {
   entry: {
   entry: {
-    app: [
-      'webpack-dev-server/client?http://localhost:3333',
-      './public/app/dev.ts',
-    ],
+    app: ['webpack-dev-server/client?http://localhost:3333', './public/app/dev.ts'],
   },
   },
 
 
   output: {
   output: {
     path: path.resolve(__dirname, '../../public/build'),
     path: path.resolve(__dirname, '../../public/build'),
     filename: '[name].[hash].js',
     filename: '[name].[hash].js',
-    publicPath: "/public/build/",
+    publicPath: '/public/build/',
     pathinfo: false,
     pathinfo: false,
   },
   },
 
 
@@ -34,8 +31,8 @@ module.exports = merge(common, {
     hot: true,
     hot: true,
     port: 3333,
     port: 3333,
     proxy: {
     proxy: {
-      '!/public/build': 'http://localhost:3000'
-    }
+      '!/public/build': 'http://localhost:3000',
+    },
   },
   },
 
 
   optimization: {
   optimization: {
@@ -49,38 +46,37 @@ module.exports = merge(common, {
       {
       {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
         exclude: /node_modules/,
         exclude: /node_modules/,
-        use: [{
-          loader: 'babel-loader',
-          options: {
-            cacheDirectory: true,
-            babelrc: false,
-            plugins: [
-              'syntax-dynamic-import',
-              'react-hot-loader/babel'
-            ]
-          }
-        },
-        {
-          loader: 'ts-loader',
-          options: {
-            transpileOnly: true,
-            experimentalWatchApi: true
+        use: [
+          {
+            loader: 'babel-loader',
+            options: {
+              cacheDirectory: true,
+              babelrc: false,
+              plugins: ['syntax-dynamic-import', 'react-hot-loader/babel'],
+            },
           },
           },
-        }],
+          {
+            loader: 'ts-loader',
+            options: {
+              transpileOnly: true,
+              experimentalWatchApi: true,
+            },
+          },
+        ],
       },
       },
       {
       {
         test: /\.scss$/,
         test: /\.scss$/,
         use: [
         use: [
-          "style-loader", // creates style nodes from JS strings
-          "css-loader", // translates CSS into CommonJS
-          "sass-loader" // compiles Sass to CSS
-        ]
+          'style-loader', // creates style nodes from JS strings
+          'css-loader', // translates CSS into CommonJS
+          'sass-loader', // compiles Sass to CSS
+        ],
       },
       },
       {
       {
         test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
         test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
-        loader: 'file-loader'
+        loader: 'file-loader',
       },
       },
-    ]
+    ],
   },
   },
 
 
   plugins: [
   plugins: [
@@ -89,16 +85,16 @@ module.exports = merge(common, {
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index-template.html'),
       template: path.resolve(__dirname, '../../public/views/index-template.html'),
       inject: 'body',
       inject: 'body',
-      alwaysWriteToDisk: true
+      alwaysWriteToDisk: true,
     }),
     }),
     new HtmlWebpackHarddiskPlugin(),
     new HtmlWebpackHarddiskPlugin(),
     new webpack.NamedModulesPlugin(),
     new webpack.NamedModulesPlugin(),
     new webpack.HotModuleReplacementPlugin(),
     new webpack.HotModuleReplacementPlugin(),
     new webpack.DefinePlugin({
     new webpack.DefinePlugin({
-      'GRAFANA_THEME': JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
+      GRAFANA_THEME: JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
       'process.env': {
       'process.env': {
-        'NODE_ENV': JSON.stringify('development')
-      }
+        NODE_ENV: JSON.stringify('development'),
+      },
     }),
     }),
-  ]
+  ],
 });
 });