Commit 07eba60e by Torkel Ödegaard Committed by GitHub

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

New data source as separate page
parents 9346ee00 2e4a1f31
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));
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-');
});
});
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);
}
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;
......
......@@ -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;
......@@ -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
......
<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>
......@@ -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>
......
......@@ -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>
......@@ -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',
......
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[];
}
......@@ -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';
......
.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;
}
......@@ -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;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment