Parcourir la source

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

Explore Datasource selector
David il y a 7 ans
Parent
commit
a13b4f2b3f

+ 1 - 0
pkg/plugins/datasource_plugin.go

@@ -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"`

+ 84 - 17
public/app/containers/Explore/Explore.tsx

@@ -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 testResult = await datasource.testDatasource();
-    if (testResult.status === 'success') {
-      this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
+    const { datasourceSrv } = this.props;
+    const { initialDatasource } = this.state;
+    if (!datasourceSrv) {
+      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}
 
 
+        {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}>
-            Error connecting to datasource.
-          </div>
+          <div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
         ) : null}
         ) : null}
 
 
-        {datasource ? (
+        {datasource && !datasourceError ? (
           <div className="explore-container">
           <div className="explore-container">
             <QueryRows
             <QueryRows
               queries={queries}
               queries={queries}

+ 14 - 5
public/app/features/plugins/datasource_srv.ts

@@ -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();
-    var pluginDef = dsConfig.meta;
+    const deferred = this.$q.defer();
+    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;

+ 27 - 1
public/app/features/plugins/specs/datasource_srv.jest.ts

@@ -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',

+ 1 - 0
public/app/plugins/datasource/prometheus/datasource.ts

@@ -357,6 +357,7 @@ export class PrometheusDatasource {
       state = {
       state = {
         ...state,
         ...state,
         queries,
         queries,
+        datasource: this.name,
       };
       };
     }
     }
     return state;
     return state;

+ 21 - 9
public/app/plugins/datasource/prometheus/plugin.json

@@ -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", "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,
   "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"
   }
   }
-}
+}

+ 4 - 0
public/sass/pages/_explore.scss

@@ -60,6 +60,10 @@
     flex-wrap: wrap;
     flex-wrap: wrap;
   }
   }
 
 
+  .datasource-picker {
+    min-width: 10rem;
+  }
+
   .timepicker {
   .timepicker {
     display: flex;
     display: flex;