Explorar o código

Merge pull request #13537 from grafana/new-data-source-as-separate-page

New data source as separate page
Torkel Ödegaard %!s(int64=7) %!d(string=hai) anos
pai
achega
07eba60e24

+ 88 - 0
public/app/features/datasources/NewDataSourcePage.tsx

@@ -0,0 +1,88 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import { hot } from 'react-hot-loader';
+import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import { NavModel, Plugin } from 'app/types';
+import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
+import { updateLocation } from '../../core/actions';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { getDataSourceTypes } from './state/selectors';
+
+export interface Props {
+  navModel: NavModel;
+  dataSourceTypes: Plugin[];
+  addDataSource: typeof addDataSource;
+  loadDataSourceTypes: typeof loadDataSourceTypes;
+  updateLocation: typeof updateLocation;
+  dataSourceTypeSearchQuery: string;
+  setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
+}
+
+class NewDataSourcePage extends PureComponent<Props> {
+  componentDidMount() {
+    this.props.loadDataSourceTypes();
+  }
+
+  onDataSourceTypeClicked = type => {
+    this.props.addDataSource(type);
+  };
+
+  onSearchQueryChange = event => {
+    this.props.setDataSourceTypeSearchQuery(event.target.value);
+  };
+
+  render() {
+    const { navModel, dataSourceTypes, dataSourceTypeSearchQuery } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">
+          <h2 className="add-data-source-header">Choose data source type</h2>
+          <div className="add-data-source-search">
+            <label className="gf-form--has-input-icon">
+              <input
+                type="text"
+                className="gf-form-input width-20"
+                value={dataSourceTypeSearchQuery}
+                onChange={this.onSearchQueryChange}
+                placeholder="Filter by name or type"
+              />
+              <i className="gf-form-input-icon fa fa-search" />
+            </label>
+          </div>
+          <div className="add-data-source-grid">
+            {dataSourceTypes.map((type, index) => {
+              return (
+                <div
+                  onClick={() => this.onDataSourceTypeClicked(type)}
+                  className="add-data-source-grid-item"
+                  key={`${type.id}-${index}`}
+                >
+                  <img className="add-data-source-grid-item-logo" src={type.info.logos.small} />
+                  <span className="add-data-source-grid-item-text">{type.name}</span>
+                </div>
+              );
+            })}
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    navModel: getNavModel(state.navIndex, 'datasources'),
+    dataSourceTypes: getDataSourceTypes(state.dataSources),
+  };
+}
+
+const mapDispatchToProps = {
+  addDataSource,
+  loadDataSourceTypes,
+  updateLocation,
+  setDataSourceTypeSearchQuery,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(NewDataSourcePage));

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

@@ -0,0 +1,44 @@
+import { findNewName, nameExits } from './actions';
+import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
+
+describe('Name exists', () => {
+  const plugins = getMockPlugins(5);
+
+  it('should be true', () => {
+    const name = 'pretty cool plugin-1';
+
+    expect(nameExits(plugins, name)).toEqual(true);
+  });
+
+  it('should be false', () => {
+    const name = 'pretty cool plugin-6';
+
+    expect(nameExits(plugins, name));
+  });
+});
+
+describe('Find new name', () => {
+  it('should create a new name', () => {
+    const plugins = getMockPlugins(5);
+    const name = 'pretty cool plugin-1';
+
+    expect(findNewName(plugins, name)).toEqual('pretty cool plugin-6');
+  });
+
+  it('should create new name without suffix', () => {
+    const plugin = getMockPlugin();
+    plugin.name = 'prometheus';
+    const plugins = [plugin];
+    const name = 'prometheus';
+
+    expect(findNewName(plugins, name)).toEqual('prometheus-1');
+  });
+
+  it('should handle names that end with -', () => {
+    const plugin = getMockPlugin();
+    const plugins = [plugin];
+    const name = 'pretty cool plugin-';
+
+    expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
+  });
+});

+ 105 - 2
public/app/features/datasources/state/actions.ts

@@ -1,12 +1,16 @@
 import { ThunkAction } from 'redux-thunk';
-import { DataSource, StoreState } from 'app/types';
+import { DataSource, Plugin, StoreState } from 'app/types';
 import { getBackendSrv } from '../../../core/services/backend_srv';
 import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
