mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
030d06331f
commit
d06b26de26
@ -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"`
|
||||
|
@ -1,16 +1,17 @@
|
||||
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}
|
||||
|
||||
{datasourceError ? (
|
||||
<div className="explore-container" title={datasourceError}>
|
||||
Error connecting to datasource.
|
||||
</div>
|
||||
{datasourceMissing ? (
|
||||
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
|
||||
) : null}
|
||||
|
||||
{datasource ? (
|
||||
{datasourceError ? (
|
||||
<div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
|
||||
) : null}
|
||||
|
||||
{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"
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,10 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.datasource-picker {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.timepicker {
|
||||
display: flex;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user