Przeglądaj źródła

Merge branch 'data-sources-list-to-react'

Torkel Ödegaard 7 lat temu
rodzic
commit
f37a60dcd5
23 zmienionych plików z 831 dodań i 129 usunięć
  1. 22 0
      public/app/features/datasources/DataSourceList.test.tsx
  2. 23 0
      public/app/features/datasources/DataSourcesActionBar.test.tsx
  3. 62 0
      public/app/features/datasources/DataSourcesActionBar.tsx
  4. 34 0
      public/app/features/datasources/DataSourcesList.tsx
  5. 20 0
      public/app/features/datasources/DataSourcesListItem.test.tsx
  6. 35 0
      public/app/features/datasources/DataSourcesListItem.tsx
  7. 37 0
      public/app/features/datasources/DataSourcesListPage.test.tsx
  8. 76 0
      public/app/features/datasources/DataSourcesListPage.tsx
  9. 45 0
      public/app/features/datasources/__mocks__/dataSourcesMocks.ts
  10. 108 0
      public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap
  11. 42 0
      public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap
  12. 45 0
      public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap
  13. 164 0
      public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap
  14. 51 0
      public/app/features/datasources/state/actions.ts
  15. 29 0
      public/app/features/datasources/state/reducers.ts
  16. 11 0
      public/app/features/datasources/state/selectors.ts
  17. 0 1
      public/app/features/plugins/all.ts
  18. 0 61
      public/app/features/plugins/ds_list_ctrl.ts
  19. 0 63
      public/app/features/plugins/partials/ds_list.html
  20. 5 3
      public/app/routes/routes.ts
  21. 2 0
      public/app/store/configureStore.ts
  22. 18 0
      public/app/types/datasources.ts
  23. 2 1
      public/app/types/index.ts

+ 22 - 0
public/app/features/datasources/DataSourceList.test.tsx