+import { updateLocation } from '../../../core/actions';
+import { UpdateLocationAction } from '../../../core/actions/location';
 
 export enum ActionTypes {
   LoadDataSources = 'LOAD_DATA_SOURCES',
+  LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
   SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
   SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
+  SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
 }
 
 export interface LoadDataSourcesAction {
@@ -24,11 +28,26 @@ export interface SetDataSourcesLayoutModeAction {
   payload: LayoutMode;
 }
 
+export interface LoadDataSourceTypesAction {
+  type: ActionTypes.LoadDataSourceTypes;
+  payload: Plugin[];
+}
+
+export interface SetDataSourceTypeSearchQueryAction {
+  type: ActionTypes.SetDataSourceTypeSearchQuery;
+  payload: string;
+}
+
 const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
   type: ActionTypes.LoadDataSources,
   payload: dataSources,
 });
 
+const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
+  type: ActionTypes.LoadDataSourceTypes,
+  payload: dataSourceTypes,
+});
+
 export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({
   type: ActionTypes.SetDataSourcesSearchQuery,
   payload: searchQuery,
@@ -39,7 +58,18 @@ export const setDataSourcesLayoutMode = (layoutMode: LayoutMode): SetDataSources
   payload: layoutMode,
 });
 
-export type Action = LoadDataSourcesAction | SetDataSourcesSearchQueryAction | SetDataSourcesLayoutModeAction;
+export const setDataSourceTypeSearchQuery = (query: string): SetDataSourceTypeSearchQueryAction => ({
+  type: ActionTypes.SetDataSourceTypeSearchQuery,
+  payload: query,
+});
+
+export type Action =
+  | LoadDataSourcesAction
+  | SetDataSourcesSearchQueryAction
+  | SetDataSourcesLayoutModeAction
+  | UpdateLocationAction
+  | LoadDataSourceTypesAction
+  | SetDataSourceTypeSearchQueryAction;
 
 type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
 
@@ -49,3 +79,76 @@ export function loadDataSources(): ThunkResult<void> {
     dispatch(dataSourcesLoaded(response));
   };
 }
+
+export function addDataSource(plugin: Plugin): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    await dispatch(loadDataSources());
+
+    const dataSources = getStore().dataSources.dataSources;
+
+    const newInstance = {
+      name: plugin.name,
+      type: plugin.id,
+      access: 'proxy',
+      isDefault: dataSources.length === 0,
+    };
+
+    if (nameExits(dataSources, newInstance.name)) {
+      newInstance.name = findNewName(dataSources, newInstance.name);
+    }
+
+    const result = await getBackendSrv().post('/api/datasources', newInstance);
+    dispatch(updateLocation({ path: `/datasources/edit/${result.id}` }));
+  };
+}
+
+export function loadDataSourceTypes(): ThunkResult<void> {
+  return async dispatch => {
+    const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
+    dispatch(dataSourceTypesLoaded(result));
+  };
+}
+
+export function nameExits(dataSources, name) {
+  return (
+    dataSources.filter(dataSource => {
+      return dataSource.name === name;
+    }).length > 0
+  );
+}
+
+export function findNewName(dataSources, name) {
+  // Need to loop through current data sources to make sure
+  // the name doesn't exist
+  while (nameExits(dataSources, name)) {
+    // If there's a duplicate name that doesn't end with '-x'
+    // we can add -1 to the name and be done.
+    if (!nameHasSuffix(name)) {
+      name = `${name}-1`;
+    } else {
+      // if there's a duplicate name that ends with '-x'
+      // we can try to increment the last digit until the name is unique
+
+      // remove the 'x' part and replace it with the new number
+      name = `${getNewName(name)}${incrementLastDigit(getLastDigit(name))}`;
+    }
+  }
+
+  return name;
+}
+
+function nameHasSuffix(name) {
+  return name.endsWith('-', name.length - 1);
+}
+
+function getLastDigit(name) {
+  return parseInt(name.slice(-1), 10);
+}
+
+function incrementLastDigit(digit) {
+  return isNaN(digit) ? 1 : digit + 1;
+}
+
+function getNewName(name) {
+  return name.slice(0, name.length - 1);
+}

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

