Explorar el Código

Merge remote-tracking branch 'origin/data-source-settings-to-react' into develop

Torkel Ödegaard hace 7 años
padre
commit
e0feb72637
Se han modificado 39 ficheros con 1099 adiciones y 451 borrados
  1. 1 0
      .gitignore
  2. 4 0
      public/app/core/services/AngularLoader.ts
  3. 1 1
      public/app/core/services/bridge_srv.ts
  4. 1 1
      public/app/core/utils/connectWithReduxStore.tsx
  5. 1 1
      public/app/features/dashboard/dashgrid/PanelEditor.tsx
  6. 1 1
      public/app/features/dashboard/utils/getPanelMenu.ts
  7. 0 125
      public/app/features/datasources/DataSourceSettings.tsx
  8. 3 0
      public/app/features/datasources/__mocks__/dataSourcesMocks.ts
  9. 20 0
      public/app/features/datasources/settings/BasicSettings.test.tsx
  10. 34 0
      public/app/features/datasources/settings/BasicSettings.tsx
  11. 31 0
      public/app/features/datasources/settings/ButtonRow.test.tsx
  12. 25 0
      public/app/features/datasources/settings/ButtonRow.tsx
  13. 63 0
      public/app/features/datasources/settings/DataSourceSettings.test.tsx
  14. 245 0
      public/app/features/datasources/settings/DataSourceSettings.tsx
  15. 63 0
      public/app/features/datasources/settings/PluginSettings.tsx
  16. 25 0
      public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap
  17. 59 0
      public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap
  18. 379 0
      public/app/features/datasources/settings/__snapshots__/DataSourceSettings.test.tsx.snap
  19. 54 13
      public/app/features/datasources/state/actions.ts
  20. 3 0
      public/app/features/datasources/state/navModel.ts
  21. 4 1
      public/app/features/datasources/state/reducers.ts
  22. 9 1
      public/app/features/datasources/state/selectors.ts
  23. 2 0
      public/app/features/plugins/__mocks__/pluginMocks.ts
  24. 6 0
      public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap
  25. 0 1
      public/app/features/plugins/all.ts
  26. 1 1
      public/app/features/plugins/ds_dashboards_ctrl.ts
  27. 1 1
      public/app/features/plugins/ds_edit_ctrl.ts
  28. 0 72
      public/app/features/plugins/partials/ds_edit.html
  29. 8 0
      public/app/features/plugins/plugin_component.ts
  30. 0 179
      public/app/features/plugins/plugin_edit_ctrl.ts
  31. 3 8
      public/app/plugins/datasource/cloudwatch/datasource.ts
  32. 1 1
      public/app/routes/ReactContainer.tsx
  33. 6 4
      public/app/routes/routes.ts
  34. 3 4
      public/app/store/configureStore.ts
  35. 5 0
      public/app/store/store.ts
  36. 3 0
      public/app/types/datasources.ts
  37. 1 0
      public/app/types/plugins.ts
  38. 1 0
      public/app/types/series.ts
  39. 32 36
      scripts/webpack/webpack.hot.js

+ 1 - 0
.gitignore

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

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

