diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx new file mode 100644 index 00000000000..527ecf6db83 --- /dev/null +++ b/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 { + 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 ( +
+ +
+

Choose data source type

+
+ +
+
+ {dataSourceTypes.map((type, index) => { + return ( +
this.onDataSourceTypeClicked(type)} + className="add-data-source-grid-item" + key={`${type.id}-${index}`} + > + + {type.name} +
+ ); + })} +
+
+
+ ); + } +} + +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)); diff --git a/public/app/features/datasources/state/actions.test.ts b/public/app/features/datasources/state/actions.test.ts new file mode 100644 index 00000000000..d0a8caad9a7 --- /dev/null +++ b/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-'); + }); +}); diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 297797f2e59..33d6b79c5df 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/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 = ThunkAction; @@ -49,3 +79,76 @@ export function loadDataSources(): ThunkResult { dispatch(dataSourcesLoaded(response)); }; } + +export function addDataSource(plugin: Plugin): ThunkResult { + 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 { + 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); +} diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index d57b0ad523a..acb228d3ed6 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/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; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 6df08f68037..80e1400114f 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/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; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index c90ba0dd7cc..6e34f631408 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -534,12 +534,12 @@ export class Explore extends React.PureComponent { ) : ( -
- -
- )} + + )} {!datasourceMissing ? (
The name is used when you select the data source in panels. @@ -22,13 +17,6 @@
- -
- Type -
- -
-
@@ -66,17 +54,19 @@
-
- - - Back -
+
+ This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource. +
-
-
-
+
+ + + Back +
- +
+
+
+ + diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index 6d014af567c..17aedd48afd 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -1,9 +1,9 @@
-

HTTP

+

HTTP

- URL + URL
-
+
-
-
- Access -
- -
-
-
- -
-
+
+
+ Access +
+ +
+
+
+ +
+
-
-
-

- Access mode controls how requests to the data source will be handled. - Server should be the preferred way if nothing else stated. -

-
Server access mode (Default):
-

- 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. -

-
Browser access mode:
-

- 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. -

-
-
-
+
+

+ Access mode controls how requests to the data source will be handled. + Server should be the preferred way if nothing else stated. +

+
Server access mode (Default):
+

+ 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. +

+
Browser access mode:
+

+ 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. +

-

Auth

-
-
- - -
-
- - -
-
+
+
+ Whitelisted Cookies + + + + Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source. + +
+
+ -
- -
+

Auth

+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
Basic Auth Details
+
+ User + +
+
+ Password + +
+
+ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+ +
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+ +
+
+ +
+
+ + reset +
+
+ +
+
+ +
+
+ +
+
+ + reset +
+
+
-
-
Basic Auth Details
-
- - User - - -
- -
- - Password - - -
-
- -
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- -

Advanced HTTP Settings

-
-
-
- Whitelisted Cookies - - - - Grafana Proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source. - -
-
-
diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index a70a1de98a4..4de2fadd52d 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -6,18 +6,18 @@
- Database + Database
- User + User
- Password + Password
diff --git a/public/app/plugins/datasource/stackdriver/partials/config.html b/public/app/plugins/datasource/stackdriver/partials/config.html index d8029abc39f..46b79d8bb0d 100644 --- a/public/app/plugins/datasource/stackdriver/partials/config.html +++ b/public/app/plugins/datasource/stackdriver/partials/config.html @@ -81,4 +81,6 @@
-

Do not forget to save your changes after uploading a file.

+
+ Do not forget to save your changes after uploading a file. +
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 3ebf4236fa3..9aecf53e7bb 100644 --- a/public/app/routes/routes.ts +++ b/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: '', + resolve: { + component: () => NewDataSourcePage, + }, }) .when('/dashboards', { templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_list.html', diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index b9936e7c01b..4d8d755f106 100644 --- a/public/app/types/datasources.ts +++ b/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[]; } diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index be3a3b90f78..b9b1527e2cc 100644 --- a/public/sass/_grafana.scss +++ b/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'; diff --git a/public/sass/components/_add_data_source.scss b/public/sass/components/_add_data_source.scss new file mode 100644 index 00000000000..508f7f80d8e --- /dev/null +++ b/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; +} diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 11a8abb7640..f39be84ec04 100644 --- a/public/sass/components/_cards.scss +++ b/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; }