@@ -1,4 +1,4 @@
-import { DataSource, DataSourcesState } from 'app/types';
+import { DataSource, DataSourcesState, Plugin } from 'app/types';
 import { Action, ActionTypes } from './actions';
 import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
 
@@ -7,6 +7,8 @@ const initialState: DataSourcesState = {
   layoutMode: LayoutModes.Grid,
   searchQuery: '',
   dataSourcesCount: 0,
+  dataSourceTypes: [] as Plugin[],
+  dataSourceTypeSearchQuery: '',
 };
 
 export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
@@ -19,6 +21,12 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
 
     case ActionTypes.SetDataSourcesLayoutMode:
       return { ...state, layoutMode: action.payload };
+
+    case ActionTypes.LoadDataSourceTypes:
+      return { ...state, dataSourceTypes: action.payload };
+
+    case ActionTypes.SetDataSourceTypeSearchQuery:
+      return { ...state, dataSourceTypeSearchQuery: action.payload };
   }
 
   return state;

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

@@ -6,6 +6,14 @@ export const getDataSources = state => {
   });
 };
 
+export const getDataSourceTypes = state => {
+  const regex = new RegExp(state.dataSourceTypeSearchQuery, 'i');
+
+  return state.dataSourceTypes.filter(type => {
+    return regex.test(type.name);
+  });
+};
+
 export const getDataSourcesSearchQuery = state => state.searchQuery;
 export const getDataSourcesLayoutMode = state => state.layoutMode;
 export const getDataSourcesCount = state => state.dataSourcesCount;

+ 5 - 5
public/app/features/explore/Explore.tsx

@@ -534,12 +534,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
               </a>
             </div>
           ) : (
-              <div className="navbar-buttons explore-first-button">
-                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
-                  Close Split
+            <div className="navbar-buttons explore-first-button">
+              <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
+                Close Split
               </button>
-              </div>
-            )}
+            </div>
+          )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select

+ 15 - 25
public/app/features/plugins/partials/ds_edit.html

@@ -1,18 +1,13 @@
 <page-header model="ctrl.navModel"></page-header>
 
 <div class="page-container page-body">
-
-  <div ng-if="ctrl.current.readOnly"  class="page-action-bar">
-    <div class="grafana-info-box span8">
-      Disclaimer. This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.
-    </div>
-  </div>
+  <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-7">Name</span>
+          <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.
@@ -22,13 +17,6 @@
         </div>
         <gf-form-switch class="gf-form" label="Default" checked="ctrl.current.isDefault" switch-class="max-width-6"></gf-form-switch>
       </div>
-
-      <div class="gf-form">
-        <span class="gf-form-label width-7">Type</span>
-        <div class="gf-form-select-wrapper max-width-23">
-          <select class="gf-form-input" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.userChangedType()"></select>
-        </div>
-      </div>
     </div>
 
     <div class="grafana-info-box" ng-if="ctrl.datasourceMeta.state === 'alpha'">
@@ -66,17 +54,19 @@
       </div>
     </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>
+		<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 />
+		<br />
+		<br />
+		<br />
 
-  </form>
+	</form>
 </div>

+ 116 - 128
public/app/features/plugins/partials/ds_http_settings.html

@@ -1,9 +1,9 @@
 <div class="gf-form-group">
-  <h3 class="page-heading">HTTP</h3>
+	<h3 class="page-heading">HTTP</h3>
   <div class="gf-form-group">
     <div class="gf-form-inline">
       <div class="gf-form max-width-30">
-        <span class="gf-form-label width-7">URL</span>
+        <span class="gf-form-label width-10">URL</span>
         <input class="gf-form-input" type="text"
               ng-model='current.url' placeholder="{{suggestUrl}}"
               bs-typeahead="getSuggestUrls"  min-length="0"
@@ -20,140 +20,128 @@
           </span>
         </info-popover>
       </div>
-    </div>
+		</div>
 
