Commit d06b26de by David Kaltschmidt

Explore Datasource selector

Adds a datasource selector to the Explore UI. Only datasource plugins
that have `explore: true` in their `plugin.json` can be selected.

- adds datasource selector (based on react-select) to explore UI
- adds getExploreSources to datasource service
- new `explore` flag in datasource plugins model
- Prometheus plugin enabled explore
parent 030d0633
......@@ -22,6 +22,7 @@ type DataSourcePlugin struct {
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
Explore bool `json:"explore"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
......
import React from 'react';
import { hot } from 'react-hot-loader';
import Select from 'react-select';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import { decodePathComponent } from 'app/core/utils/location_util';
import ElapsedTime from './ElapsedTime';
import QueryRows from './QueryRows';
import Graph from './Graph';
import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { decodePathComponent } from 'app/core/utils/location_util';
function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => {
......@@ -46,7 +47,8 @@ function parseInitialState(initial) {
interface IExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
graphResult: any;
latency: number;
loading: any;
......@@ -61,15 +63,14 @@ interface IExploreState {
// @observer
export class Explore extends React.Component<any, IExploreState> {
datasourceSrv: DatasourceSrv;
constructor(props) {
super(props);
const { range, queries } = parseInitialState(props.routeParams.initial);
this.state = {
datasource: null,
datasourceError: null,
datasourceLoading: true,
datasourceLoading: null,
datasourceMissing: false,
graphResult: null,
latency: 0,
loading: false,
......@@ -85,19 +86,43 @@ export class Explore extends React.Component<any, IExploreState> {
}
async componentDidMount() {
const datasource = await this.props.datasourceSrv.get();
const testResult = await datasource.testDatasource();
if (testResult.status === 'success') {
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
const { datasourceSrv } = this.props;
if (!datasourceSrv) {
throw new Error('No datasource service passed as props.');
}
const datasources = datasourceSrv.getExploreSources();
if (datasources.length > 0) {
this.setState({ datasourceLoading: true });
// Try default datasource, otherwise get first
let datasource = await datasourceSrv.get();
if (!datasource.meta.explore) {
datasource = await datasourceSrv.get(datasources[0].name);
}
this.setDatasource(datasource);
} else {
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
this.setState({ datasourceMissing: true });
}
}
componentDidCatch(error) {
this.setState({ datasourceError: error });
console.error(error);
}
async setDatasource(datasource) {
try {
const testResult = await datasource.testDatasource();
if (testResult.status === 'success') {
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
} else {
this.setState({ datasource: datasource, datasourceError: testResult.message, datasourceLoading: false });
}
} catch (error) {
const message = (error && error.statusText) || error;
this.setState({ datasource: datasource, datasourceError: message, datasourceLoading: false });
}
}
handleAddQueryRow = index => {
const { queries } = this.state;
const nextQueries = [
......@@ -108,6 +133,18 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState({ queries: nextQueries });
};
handleChangeDatasource = async option => {
this.setState({
datasource: null,
datasourceError: null,
datasourceLoading: true,
graphResult: null,
tableResult: null,
});
const datasource = await this.props.datasourceSrv.get(option.value);
this.setDatasource(datasource);
};
handleChangeQuery = (query, index) => {
const { queries } = this.state;
const nextQuery = {
......@@ -226,11 +263,12 @@ export class Explore extends React.Component<any, IExploreState> {
};
render() {
const { position, split } = this.props;
const { datasourceSrv, position, split } = this.props;
const {
datasource,
datasourceError,
datasourceLoading,
datasourceMissing,
graphResult,
latency,
loading,
......@@ -247,6 +285,12 @@ export class Explore extends React.Component<any, IExploreState> {
const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
const exploreClass = split ? 'explore explore-split' : 'explore';
const datasources = datasourceSrv.getExploreSources().map(ds => ({
value: ds.name,
label: ds.name,
}));
const selectedDatasource = datasource ? datasource.name : undefined;
return (
<div className={exploreClass}>
<div className="navbar">
......@@ -264,6 +308,18 @@ export class Explore extends React.Component<any, IExploreState> {
</button>
</div>
)}
{!datasourceMissing ? (
<div className="navbar-buttons">
<Select
className="datasource-picker"
clearable={false}
onChange={this.handleChangeDatasource}
options={datasources}
placeholder="Loading datasources..."
value={selectedDatasource}
/>
</div>
) : null}
<div className="navbar__spacer" />
{position === 'left' && !split ? (
<div className="navbar-buttons">
......@@ -291,13 +347,15 @@ export class Explore extends React.Component<any, IExploreState> {
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
{datasourceMissing ? (
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
) : null}
{datasourceError ? (
<div className="explore-container" title={datasourceError}>
Error connecting to datasource.
</div>
<div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
) : null}
{datasource ? (
{datasource && !datasourceError ? (
<div className="explore-container">
<QueryRows
queries={queries}
......
......@@ -7,7 +7,7 @@ export class DatasourceSrv {
datasources: any;
/** @ngInject */
constructor(private $q, private $injector, private $rootScope, private templateSrv) {
constructor(private $injector, private $rootScope, private templateSrv) {
this.init();
}
......@@ -27,27 +27,25 @@ export class DatasourceSrv {
}
if (this.datasources[name]) {
return this.$q.when(this.datasources[name]);
return Promise.resolve(this.datasources[name]);
}
return this.loadDatasource(name);
}
loadDatasource(name) {
var dsConfig = config.datasources[name];
const dsConfig = config.datasources[name];
if (!dsConfig) {
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
return Promise.reject({ message: 'Datasource named ' + name + ' was not found' });
}
var deferred = this.$q.defer();
var pluginDef = dsConfig.meta;
const pluginDef = dsConfig.meta;
importPluginModule(pluginDef.module)
return importPluginModule(pluginDef.module)
.then(plugin => {
// check if its in cache now
if (this.datasources[name]) {
deferred.resolve(this.datasources[name]);
return;
return this.datasources[name];
}
// plugin module needs to export a constructor function named Datasource
......@@ -55,17 +53,15 @@ export class DatasourceSrv {
throw new Error('Plugin module is missing Datasource constructor');
}
var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
instance.meta = pluginDef;
instance.name = name;
this.datasources[name] = instance;
deferred.resolve(instance);
return instance;
})
.catch(err => {
this.$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]);
});
return deferred.promise;
}
getAll() {
......@@ -73,7 +69,7 @@ export class DatasourceSrv {
}
getAnnotationSources() {
var sources = [];
const sources = [];
this.addDataSourceVariables(sources);
......@@ -86,6 +82,14 @@ export class DatasourceSrv {
return sources;
}
getExploreSources() {
const { datasources } = config;
const es = Object.keys(datasources)
.map(name => datasources[name])
.filter(ds => ds.meta && ds.meta.explore);
return _.sortBy(es, ['name']);
}
getMetricSources(options) {
var metricSources = [];
......@@ -155,3 +159,4 @@ export class DatasourceSrv {
}
coreModule.service('datasourceSrv', DatasourceSrv);
export default DatasourceSrv;
......@@ -16,10 +16,36 @@ const templateSrv = {
};
describe('datasource_srv', function() {
let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
let metricSources;
let _datasourceSrv = new DatasourceSrv({}, {}, templateSrv);
describe('when loading explore sources', () => {
beforeEach(() => {
config.datasources = {
explore1: {
name: 'explore1',
meta: { explore: true, metrics: true },
},
explore2: {
name: 'explore2',
meta: { explore: true, metrics: false },
},
nonExplore: {
name: 'nonExplore',
meta: { explore: false, metrics: true },
},
};
});
it('should return list of explore sources', () => {
const exploreSources = _datasourceSrv.getExploreSources();
expect(exploreSources.length).toBe(2);
expect(exploreSources[0].name).toBe('explore1');
expect(exploreSources[1].name).toBe('explore2');
});
});
describe('when loading metric sources', () => {
let metricSources;
let unsortedDatasources = {
mmm: {
type: 'test-db',
......
......@@ -2,21 +2,30 @@
"type": "datasource",
"name": "Prometheus",
"id": "prometheus",
"includes": [
{"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
{"type": "dashboard", "name": "Prometheus 2.0 Stats", "path": "dashboards/prometheus_2_stats.json"},
{"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
{
"type": "dashboard",
"name": "Prometheus Stats",
"path": "dashboards/prometheus_stats.json"
},
{
"type": "dashboard",
"name": "Prometheus 2.0 Stats",
"path": "dashboards/prometheus_2_stats.json"
},
{
"type": "dashboard",
"name": "Grafana Stats",
"path": "dashboards/grafana_stats.json"
}
],
"metrics": true,
"alerting": true,
"annotations": true,
"explore": true,
"queryOptions": {
"minInterval": true
},
"info": {
"description": "Prometheus Data Source for Grafana",
"author": {
......@@ -28,8 +37,11 @@
"large": "img/prometheus_logo.svg"
},
"links": [
{"name": "Prometheus", "url": "https://prometheus.io/"}
{
"name": "Prometheus",
"url": "https://prometheus.io/"
}
],
"version": "5.0.0"
}
}
}
\ No newline at end of file
......@@ -60,6 +60,10 @@
flex-wrap: wrap;
}
.datasource-picker {
min-width: 6rem;
}
.timepicker {
display: flex;
......
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