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