-    <div class="gf-form-inline" ng-if="showAccessOption">
-      <div class="gf-form max-width-30">
-        <span class="gf-form-label width-7">Access</span>
-        <div class="gf-form-select-wrapper max-width-24">
-          <select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select>
-        </div>
-      </div>
-      <div class="gf-form">
-        <label class="gf-form-label query-keyword pointer" ng-click="toggleAccessHelp()">
-          Help&nbsp;
-          <i class="fa fa-caret-down" ng-show="showAccessHelp"></i>
-          <i class="fa fa-caret-right" ng-hide="showAccessHelp">&nbsp;</i>
-        </label>
-      </div>
-    </div>
+		<div class="gf-form-inline" ng-if="showAccessOption">
+			<div class="gf-form max-width-30">
+				<span class="gf-form-label width-10">Access</span>
+				<div class="gf-form-select-wrapper max-width-24">
+					<select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select>
+				</div>
+			</div>
+			<div class="gf-form">
+				<label class="gf-form-label query-keyword pointer" ng-click="toggleAccessHelp()">
+					Help&nbsp;
+					<i class="fa fa-caret-down" ng-show="showAccessHelp"></i>
+					<i class="fa fa-caret-right" ng-hide="showAccessHelp">&nbsp;</i>
+				</label>
+			</div>
+		</div>
 
-    <div class="alert alert-info" ng-show="showAccessHelp">
-      <div class="alert-body">
-        <p>
-          Access mode controls how requests to the data source will be handled.
-          <strong><i>Server</i></strong> should be the preferred way if nothing else stated.
-        </p>
-        <div class="alert-title">Server access mode (Default):</div>
-        <p>
-          All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source
-          and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements.
-          The URL needs to be accessible from the grafana backend/server if you select this access mode.
-        </p>
-        <div class="alert-title">Browser access mode:</div>
-        <p>
-          All requests will be made from the browser directly to the data source and may be subject to
-          Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this
-          access mode.
-        </p>
-      </div>
-    </div>
-  </div>
+		<div class="grafana-info-box m-t-2" ng-show="showAccessHelp">
+			<p>
+			Access mode controls how requests to the data source will be handled.
+			<strong><i>Server</i></strong> should be the preferred way if nothing else stated.
+			</p>
+			<div class="alert-title">Server access mode (Default):</div>
+			<p>
+			All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source
+			and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements.
+			The URL needs to be accessible from the grafana backend/server if you select this access mode.
+			</p>
+			<div class="alert-title">Browser access mode:</div>
+			<p>
+			All requests will be made from the browser directly to the data source and may be subject to
+			Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this
+			access mode.
+		</div>
 
-  <h3 class="page-heading">Auth</h3>
-  <div class="gf-form-group">
-    <div class="gf-form-inline">
-      <gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-8" switch-class="max-width-6"></gf-form-switch>
-      <gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
-    </div>
-    <div class="gf-form-inline">
-      <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-8" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
-      <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
-    </div>
-  </div>
+		<div class="gf-form-inline" ng-if="current.access=='proxy'">
+			<div class="gf-form">
+				<span class="gf-form-label width-10">Whitelisted Cookies</span>
+				<bootstrap-tagsinput ng-model="current.jsonData.keepCookies" width-class="width-20" tagclass="label label-tag" placeholder="Add Name">
+				</bootstrap-tagsinput>
+				<info-popover mode="right-absolute">
+					Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source.
+				</info-popover>
+			</div>
+		</div>
+	</div>
 
-  <div class="gf-form-inline">
-    <gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verification (Insecure)" label-class="width-16" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
-  </div>
-</div>
+	<h3 class="page-heading">Auth</h3>
+	<div class="gf-form-group">
+		<div class="gf-form-inline">
+			<gf-form-switch class="gf-form" label="Basic Auth" checked="current.basicAuth" label-class="width-10" switch-class="max-width-6"></gf-form-switch>
+			<gf-form-switch class="gf-form" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests." checked="current.withCredentials" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
+		</div>
+		<div class="gf-form-inline">
+			<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="TLS Client Auth" label-class="width-10" checked="current.jsonData.tlsAuth" switch-class="max-width-6"></gf-form-switch>
+			<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="With CA Cert" tooltip="Needed for verifing self-signed TLS Certs" checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6"></gf-form-switch>
+		</div>
+		<div class="gf-form-inline">
+			<gf-form-switch class="gf-form" ng-if="current.access=='proxy'" label="Skip TLS Verify" label-class="width-10" checked="current.jsonData.tlsSkipVerify" switch-class="max-width-6"></gf-form-switch>
+		</div>
+	</div>
 