@@ -4,6 +4,7 @@ import _ from 'lodash';
 
 export interface AngularComponent {
   destroy();
+  digest();
 }
 
 export class AngularLoader {
@@ -24,6 +25,9 @@ export class AngularLoader {
         scope.$destroy();
         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 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 { updateLocation } from 'app/core/actions';
 

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

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

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelEditor.tsx

@@ -4,7 +4,7 @@ import classNames from 'classnames';
 import { QueriesTab } from './QueriesTab';
 import { VizTypePicker } from './VizTypePicker';
 
-import { store } from 'app/store/configureStore';
+import { store } from 'app/store/store';
 import { updateLocation } from 'app/core/actions';
 
 import { PanelModel } from '../panel_model';

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

@@ -1,5 +1,5 @@
 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 { 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 {
     access: '',
     basicAuth: false,
+    basicAuthUser: '',
+    basicAuthPassword: '',
+    withCredentials: false,
     database: '',
     id: 13,
     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>
+`;

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

@@ -0,0 +1,379 @@
+// 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": "screenshot/1",
+                "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": "screenshot/1",
+                "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": "screenshot/1",
+                "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": "screenshot/1",
+                "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 { 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 { DataSource, Plugin, StoreState } from 'app/types';
 
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
@@ -14,43 +16,49 @@ export enum ActionTypes {
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
   SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
   SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
+  SetDataSourceName = 'SET_DATA_SOURCE_NAME',
 }
 
-export interface LoadDataSourcesAction {
+interface LoadDataSourcesAction {
   type: ActionTypes.LoadDataSources;
   payload: DataSource[];
 }
 
-export interface SetDataSourcesSearchQueryAction {
+interface SetDataSourcesSearchQueryAction {
   type: ActionTypes.SetDataSourcesSearchQuery;
   payload: string;
 }
 
-export interface SetDataSourcesLayoutModeAction {
+interface SetDataSourcesLayoutModeAction {
   type: ActionTypes.SetDataSourcesLayoutMode;
   payload: LayoutMode;
 }
 
-export interface LoadDataSourceTypesAction {
+interface LoadDataSourceTypesAction {
   type: ActionTypes.LoadDataSourceTypes;
   payload: Plugin[];
 }
 
-export interface SetDataSourceTypeSearchQueryAction {
+interface SetDataSourceTypeSearchQueryAction {
   type: ActionTypes.SetDataSourceTypeSearchQuery;
   payload: string;
 }
 
-export interface LoadDataSourceAction {
+interface LoadDataSourceAction {
   type: ActionTypes.LoadDataSource;
   payload: DataSource;
 }
 
-export interface LoadDataSourceMetaAction {
+interface LoadDataSourceMetaAction {
   type: ActionTypes.LoadDataSourceMeta;
   payload: Plugin;
 }
 
+interface SetDataSourceNameAction {
+  type: ActionTypes.SetDataSourceName;
+  payload: string;
+}
+
 const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
   type: ActionTypes.LoadDataSources,
   payload: dataSources,
@@ -86,6 +94,11 @@ export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSe
   payload: query,
 });
 
+export const setDataSourceName = (name: string) => ({
+  type: ActionTypes.SetDataSourceName,
+  payload: name,
+});
+
 export type Action =
   | LoadDataSourcesAction
   | SetDataSourcesSearchQueryAction
@@ -95,7 +108,8 @@ export type Action =
   | SetDataSourceTypeSearchQueryAction
   | LoadDataSourceAction
   | UpdateNavIndexAction
-  | LoadDataSourceMetaAction;
+  | LoadDataSourceMetaAction
+  | SetDataSourceNameAction;
 
 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) {
   return (
     dataSources.filter(dataSource => {
@@ -173,6 +204,16 @@ export function findNewName(dataSources, 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) {
   return name.endsWith('-', name.length - 1);
 }

+ 3 - 0
public/app/features/datasources/state/navModel.ts

@@ -48,6 +48,9 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
     {
       access: '',
       basicAuth: false,
+      basicAuthUser: '',
+      basicAuthPassword: '',
+      withCredentials: false,
       database: '',
       id: 1,
       isDefault: false,

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

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

+ 2 - 0
public/app/features/plugins/__mocks__/pluginMocks.ts

@@ -26,6 +26,7 @@ export const getMockPlugins = (amount: number): Plugin[] => {
       pinned: false,
       state: '',
       type: '',
+      module: {},
     });
   }
 
@@ -55,5 +56,6 @@ export const getMockPlugin = () => {
     pinned: false,
     state: '',
     type: '',
+    module: {},
   };
 };

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

@@ -33,6 +33,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.0",
+          "module": Object {},
           "name": "pretty cool plugin-0",
           "pinned": false,
           "state": "",
@@ -66,6 +67,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.1",
+          "module": Object {},
           "name": "pretty cool plugin-1",
           "pinned": false,
           "state": "",
@@ -99,6 +101,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.2",
+          "module": Object {},
           "name": "pretty cool plugin-2",
           "pinned": false,
           "state": "",
@@ -132,6 +135,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.3",
+          "module": Object {},
           "name": "pretty cool plugin-3",
           "pinned": false,
           "state": "",
@@ -165,6 +169,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.4",
+          "module": Object {},
           "name": "pretty cool plugin-4",
           "pinned": false,
           "state": "",
@@ -198,6 +203,7 @@ exports[`Render should render component 1`] = `
             "version": "1",
           },
           "latestVersion": "1.5",
+          "module": Object {},
           "name": "pretty cool plugin-5",
           "pinned": false,
           "state": "",

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

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

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

@@ -1,5 +1,5 @@
 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 { buildNavModel } from './state/navModel';
 

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

@@ -1,7 +1,7 @@
 import _ from 'lodash';
 import config from 'app/core/config';
 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 { 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>

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

@@ -149,6 +149,14 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
             return { notFound: true };
           }
 
+          scope.$watch(
+            'ctrl.current',
+            () => {
+              scope.onModelChanged(scope.ctrl.current);
+            },
+            true
+          );
+
           return {
             baseUrl: dsMeta.baseUrl,
             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 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) {

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

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
 
 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 { DatasourceSrv } from 'app/features/plugins/datasource_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 UsersListPage from 'app/features/users/UsersListPage';
 import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
+import DataSourceSettings from '../features/datasources/settings/DataSourceSettings';
 import OrgDetailsPage from '../features/org/OrgDetailsPage';
 
 /** @ngInject */
@@ -74,10 +75,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
         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', {
       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 usersReducers from 'app/features/users/state/reducers';
 import organizationReducers from 'app/features/org/state/reducers';
+import { setStore } from './store';
 
 const rootReducers = {
   ...sharedReducers,
@@ -25,8 +26,6 @@ const rootReducers = {
   ...organizationReducers,
 };
 
-export let store;
-
 export function addRootReducer(reducers) {
   Object.assign(rootReducers, ...reducers);
 }
@@ -38,8 +37,8 @@ export function configureStore() {
 
   if (process.env.NODE_ENV !== 'production') {
     // DEV builds we had the logger middleware
-    store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())));
+    setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
   } 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;
+}

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

@@ -13,9 +13,12 @@ export interface DataSource {
   user: string;
   database: string;
   basicAuth: boolean;
+  basicAuthPassword: string;
+  basicAuthUser: string;
   isDefault: boolean;
   jsonData: { authType: string; defaultRegion: string };
   readOnly: boolean;
+  withCredentials: boolean;
   meta?: PluginMeta;
   pluginExports?: PluginExports;
   init?: () => void;

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

@@ -73,6 +73,7 @@ export interface Plugin {
   pinned: boolean;
   state: string;
   type: string;
+  module: any;
 }
 
 export interface PluginDashboard {

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

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

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

@@ -4,22 +4,19 @@ const merge = require('webpack-merge');
 const common = require('./webpack.common.js');
 const path = require('path');
 const webpack = require('webpack');
-const HtmlWebpackPlugin = require("html-webpack-plugin");
+const HtmlWebpackPlugin = require('html-webpack-plugin');
 const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 
 module.exports = merge(common, {
   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: {
     path: path.resolve(__dirname, '../../public/build'),
     filename: '[name].[hash].js',
-    publicPath: "/public/build/",
+    publicPath: '/public/build/',
     pathinfo: false,
   },
 
@@ -34,8 +31,8 @@ module.exports = merge(common, {
     hot: true,
     port: 3333,
     proxy: {
-      '!/public/build': 'http://localhost:3000'
-    }
+      '!/public/build': 'http://localhost:3000',
+    },
   },
 
   optimization: {
@@ -49,38 +46,37 @@ module.exports = merge(common, {
       {
         test: /\.tsx?$/,
         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$/,
         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=&.]+)?$/,
-        loader: 'file-loader'
+        loader: 'file-loader',
       },
-    ]
+    ],
   },
 
   plugins: [
@@ -89,16 +85,16 @@ module.exports = merge(common, {
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index-template.html'),
       inject: 'body',
-      alwaysWriteToDisk: true
+      alwaysWriteToDisk: true,
     }),
     new HtmlWebpackHarddiskPlugin(),
     new webpack.NamedModulesPlugin(),
     new webpack.HotModuleReplacementPlugin(),
     new webpack.DefinePlugin({
-      'GRAFANA_THEME': JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
+      GRAFANA_THEME: JSON.stringify(process.env.GRAFANA_THEME || 'dark'),
       'process.env': {
-        'NODE_ENV': JSON.stringify('development')
-      }
+        NODE_ENV: JSON.stringify('development'),
+      },
     }),
-  ]
+  ],
 });