mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #12596 from grafana/davkal/explore-datasource-selector
Explore Datasource selector
This commit is contained in:
commit
a13b4f2b3f
@ -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"`
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
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) {
|
|||||||
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) {
|
|||||||
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 {
|
|||||||
|
|
||||||
// @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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
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 +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> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
</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> {
|
|||||||
|
|
||||||
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
||||||
|
|
||||||
{datasourceError ? (
|
{datasourceMissing ? (
|
||||||
<div className="explore-container" title={datasourceError}>
|
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
|
||||||
Error connecting to datasource.
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{datasource ? (
|
{datasourceError ? (
|
||||||
|
<div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{datasource && !datasourceError ? (
|
||||||
<div className="explore-container">
|
<div className="explore-container">
|
||||||
<QueryRows
|
<QueryRows
|
||||||
queries={queries}
|
queries={queries}
|
||||||
|
@ -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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAnnotationSources() {
|
getAnnotationSources() {
|
||||||
var sources = [];
|
const sources = [];
|
||||||
|
|
||||||
this.addDataSourceVariables(sources);
|
this.addDataSourceVariables(sources);
|
||||||
|
|
||||||
@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
coreModule.service('datasourceSrv', DatasourceSrv);
|
coreModule.service('datasourceSrv', DatasourceSrv);
|
||||||
|
export default DatasourceSrv;
|
||||||
|
@ -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 {
|
|||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
queries,
|
queries,
|
||||||
|
datasource: this.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
@ -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 @@
|
|||||||
"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 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datasource-picker {
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
.timepicker {
|
.timepicker {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user