-<div class="gf-form-group" ng-if="current.basicAuth">
-  <h6>Basic Auth Details</h6>
-  <div class="gf-form" ng-if="current.basicAuth">
-    <span class="gf-form-label width-7">
-      User
-    </span>
-    <input class="gf-form-input max-width-21" type="text"  ng-model='current.basicAuthUser' placeholder="user" required></input>
-  </div>
+	<div class="gf-form-group" ng-if="current.basicAuth">
+		<h6>Basic Auth Details</h6>
+		<div class="gf-form" ng-if="current.basicAuth">
+			<span class="gf-form-label width-10">User</span>
+			<input class="gf-form-input max-width-21" type="text"  ng-model='current.basicAuthUser' placeholder="user" required></input>
+		</div>
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Password</span>
+			<input class="gf-form-input max-width-21" type="password" ng-model='current.basicAuthPassword' placeholder="password" required></input>
+		</div>
+	</div>
 
-  <div class="gf-form">
-    <span class="gf-form-label width-7">
-      Password
-    </span>
-    <input class="gf-form-input max-width-21" type="password" ng-model='current.basicAuthPassword' placeholder="password" required></input>
-  </div>
-</div>
+	<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
+		<div class="gf-form">
+			<h6>TLS Auth Details</h6>
+			<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
+		</div>
+		<div ng-if="current.jsonData.tlsAuthWithCACert">
+			<div class="gf-form-inline">
+				<div class="gf-form gf-form--v-stretch">
+					<label class="gf-form-label width-7">CA Cert</label>
+				</div>
+				<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
+					<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
+				</div>
 
-<div class="gf-form-group" ng-if="(current.jsonData.tlsAuth || current.jsonData.tlsAuthWithCACert) && current.access=='proxy'">
-  <div class="gf-form">
-    <h6>TLS Auth Details</h6>
-    <info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
-  </div>
-  <div ng-if="current.jsonData.tlsAuthWithCACert">
-    <div class="gf-form-inline">
-      <div class="gf-form gf-form--v-stretch">
-        <label class="gf-form-label width-7">CA Cert</label>
-      </div>
-      <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert">
-        <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----"></textarea>
-      </div>
+				<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
+					<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
+					<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsCACert = false">reset</a>
+				</div>
+			</div>
+		</div>
 
-      <div class="gf-form" ng-if="current.secureJsonFields.tlsCACert">
-        <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
-        <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsCACert = false">reset</a>
-      </div>
-    </div>
-  </div>
-
-  <div ng-if="current.jsonData.tlsAuth">
-    <div class="gf-form-inline">
-      <div class="gf-form gf-form--v-stretch">
-        <label class="gf-form-label width-7">Client Cert</label>
-      </div>
-      <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
-        <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
-      </div>
-      <div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
-        <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
-        <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
-      </div>
-    </div>
+		<div ng-if="current.jsonData.tlsAuth">
+		<div class="gf-form-inline">
+			<div class="gf-form gf-form--v-stretch">
+				<label class="gf-form-label width-7">Client Cert</label>
+			</div>
+			<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert">
+				<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
+			</div>
+			<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert">
+				<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
+				<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientCert = false">reset</a>
+			</div>
+		</div>
 
-    <div class="gf-form-inline">
-      <div class="gf-form gf-form--v-stretch">
-        <label class="gf-form-label width-7">Client Key</label>
-      </div>
-      <div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
-        <textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
-      </div>
-      <div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
-        <input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
-        <a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
-      </div>
-    </div>
-  </div>
+		<div class="gf-form-inline">
+			<div class="gf-form gf-form--v-stretch">
+				<label class="gf-form-label width-7">Client Key</label>
+			</div>
+			<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey">
+				<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
+			</div>
+			<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey">
+				<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
+				<a class="btn btn-secondary gf-form-btn" href="#" ng-click="current.secureJsonFields.tlsClientKey = false">reset</a>
+			</div>
+		</div>
+	</div>
 </div>
 
-<h3 class="page-heading" ng-if="current.access=='proxy'">Advanced HTTP Settings</h3>
-<div class="gf-form-group" ng-if="current.access=='proxy'">
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label width-10">Whitelisted Cookies</span>
-      <bootstrap-tagsinput ng-model="current.jsonData.keepCookies" tagclass="label label-tag" placeholder="Add Name">
-      </bootstrap-tagsinput>
-      <info-popover mode="right-absolute">
-        Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source.
-      </info-popover>
-    </div>
-  </div>
-</div>