@@ -0,0 +1,22 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import DataSourcesList from './DataSourcesList';
+import { getMockDataSources } from './__mocks__/dataSourcesMocks';
+import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
+
+const setup = () => {
+  const props = {
+    dataSources: getMockDataSources(3),
+    layoutMode: LayoutModes.Grid,
+  };
+
+  return shallow(<DataSourcesList {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 23 - 0
public/app/features/datasources/DataSourcesActionBar.test.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DataSourcesActionBar, Props } from './DataSourcesActionBar';
+import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    layoutMode: LayoutModes.Grid,
+    searchQuery: '',
+    setDataSourcesLayoutMode: jest.fn(),
+    setDataSourcesSearchQuery: jest.fn(),
+  };
+
+  return shallow(<DataSourcesActionBar {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 62 - 0
public/app/features/datasources/DataSourcesActionBar.tsx

@@ -0,0 +1,62 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
+import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
+import { getDataSourcesLayoutMode, getDataSourcesSearchQuery } from './state/selectors';
+
+export interface Props {
+  searchQuery: string;
+  layoutMode: LayoutMode;
+  setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode;
+  setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery;
+}
+
+export class DataSourcesActionBar extends PureComponent<Props> {
+  onSearchQueryChange = event => {
+    this.props.setDataSourcesSearchQuery(event.target.value);
+  };
+
+  render() {
+    const { searchQuery, layoutMode, setDataSourcesLayoutMode } = this.props;
+
+    return (
+      <div className="page-action-bar">
+        <div className="gf-form gf-form--grow">
+          <label className="gf-form--has-input-icon">
+            <input
+              type="text"
+              className="gf-form-input width-20"
+              value={searchQuery}
+              onChange={this.onSearchQueryChange}
+              placeholder="Filter by name or type"
+            />
+            <i className="gf-form-input-icon fa fa-search" />
+          </label>
+          <LayoutSelector
+            mode={layoutMode}
+            onLayoutModeChanged={(mode: LayoutMode) => setDataSourcesLayoutMode(mode)}
+          />
+        </div>
+        <div className="page-action-bar__spacer" />
+        <a className="page-header__cta btn btn-success" href="datasources/new">
+          <i className="fa fa-plus" />
+          Add data source
+        </a>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    searchQuery: getDataSourcesSearchQuery(state.dataSources),
+    layoutMode: getDataSourcesLayoutMode(state.dataSources),
+  };
+}
+
+const mapDispatchToProps = {
+  setDataSourcesLayoutMode,
+  setDataSourcesSearchQuery,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(DataSourcesActionBar);

+ 34 - 0
public/app/features/datasources/DataSourcesList.tsx

@@ -0,0 +1,34 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames/bind';
+import DataSourcesListItem from './DataSourcesListItem';
+import { DataSource } from 'app/types';
+import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
+
+export interface Props {
+  dataSources: DataSource[];
+  layoutMode: LayoutMode;
+}
+
+export class DataSourcesList extends PureComponent<Props> {
+  render() {
+    const { dataSources, layoutMode } = this.props;
+
+    const listStyle = classNames({
+      'card-section': true,
+      'card-list-layout-grid': layoutMode === LayoutModes.Grid,
+      'card-list-layout-list': layoutMode === LayoutModes.List,
+    });
+
+    return (
+      <section className={listStyle}>
+        <ol className="card-list">
+          {dataSources.map((dataSource, index) => {
+            return <DataSourcesListItem dataSource={dataSource} key={`${dataSource.id}-${index}`} />;
+          })}
+        </ol>
+      </section>
+    );
+  }
+}
+
+export default DataSourcesList;

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

@@ -0,0 +1,20 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import DataSourcesListItem from './DataSourcesListItem';
+import { getMockDataSource } from './__mocks__/dataSourcesMocks';
+
+const setup = () => {
+  const props = {
+    dataSource: getMockDataSource(),
+  };
+
+  return shallow(<DataSourcesListItem {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 35 - 0
public/app/features/datasources/DataSourcesListItem.tsx

@@ -0,0 +1,35 @@
+import React, { PureComponent } from 'react';
+import { DataSource } from 'app/types';
+
+export interface Props {
+  dataSource: DataSource;
+}
+
+export class DataSourcesListItem extends PureComponent<Props> {
+  render() {
+    const { dataSource } = this.props;
+    return (
+      <li className="card-item-wrapper">
+        <a className="card-item" href={`datasources/edit/${dataSource.id}`}>
+          <div className="card-item-header">
+            <div className="card-item-type">{dataSource.type}</div>
+          </div>
+          <div className="card-item-body">
+            <figure className="card-item-figure">
+              <img src={dataSource.typeLogoUrl} />
+            </figure>
+            <div className="card-item-details">
+              <div className="card-item-name">
+                {dataSource.name}
+                {dataSource.isDefault && <span className="btn btn-secondary btn-mini">default</span>}
+              </div>
+              <div className="card-item-sub-name">{dataSource.url}</div>
+            </div>
+          </div>
+        </a>
+      </li>
+    );
+  }
+}
+
+export default DataSourcesListItem;

+ 37 - 0
public/app/features/datasources/DataSourcesListPage.test.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DataSourcesListPage, Props } from './DataSourcesListPage';
+import { DataSource, NavModel } from 'app/types';
+import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
+import { getMockDataSources } from './__mocks__/dataSourcesMocks';
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    dataSources: [] as DataSource[],
+    layoutMode: LayoutModes.Grid,
+    loadDataSources: jest.fn(),
+    navModel: {} as NavModel,
+    dataSourcesCount: 0,
+  };
+
+  Object.assign(props, propOverrides);
+
+  return shallow(<DataSourcesListPage {...props} />);
+};
+
+describe('Render', () => {
+  it('should render component', () => {
+    const wrapper = setup();
+
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  it('should render action bar and datasources', () => {
+    const wrapper = setup({
+      dataSources: getMockDataSources(5),
+      dataSourcesCount: 5,
+    });
+
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 76 - 0
public/app/features/datasources/DataSourcesListPage.tsx

@@ -0,0 +1,76 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { hot } from 'react-hot-loader';
+import PageHeader from '../../core/components/PageHeader/PageHeader';
+import DataSourcesActionBar from './DataSourcesActionBar';
+import DataSourcesList from './DataSourcesList';
+import { loadDataSources } from './state/actions';
+import { getDataSources, getDataSourcesCount, getDataSourcesLayoutMode } from './state/selectors';
+import { getNavModel } from '../../core/selectors/navModel';
+import { DataSource, NavModel } from 'app/types';
+import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
+import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
+
+export interface Props {
+  navModel: NavModel;
+  dataSources: DataSource[];
+  dataSourcesCount: number;
+  layoutMode: LayoutMode;
+  loadDataSources: typeof loadDataSources;
+}
+
+const emptyListModel = {
+  title: 'There are no data sources defined yet',
+  buttonIcon: 'gicon gicon-add-datasources',
+  buttonLink: 'datasources/new',
+  buttonTitle: 'Add data source',
+  proTip: 'You can also define data sources through configuration files.',
+  proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
+  proTipLinkTitle: 'Learn more',
+  proTipTarget: '_blank',
+};
+
+export class DataSourcesListPage extends PureComponent<Props> {
+  componentDidMount() {
+    this.fetchDataSources();
+  }
+
+  async fetchDataSources() {
+    return await this.props.loadDataSources();
+  }
+
+  render() {
+    const { dataSources, dataSourcesCount, navModel, layoutMode } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">
+          {dataSourcesCount === 0 ? (
+            <EmptyListCTA model={emptyListModel} />
+          ) : (
+            [
+              <DataSourcesActionBar key="action-bar" />,
+              <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
+            ]
+          )}
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    navModel: getNavModel(state.navIndex, 'datasources'),
+    dataSources: getDataSources(state.dataSources),
+    layoutMode: getDataSourcesLayoutMode(state.dataSources),
+    dataSourcesCount: getDataSourcesCount(state.dataSources),
+  };
+}
+
+const mapDispatchToProps = {
+  loadDataSources,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourcesListPage));

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

@@ -0,0 +1,45 @@
+import { DataSource } from 'app/types';
+
+export const getMockDataSources = (amount: number): DataSource[] => {
+  const dataSources = [];
+
+  for (let i = 0; i <= amount; i++) {
+    dataSources.push({
+      access: '',
+      basicAuth: false,
+      database: `database-${i}`,
+      id: i,
+      isDefault: false,
+      jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
+      name: `dataSource-${i}`,
+      orgId: 1,
+      password: '',
+      readOnly: false,
+      type: 'cloudwatch',
+      typeLogoUrl: 'public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png',
+      url: '',
+      user: '',
+    });
+  }
+
+  return dataSources;
+};
+
+export const getMockDataSource = (): DataSource => {
+  return {
+    access: '',
+    basicAuth: false,
+    database: '',
+    id: 13,
+    isDefault: false,
+    jsonData: { 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: '',
+  };
+};

+ 108 - 0
public/app/features/datasources/__snapshots__/DataSourceList.test.tsx.snap

@@ -0,0 +1,108 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<section
+  className="card-section card-list-layout-grid"
+>
+  <ol
+    className="card-list"
+  >
+    <DataSourcesListItem
+      dataSource={
+        Object {
+          "access": "",
+          "basicAuth": false,
+          "database": "database-0",
+          "id": 0,
+          "isDefault": false,
+          "jsonData": Object {
+            "authType": "credentials",
+            "defaultRegion": "eu-west-2",
+          },
+          "name": "dataSource-0",
+          "orgId": 1,
+          "password": "",
+          "readOnly": false,
+          "type": "cloudwatch",
+          "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+          "url": "",
+          "user": "",
+        }
+      }
+      key="0-0"
+    />
+    <DataSourcesListItem
+      dataSource={
+        Object {
+          "access": "",
+          "basicAuth": false,
+          "database": "database-1",
+          "id": 1,
+          "isDefault": false,
+          "jsonData": Object {
+            "authType": "credentials",
+            "defaultRegion": "eu-west-2",
+          },
+          "name": "dataSource-1",
+          "orgId": 1,
+          "password": "",
+          "readOnly": false,
+          "type": "cloudwatch",
+          "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+          "url": "",
+          "user": "",
+        }
+      }
+      key="1-1"
+    />
+    <DataSourcesListItem
+      dataSource={
+        Object {
+          "access": "",
+          "basicAuth": false,
+          "database": "database-2",
+          "id": 2,
+          "isDefault": false,
+          "jsonData": Object {
+            "authType": "credentials",
+            "defaultRegion": "eu-west-2",
+          },
+          "name": "dataSource-2",
+          "orgId": 1,
+          "password": "",
+          "readOnly": false,
+          "type": "cloudwatch",
+          "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+          "url": "",
+          "user": "",
+        }
+      }
+      key="2-2"
+    />
+    <DataSourcesListItem
+      dataSource={
+        Object {
+          "access": "",
+          "basicAuth": false,
+          "database": "database-3",
+          "id": 3,
+          "isDefault": false,
+          "jsonData": Object {
+            "authType": "credentials",
+            "defaultRegion": "eu-west-2",
+          },
+          "name": "dataSource-3",
+          "orgId": 1,
+          "password": "",
+          "readOnly": false,
+          "type": "cloudwatch",
+          "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+          "url": "",
+          "user": "",
+        }
+      }
+      key="3-3"
+    />
+  </ol>
+</section>
+`;

+ 42 - 0
public/app/features/datasources/__snapshots__/DataSourcesActionBar.test.tsx.snap

@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<div
+  className="page-action-bar"
+>
+  <div
+    className="gf-form gf-form--grow"
+  >
+    <label
+      className="gf-form--has-input-icon"
+    >
+      <input
+        className="gf-form-input width-20"
+        onChange={[Function]}
+        placeholder="Filter by name or type"
+        type="text"
+        value=""
+      />
+      <i
+        className="gf-form-input-icon fa fa-search"
+      />
+    </label>
+    <LayoutSelector
+      mode="grid"
+      onLayoutModeChanged={[Function]}
+    />
+  </div>
+  <div
+    className="page-action-bar__spacer"
+  />
+  <a
+    className="page-header__cta btn btn-success"
+    href="datasources/new"
+  >
+    <i
+      className="fa fa-plus"
+    />
+    Add data source
+  </a>
+</div>
+`;

+ 45 - 0
public/app/features/datasources/__snapshots__/DataSourcesListItem.test.tsx.snap

@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+<li
+  className="card-item-wrapper"
+>
+  <a
+    className="card-item"
+    href="datasources/edit/13"
+  >
+    <div
+      className="card-item-header"
+    >
+      <div
+        className="card-item-type"
+      >
+        cloudwatch
+      </div>
+    </div>
+    <div
+      className="card-item-body"
+    >
+      <figure
+        className="card-item-figure"
+      >
+        <img
+          src="public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"
+        />
+      </figure>
+      <div
+        className="card-item-details"
+      >
+        <div
+          className="card-item-name"
+        >
+          gdev-cloudwatch
+        </div>
+        <div
+          className="card-item-sub-name"
+        />
+      </div>
+    </div>
+  </a>
+</li>
+`;

+ 164 - 0
public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap

@@ -0,0 +1,164 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render action bar and datasources 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <Connect(DataSourcesActionBar)
+      key="action-bar"
+    />
+    <DataSourcesList
+      dataSources={
+        Array [
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-0",
+            "id": 0,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-0",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-1",
+            "id": 1,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-1",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-2",
+            "id": 2,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-2",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-3",
+            "id": 3,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-3",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-4",
+            "id": 4,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-4",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+          Object {
+            "access": "",
+            "basicAuth": false,
+            "database": "database-5",
+            "id": 5,
+            "isDefault": false,
+            "jsonData": Object {
+              "authType": "credentials",
+              "defaultRegion": "eu-west-2",
+            },
+            "name": "dataSource-5",
+            "orgId": 1,
+            "password": "",
+            "readOnly": false,
+            "type": "cloudwatch",
+            "typeLogoUrl": "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
+            "url": "",
+            "user": "",
+          },
+        ]
+      }
+      key="list"
+      layoutMode="grid"
+    />
+  </div>
+</div>
+`;
+
+exports[`Render should render component 1`] = `
+<div>
+  <PageHeader
+    model={Object {}}
+  />
+  <div
+    className="page-container page-body"
+  >
+    <EmptyListCTA
+      model={
+        Object {
+          "buttonIcon": "gicon gicon-add-datasources",
+          "buttonLink": "datasources/new",
+          "buttonTitle": "Add data source",
+          "proTip": "You can also define data sources through configuration files.",
+          "proTipLink": "http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list",
+          "proTipLinkTitle": "Learn more",
+          "proTipTarget": "_blank",
+          "title": "There are no data sources defined yet",
+        }
+      }
+    />
+  </div>
+</div>
+`;

+ 51 - 0
public/app/features/datasources/state/actions.ts

@@ -0,0 +1,51 @@
+import { ThunkAction } from 'redux-thunk';
+import { DataSource, StoreState } from 'app/types';
+import { getBackendSrv } from '../../../core/services/backend_srv';
+import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
+
+export enum ActionTypes {
+  LoadDataSources = 'LOAD_DATA_SOURCES',
+  SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
+  SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
+}
+
+export interface LoadDataSourcesAction {
+  type: ActionTypes.LoadDataSources;
+  payload: DataSource[];
+}
+
+export interface SetDataSourcesSearchQueryAction {
+  type: ActionTypes.SetDataSourcesSearchQuery;
+  payload: string;
+}
+
+export interface SetDataSourcesLayoutModeAction {
+  type: ActionTypes.SetDataSourcesLayoutMode;
+  payload: LayoutMode;
+}
+
+const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
+  type: ActionTypes.LoadDataSources,
+  payload: dataSources,
+});
+
+export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({
+  type: ActionTypes.SetDataSourcesSearchQuery,
+  payload: searchQuery,
+});
+
+export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSourcesLayoutModeAction => ({
+  type: ActionTypes.SetDataSourcesLayoutMode,
+  payload: layoutMode,
+});
+
+export type Action = LoadDataSourcesAction | SetDataSourcesSearchQueryAction | SetDataSourcesLayoutModeAction;
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
+
+export function loadDataSources(): ThunkResult<void> {
+  return async dispatch => {
+    const response = await getBackendSrv().get('/api/datasources');
+    dispatch(dataSourcesLoaded(response));
+  };
+}

+ 29 - 0
public/app/features/datasources/state/reducers.ts

@@ -0,0 +1,29 @@
+import { DataSource, DataSourcesState } from 'app/types';
+import { Action, ActionTypes } from './actions';
+import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
+
+const initialState: DataSourcesState = {
+  dataSources: [] as DataSource[],
+  layoutMode: LayoutModes.Grid,
+  searchQuery: '',
+  dataSourcesCount: 0,
+};
+
+export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
+  switch (action.type) {
+    case ActionTypes.LoadDataSources:
+      return { ...state, dataSources: action.payload, dataSourcesCount: action.payload.length };
+
+    case ActionTypes.SetDataSourcesSearchQuery:
+      return { ...state, searchQuery: action.payload };
+
+    case ActionTypes.SetDataSourcesLayoutMode:
+      return { ...state, layoutMode: action.payload };
+  }
+
+  return state;
+};
+
+export default {
+  dataSources: dataSourcesReducer,
+};

+ 11 - 0
public/app/features/datasources/state/selectors.ts

@@ -0,0 +1,11 @@
+export const getDataSources = state => {
+  const regex = new RegExp(state.searchQuery, 'i');
+
+  return state.dataSources.filter(dataSource => {
+    return regex.test(dataSource.name) || regex.test(dataSource.database);
+  });
+};
+
+export const getDataSourcesSearchQuery = state => state.searchQuery;
+export const getDataSourcesLayoutMode = state => state.layoutMode;
+export const getDataSourcesCount = state => state.dataSourcesCount;

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

@@ -3,6 +3,5 @@ import './plugin_page_ctrl';
 import './import_list/import_list';
 import './ds_edit_ctrl';
 import './ds_dashboards_ctrl';
-import './ds_list_ctrl';
 import './datasource_srv';
 import './plugin_component';

+ 0 - 61
public/app/features/plugins/ds_list_ctrl.ts

@@ -1,61 +0,0 @@
-import coreModule from '../../core/core_module';
-import _ from 'lodash';
-
-export class DataSourcesCtrl {
-  datasources: any;
-  unfiltered: any;
-  navModel: any;
-  searchQuery: string;
-
-  /** @ngInject */
-  constructor(private $scope, private backendSrv, private datasourceSrv, private navModelSrv) {
-    this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0);
-    backendSrv.get('/api/datasources').then(result => {
-      this.datasources = result;
-      this.unfiltered = result;
-    });
-  }
-
-  onQueryUpdated() {
-    const regex = new RegExp(this.searchQuery, 'ig');
-    this.datasources = _.filter(this.unfiltered, item => {
-      regex.lastIndex = 0;
-      return regex.test(item.name) || regex.test(item.type);
-    });
-  }
-
-  removeDataSourceConfirmed(ds) {
-    this.backendSrv
-      .delete('/api/datasources/' + ds.id)
-      .then(
-        () => {
-          this.$scope.appEvent('alert-success', ['Datasource deleted', '']);
-        },
-        () => {
-          this.$scope.appEvent('alert-error', ['Unable to delete datasource', '']);
-        }
-      )
-      .then(() => {
-        this.backendSrv.get('/api/datasources').then(result => {
-          this.datasources = result;
-        });
-        this.backendSrv.get('/api/frontend/settings').then(settings => {
-          this.datasourceSrv.init(settings.datasources);
-        });
-      });
-  }
-
-  removeDataSource(ds) {
-    this.$scope.appEvent('confirm-modal', {
-      title: 'Delete',
-      text: 'Are you sure you want to delete datasource ' + ds.name + '?',
-      yesText: 'Delete',
-      icon: 'fa-trash',
-      onConfirm: () => {
-        this.removeDataSourceConfirmed(ds);
-      },
-    });
-  }
-}
-
-coreModule.controller('DataSourcesCtrl', DataSourcesCtrl);

+ 0 - 63
public/app/features/plugins/partials/ds_list.html

@@ -1,63 +0,0 @@
-<page-header model="ctrl.navModel"></page-header>
-
-<div class="page-container page-body">
-	<div ng-if="ctrl.unfiltered.length">
-		<div class="page-action-bar">
-			<div class="gf-form gf-form--grow">
-				<label class="gf-form--has-input-icon">
-					<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by name or type" />
-					<i class="gf-form-input-icon fa fa-search"></i>
-				</label>
-				<layout-selector />
-			</div>
-			<div class="page-action-bar__spacer"></div>
-			<a class="page-header__cta btn btn-success" href="datasources/new">
-				<i class="fa fa-plus"></i>
-				Add data source
-			</a>
-		</div>
-
-		<section class="card-section" layout-mode>
-			<ol class="card-list">
-				<li class="card-item-wrapper" ng-repeat="ds in ctrl.datasources">
-					<a class="card-item" href="datasources/edit/{{ds.id}}/">
-						<div class="card-item-header">
-							<div class="card-item-type">
-								{{ds.type}}
-							</div>
-						</div>
-						<div class="card-item-body">
-							<figure class="card-item-figure">
-								<img ng-src="{{ds.typeLogoUrl}}">
-							</figure>
-							<div class="card-item-details">
-								<div class="card-item-name">
-									{{ds.name}}
-									<span ng-if="ds.isDefault">
-										<span class="btn btn-secondary btn-mini">default</span>
-									</span>
-								</div>
-								<div class="card-item-sub-name">
-									{{ds.url}}
-								</div>
-							</div>
-						</div>
-					</a>
-				</li>
-			</ol>
-		</section>
-	</div>
-
-	<div ng-if="ctrl.unfiltered.length === 0">
-		<empty-list-cta model="{
-			title: 'There are no data sources defined yet',
-			buttonIcon: 'gicon gicon-add-datasources',
-			buttonLink: 'datasources/new',
-			buttonTitle: 'Add data source',
-			proTip: 'You can also define data sources through configuration files.',
-			proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
-			proTipLinkTitle: 'Learn more',
-			proTipTarget: '_blank'
-		}" />
-	</div>
-</div>

+ 5 - 3
public/app/routes/routes.ts

@@ -9,6 +9,7 @@ import ApiKeys from 'app/features/api-keys/ApiKeysPage';
 import PluginListPage from 'app/features/plugins/PluginListPage';
 import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
 import FolderPermissions from 'app/features/folders/FolderPermissions';
+import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage';
 
 /** @ngInject */
 export function setupAngularRoutes($routeProvider, $locationProvider) {
@@ -63,9 +64,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controllerAs: 'ctrl',
     })
     .when('/datasources', {
-      templateUrl: 'public/app/features/plugins/partials/ds_list.html',
-      controller: 'DataSourcesCtrl',
-      controllerAs: 'ctrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => DataSourcesListPage,
+      },
     })
     .when('/datasources/edit/:id', {
       templateUrl: 'public/app/features/plugins/partials/ds_edit.html',

+ 2 - 0
public/app/store/configureStore.ts

@@ -8,6 +8,7 @@ import apiKeysReducers from 'app/features/api-keys/state/reducers';
 import foldersReducers from 'app/features/folders/state/reducers';
 import dashboardReducers from 'app/features/dashboard/state/reducers';
 import pluginReducers from 'app/features/plugins/state/reducers';
+import dataSourcesReducers from 'app/features/datasources/state/reducers';
 
 const rootReducer = combineReducers({
   ...sharedReducers,
@@ -17,6 +18,7 @@ const rootReducer = combineReducers({
   ...foldersReducers,
   ...dashboardReducers,
   ...pluginReducers,
+  ...dataSourcesReducers,
 });
 
 export let store;

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

@@ -1,7 +1,25 @@
+import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
+
 export interface DataSource {
   id: number;
   orgId: number;
   name: string;
   typeLogoUrl: string;
   type: string;
+  access: string;
+  url: string;
+  password: string;
+  user: string;
+  database: string;
+  basicAuth: false;
+  isDefault: false;
+  jsonData: { authType: string; defaultRegion: string };
+  readOnly: false;
+}
+
+export interface DataSourcesState {
+  dataSources: DataSource[];
+  searchQuery: string;
+  layoutMode: LayoutMode;
+  dataSourcesCount: number;
 }

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

@@ -5,9 +5,9 @@ import { NavModel, NavModelItem, NavIndex } from './navModel';
 import { FolderDTO, FolderState, FolderInfo } from './folders';
 import { DashboardState } from './dashboard';
 import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
-import { DataSource } from './datasources';
 import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
 import { User } from './user';
+import { DataSource, DataSourcesState } from './datasources';
 import { PluginMeta, Plugin, PluginsState } from './plugins';
 
 export {
@@ -41,6 +41,7 @@ export {
   User,
   Plugin,
   PluginsState,
+  DataSourcesState,
 };
 
 export interface StoreState {