Commit a13b4f2b by David Committed by GitHub

Merge pull request #12596 from grafana/davkal/explore-datasource-selector

Explore Datasource selector
parents 0f6e5e29 eb2abe80
...@@ -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) => {
...@@ -34,6 +35,7 @@ function parseInitialState(initial) { ...@@ -34,6 +35,7 @@ function parseInitialState(initial) {
try { try {
const parsed = JSON.parse(decodePathComponent(initial)); const parsed = JSON.parse(decodePathComponent(initial));
return { return {
datasource: parsed.datasource,
queries: parsed.queries.map(q => q.query), queries: parsed.queries.map(q => q.query),
range: parsed.range, range: parsed.range,
}; };
...@@ -46,8 +48,10 @@ function parseInitialState(initial) { ...@@ -46,8 +48,10 @@ 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;
initialDatasource?: string;
latency: number; latency: number;
loading: any; loading: any;
queries: any; queries: any;
...@@ -61,16 +65,16 @@ interface IExploreState { ...@@ -61,16 +65,16 @@ 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 { datasource, queries, range } = 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,
initialDatasource: datasource,
latency: 0, latency: 0,
loading: false, loading: false,
queries: ensureQueries(queries), queries: ensureQueries(queries),
...@@ -85,19 +89,49 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -85,19 +89,49 @@ 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(); const { initialDatasource } = this.state;
if (testResult.status === 'success') { if (!datasourceSrv) {
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit()); throw new Error('No datasource service passed as props.');
}
const datasources = datasourceSrv.getExploreSources();
if (datasources.length > 0) {
this.setState({ datasourceLoading: true });
// Priority: datasource in url, default datasource, first explore datasource
let datasource;
if (initialDatasource) {
datasource = await datasourceSrv.get(initialDatasource);
} else { } else {
this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); datasource = await datasourceSrv.get();
}
if (!datasource.meta.explore) {
datasource = await datasourceSrv.get(datasources[0].name);
}
this.setDatasource(datasource);
} else {
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 +142,18 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -108,6 +142,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 +272,12 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -226,11 +272,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 +294,12 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -247,6 +294,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 +317,18 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -264,6 +317,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 +356,15 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -291,13 +356,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}
......
...@@ -34,13 +34,13 @@ export class DatasourceSrv { ...@@ -34,13 +34,13 @@ export class DatasourceSrv {
} }
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 this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
} }
var deferred = this.$q.defer(); const deferred = this.$q.defer();
var pluginDef = dsConfig.meta; const pluginDef = dsConfig.meta;
importPluginModule(pluginDef.module) importPluginModule(pluginDef.module)
.then(plugin => { .then(plugin => {
...@@ -55,7 +55,7 @@ export class DatasourceSrv { ...@@ -55,7 +55,7 @@ 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;
...@@ -73,7 +73,7 @@ export class DatasourceSrv { ...@@ -73,7 +73,7 @@ export class DatasourceSrv {
} }
getAnnotationSources() { getAnnotationSources() {
var sources = []; const sources = [];
this.addDataSourceVariables(sources); this.addDataSourceVariables(sources);
...@@ -86,6 +86,14 @@ export class DatasourceSrv { ...@@ -86,6 +86,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 +163,4 @@ export class DatasourceSrv { ...@@ -155,3 +163,4 @@ export class DatasourceSrv {
} }
coreModule.service('datasourceSrv', DatasourceSrv); coreModule.service('datasourceSrv', DatasourceSrv);
export default DatasourceSrv;
...@@ -17,9 +17,35 @@ const templateSrv = { ...@@ -17,9 +17,35 @@ 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',
......
...@@ -357,6 +357,7 @@ export class PrometheusDatasource { ...@@ -357,6 +357,7 @@ export class PrometheusDatasource {
state = { state = {
...state, ...state,
queries, queries,
datasource: this.name,
}; };
} }
return state; return state;
......
...@@ -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,7 +37,10 @@ ...@@ -28,7 +37,10 @@
"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"
} }
......
...@@ -60,6 +60,10 @@ ...@@ -60,6 +60,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.datasource-picker {
min-width: 10rem;
}
.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