+ 3 - 3
public/app/plugins/datasource/influxdb/partials/config.html

@@ -6,18 +6,18 @@
 <div class="gf-form-group">
 	<div class="gf-form-inline">
 		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-7">Database</span>
+			<span class="gf-form-label width-10">Database</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.database' placeholder="" required></input>
 		</div>
 	</div>
 
 	<div class="gf-form-inline">
 		<div class="gf-form max-width-15">
-			<span class="gf-form-label width-7">User</span>
+			<span class="gf-form-label width-10">User</span>
 			<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder=""></input>
 		</div>
 		<div class="gf-form max-width-15">
-			<span class="gf-form-label width-7">Password</span>
+			<span class="gf-form-label width-10">Password</span>
 			<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder=""></input>
 		</div>
 	</div>

+ 3 - 1
public/app/plugins/datasource/stackdriver/partials/config.html

@@ -81,4 +81,6 @@
   </div>
 </div>
 
-<p class="gf-form-label" ng-hide="ctrl.current.secureJsonFields.privateKey"><i class="fa fa-save"></i> Do not forget to save your changes after uploading a file.</p>
+<div class="grafana-info-box" ng-hide="ctrl.current.secureJsonFields.privateKey">
+	Do not forget to save your changes after uploading a file.
+</div>

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

@@ -10,6 +10,7 @@ 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';
+import NewDataSourcePage from '../features/datasources/NewDataSourcePage';
 import UsersListPage from 'app/features/users/UsersListPage';
 
 /** @ngInject */
@@ -81,9 +82,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controllerAs: 'ctrl',
     })
     .when('/datasources/new', {
-      templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
-      controller: 'DataSourceEditCtrl',
-      controllerAs: 'ctrl',
+      template: '<react-container />',
+      resolve: {
+        component: () => NewDataSourcePage,
+      },
     })
     .when('/dashboards', {
       templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_list.html',

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

@@ -1,4 +1,5 @@
 import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
+import { Plugin } from './plugins';
 
 export interface DataSource {
   id: number;
@@ -20,6 +21,8 @@ export interface DataSource {
 export interface DataSourcesState {
   dataSources: DataSource[];
   searchQuery: string;
+  dataSourceTypeSearchQuery: string;
   layoutMode: LayoutMode;
   dataSourcesCount: number;
+  dataSourceTypes: Plugin[];
 }

+ 1 - 0
public/sass/_grafana.scss

@@ -95,6 +95,7 @@
 @import 'components/user-picker';
 @import 'components/description-picker';
 @import 'components/delete_button';
+@import 'components/_add_data_source.scss';
 
 // PAGES
 @import 'pages/login';

+ 46 - 0
public/sass/components/_add_data_source.scss

@@ -0,0 +1,46 @@
+.add-data-source-header {
+  margin-bottom: $spacer * 2;
+  padding-top: $spacer;
+  text-align: center;
+}
+
+.add-data-source-search {
+  display: flex;
+  justify-content: center;
+  margin-bottom: $panel-margin * 2;
+}
+
+.add-data-source-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  grid-row-gap: 10px;
+  grid-column-gap: 10px;
+
+  @include media-breakpoint-up(md) {
+    grid-template-columns: repeat(3, 1fr);
+  }
+}
+
+.add-data-source-grid-item {
+  padding: 15px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  background: $card-background;
+  box-shadow: $card-shadow;
+  color: $text-color;
+
+  &:hover {
+    background: $card-background-hover;
+    color: $text-color-strong;
+  }
+}
+
+.add-data-source-grid-item-text {
+  font-size: $font-size-h5;
+}
+
+.add-data-source-grid-item-logo {
+  margin: 0 15px;
+  width: 55px;
+}

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

@@ -191,6 +191,7 @@
   .card-item-wrapper {
     padding: 0;
     width: 100%;
+    margin-bottom: 3px;
   }
 
   .card-item-wrapper--clickable {
@@ -198,7 +199,6 @@
   }
 
   .card-item {
-    border-bottom: 3px solid $page-bg;
     border-radius: 2px;
   }