mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13537 from grafana/new-data-source-as-separate-page
New data source as separate page
This commit is contained in:
88
public/app/features/datasources/NewDataSourcePage.tsx
Normal file
88
public/app/features/datasources/NewDataSourcePage.tsx
Normal file
@@ -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
public/app/features/datasources/state/actions.test.ts
Normal file
44
public/app/features/datasources/state/actions.test.ts
Normal file
@@ -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-');
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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 & 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>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ctrl.current.readOnly" ng-click="ctrl.saveChanges()">Save & 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>
|
||||
|
||||
</form>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -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
|
||||
<i class="fa fa-caret-down" ng-show="showAccessHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="showAccessHelp"> </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
|
||||
<i class="fa fa-caret-down" ng-show="showAccessHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="showAccessHelp"> </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>
|
||||
<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-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-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 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>
|
||||
|
||||
<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">
|
||||
<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" 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 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>
|
||||
|
@@ -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',
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
@@ -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
public/sass/components/_add_data_source.scss
Normal file
46
public/sass/components/_add_data_source.scss
Normal file
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user