Parcourir la source

Merge pull request #13416 from grafana/davkal/11999-explore-from-mixed-panels

Explore: jump to explore from panels with mixed datasources
David il y a 7 ans
Parent
commit
321c09aec4

+ 12 - 10
public/app/core/services/keybindingSrv.ts

@@ -4,7 +4,7 @@ import _ from 'lodash';
 import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
-import { renderUrl } from 'app/core/utils/url';
+import { getExploreUrl } from 'app/core/utils/explore';
 
 import Mousetrap from 'mousetrap';
 import 'mousetrap-global-bind';
@@ -15,7 +15,14 @@ export class KeybindingSrv {
   timepickerOpen = false;
 
   /** @ngInject */
-  constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) {
+  constructor(
+    private $rootScope,
+    private $location,
+    private $timeout,
+    private datasourceSrv,
+    private timeSrv,
+    private contextSrv
+  ) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
@@ -194,14 +201,9 @@ export class KeybindingSrv {
         if (dashboard.meta.focusPanelId) {
           const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
           const datasource = await this.datasourceSrv.get(panel.datasource);
-          if (datasource && datasource.supportsExplore) {
-            const range = this.timeSrv.timeRangeForUrl();
-            const state = {
-              ...datasource.getExploreState(panel),
-              range,
-            };
-            const exploreState = JSON.stringify(state);
-            this.$location.url(renderUrl('/explore', { state: exploreState }));
+          const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv);
+          if (url) {
+            this.$timeout(() => this.$location.url(url));
           }
         }
       });

+ 4 - 5
public/app/features/explore/Wrapper.test.tsx → public/app/core/utils/explore.test.ts

@@ -1,6 +1,5 @@
-import { serializeStateToUrlParam, parseUrlState } from './Wrapper';
-import { DEFAULT_RANGE } from './TimePicker';
-import { ExploreState } from './Explore';
+import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore';
+import { ExploreState } from 'app/types/explore';
 
 const DEFAULT_EXPLORE_STATE: ExploreState = {
   datasource: null,
@@ -27,7 +26,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
   tableResult: null,
 };
 
-describe('Wrapper state functions', () => {
+describe('state functions', () => {
   describe('parseUrlState', () => {
     it('returns default state on empty string', () => {
       expect(parseUrlState('')).toMatchObject({
@@ -57,7 +56,7 @@ describe('Wrapper state functions', () => {
       };
       expect(serializeStateToUrlParam(state)).toBe(
         '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' +
-          '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}'
+        '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}'
       );
     });
   });

+ 78 - 0
public/app/core/utils/explore.ts

@@ -0,0 +1,78 @@
+import { renderUrl } from 'app/core/utils/url';
+import { ExploreState, ExploreUrlState } from 'app/types/explore';
+
+export const DEFAULT_RANGE = {
+  from: 'now-6h',
+  to: 'now',
+};
+
+/**
+ * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
+ *
+ * @param panel Origin panel of the jump to Explore
+ * @param panelTargets The origin panel's query targets
+ * @param panelDatasource The origin panel's datasource
+ * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
+ * @param timeSrv Time service to get the current dashboard range from
+ */
+export async function getExploreUrl(
+  panel: any,
+  panelTargets: any[],
+  panelDatasource: any,
+  datasourceSrv: any,
+  timeSrv: any
+) {
+  let exploreDatasource = panelDatasource;
+  let exploreTargets = panelTargets;
+  let url;
+
+  // Mixed datasources need to choose only one datasource
+  if (panelDatasource.meta.id === 'mixed' && panelTargets) {
+    // Find first explore datasource among targets
+    let mixedExploreDatasource;
+    for (const t of panel.targets) {
+      const datasource = await datasourceSrv.get(t.datasource);
+      if (datasource && datasource.meta.explore) {
+        mixedExploreDatasource = datasource;
+        break;
+      }
+    }
+
+    // Add all its targets
+    if (mixedExploreDatasource) {
+      exploreDatasource = mixedExploreDatasource;
+      exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name);
+    }
+  }
+
+  if (exploreDatasource && exploreDatasource.meta.explore) {
+    const range = timeSrv.timeRangeForUrl();
+    const state = {
+      ...exploreDatasource.getExploreState(exploreTargets),
+      range,
+    };
+    const exploreState = JSON.stringify(state);
+    url = renderUrl('/explore', { state: exploreState });
+  }
+  return url;
+}
+
+export function parseUrlState(initial: string | undefined): ExploreUrlState {
+  if (initial) {
+    try {
+      return JSON.parse(decodeURI(initial));
+    } catch (e) {
+      console.error(e);
+    }
+  }
+  return { datasource: null, queries: [], range: DEFAULT_RANGE };
+}
+
+export function serializeStateToUrlParam(state: ExploreState): string {
+  const urlState: ExploreUrlState = {
+    datasource: state.datasourceName,
+    queries: state.queries.map(q => ({ query: q.query })),
+    range: state.range,
+  };
+  return JSON.stringify(urlState);
+}

+ 0 - 5
public/app/core/utils/location_util.ts

@@ -1,10 +1,5 @@
 import config from 'app/core/config';
 
-// Slash encoding for angular location provider, see https://github.com/angular/angular.js/issues/10479
-const SLASH = '<SLASH>';
-export const decodePathComponent = (pc: string) => decodeURIComponent(pc).replace(new RegExp(SLASH, 'g'), '/');
-export const encodePathComponent = (pc: string) => encodeURIComponent(pc.replace(/\//g, SLASH));
-
 export const stripBaseFromUrl = url => {
   const appSubUrl = config.appSubUrl;
   const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;

+ 3 - 27
public/app/features/explore/Explore.tsx

@@ -2,19 +2,20 @@ import React from 'react';
 import { hot } from 'react-hot-loader';
 import Select from 'react-select';
 
-import { Query, Range, ExploreUrlState } from 'app/types/explore';
+import { ExploreState, ExploreUrlState } from 'app/types/explore';
 import kbn from 'app/core/utils/kbn';
 import colors from 'app/core/utils/colors';
 import store from 'app/core/store';
 import TimeSeries from 'app/core/time_series2';
 import { parse as parseDate } from 'app/core/utils/datemath';
+import { DEFAULT_RANGE } from 'app/core/utils/explore';
 
 import ElapsedTime from './ElapsedTime';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
 import Logs from './Logs';
 import Table from './Table';
-import TimePicker, { DEFAULT_RANGE } from './TimePicker';
+import TimePicker from './TimePicker';
 import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 
 const MAX_HISTORY_ITEMS = 100;
@@ -58,31 +59,6 @@ interface ExploreProps {
   urlState: ExploreUrlState;
 }
 
-export interface ExploreState {
-  datasource: any;
-  datasourceError: any;
-  datasourceLoading: boolean | null;
-  datasourceMissing: boolean;
-  datasourceName?: string;
-  graphResult: any;
-  history: any[];
-  latency: number;
-  loading: any;
-  logsResult: any;
-  queries: Query[];
-  queryErrors: any[];
-  queryHints: any[];
-  range: Range;
-  requestOptions: any;
-  showingGraph: boolean;
-  showingLogs: boolean;
-  showingTable: boolean;
-  supportsGraph: boolean | null;
-  supportsLogs: boolean | null;
-  supportsTable: boolean | null;
-  tableResult: any;
-}
-
 export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   el: any;
 

+ 0 - 1
public/app/features/explore/TimePicker.tsx

@@ -5,7 +5,6 @@ import * as dateMath from 'app/core/utils/datemath';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 
 const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
-
 export const DEFAULT_RANGE = {
   from: 'now-6h',
   to: 'now',

+ 3 - 23
public/app/features/explore/Wrapper.tsx

@@ -3,31 +3,11 @@ import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 
 import { updateLocation } from 'app/core/actions';
+import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
 import { StoreState } from 'app/types';
-import { ExploreUrlState } from 'app/types/explore';
+import { ExploreState } from 'app/types/explore';
 
-import Explore, { ExploreState } from './Explore';
-import { DEFAULT_RANGE } from './TimePicker';
-
-export function parseUrlState(initial: string | undefined): ExploreUrlState {
-  if (initial) {
-    try {
-      return JSON.parse(decodeURI(initial));
-    } catch (e) {
-      console.error(e);
-    }
-  }
-  return { datasource: null, queries: [], range: DEFAULT_RANGE };
-}
-
-export function serializeStateToUrlParam(state: ExploreState): string {
-  const urlState: ExploreUrlState = {
-    datasource: state.datasourceName,
-    queries: state.queries.map(q => ({ query: q.query })),
-    range: state.range,
-  };
-  return JSON.stringify(urlState);
-}
+import Explore from './Explore';
 
 interface WrapperProps {
   backendSrv?: any;

+ 12 - 10
public/app/features/panel/metrics_panel_ctrl.ts

@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn';
 import { PanelCtrl } from 'app/features/panel/panel_ctrl';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
-import { renderUrl } from 'app/core/utils/url';
+import { getExploreUrl } from 'app/core/utils/explore';
 
 import { metricsTabDirective } from './metrics_tab';
 
@@ -314,7 +314,12 @@ class MetricsPanelCtrl extends PanelCtrl {
 
   getAdditionalMenuItems() {
     const items = [];
-    if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
+    if (
+      config.exploreEnabled &&
+      this.contextSrv.isEditor &&
+      this.datasource &&
+      (this.datasource.meta.explore || this.datasource.meta.id === 'mixed')
+    ) {
       items.push({
         text: 'Explore',
         click: 'ctrl.explore();',
@@ -325,14 +330,11 @@ class MetricsPanelCtrl extends PanelCtrl {
     return items;
   }
 
-  explore() {
-    const range = this.timeSrv.timeRangeForUrl();
-    const state = {
-      ...this.datasource.getExploreState(this.panel),
-      range,
-    };
-    const exploreState = JSON.stringify(state);
-    this.$location.url(renderUrl('/explore', { state: exploreState }));
+  async explore() {
+    const url = await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv);
+    if (url) {
+      this.$timeout(() => this.$location.url(url));
+    }
   }
 
   addQuery(target) {

+ 1 - 1
public/app/features/panel/specs/metrics_panel_ctrl.test.ts

@@ -38,7 +38,7 @@ describe('MetricsPanelCtrl', () => {
     describe('and has datasource set that supports explore and user has powers', () => {
       beforeEach(() => {
         ctrl.contextSrv = { isEditor: true };
-        ctrl.datasource = { supportsExplore: true };
+        ctrl.datasource = { meta: { explore: true } };
         additionalItems = ctrl.getAdditionalMenuItems();
       });
 

+ 0 - 2
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -8,7 +8,6 @@ import * as templatingVariable from 'app/features/templating/variable';
 export default class CloudWatchDatasource {
   type: any;
   name: any;
-  supportMetrics: any;
   proxyUrl: any;
   defaultRegion: any;
   instanceSettings: any;
@@ -17,7 +16,6 @@ export default class CloudWatchDatasource {
   constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) {
     this.type = 'cloudwatch';
     this.name = instanceSettings.name;
-    this.supportMetrics = true;
     this.proxyUrl = instanceSettings.url;
     this.defaultRegion = instanceSettings.jsonData.defaultRegion;
     this.instanceSettings = instanceSettings;

+ 0 - 4
public/app/plugins/datasource/influxdb/datasource.ts

@@ -16,8 +16,6 @@ export default class InfluxDatasource {
   basicAuth: any;
   withCredentials: any;
   interval: any;
-  supportAnnotations: boolean;
-  supportMetrics: boolean;
   responseParser: any;
 
   /** @ngInject */
@@ -34,8 +32,6 @@ export default class InfluxDatasource {
     this.basicAuth = instanceSettings.basicAuth;
     this.withCredentials = instanceSettings.withCredentials;
     this.interval = (instanceSettings.jsonData || {}).timeInterval;
-    this.supportAnnotations = true;
-    this.supportMetrics = true;
     this.responseParser = new ResponseParser();
   }
 

+ 0 - 2
public/app/plugins/datasource/opentsdb/datasource.ts

@@ -10,7 +10,6 @@ export default class OpenTsDatasource {
   basicAuth: any;
   tsdbVersion: any;
   tsdbResolution: any;
-  supportMetrics: any;
   tagKeys: any;
 
   aggregatorsPromise: any;
@@ -26,7 +25,6 @@ export default class OpenTsDatasource {
     instanceSettings.jsonData = instanceSettings.jsonData || {};
     this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1;
     this.tsdbResolution = instanceSettings.jsonData.tsdbResolution || 1;
-    this.supportMetrics = true;
     this.tagKeys = {};
 
     this.aggregatorsPromise = null;

+ 3 - 7
public/app/plugins/datasource/prometheus/datasource.ts

@@ -149,8 +149,6 @@ export class PrometheusDatasource {
   editorSrc: string;
   name: string;
   ruleMappings: { [index: string]: string };
-  supportsExplore: boolean;
-  supportMetrics: boolean;
   url: string;
   directUrl: string;
   basicAuth: any;
@@ -166,8 +164,6 @@ export class PrometheusDatasource {
     this.type = 'prometheus';
     this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
     this.name = instanceSettings.name;
-    this.supportsExplore = true;
-    this.supportMetrics = true;
     this.url = instanceSettings.url;
     this.directUrl = instanceSettings.directUrl;
     this.basicAuth = instanceSettings.basicAuth;
@@ -522,10 +518,10 @@ export class PrometheusDatasource {
     });
   }
 
-  getExploreState(panel) {
+  getExploreState(targets: any[]) {
     let state = {};
-    if (panel.targets) {
-      const queries = panel.targets.map(t => ({
+    if (targets && targets.length > 0) {
+      const queries = targets.map(t => ({
         query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr),
         format: t.format,
       }));

+ 25 - 0
public/app/types/explore.ts

@@ -9,6 +9,31 @@ export interface Query {
   key?: string;
 }
 
+export interface ExploreState {
+  datasource: any;
+  datasourceError: any;
+  datasourceLoading: boolean | null;
+  datasourceMissing: boolean;
+  datasourceName?: string;
+  graphResult: any;
+  history: any[];
+  latency: number;
+  loading: any;
+  logsResult: any;
+  queries: Query[];
+  queryErrors: any[];
+  queryHints: any[];
+  range: Range;
+  requestOptions: any;
+  showingGraph: boolean;
+  showingLogs: boolean;
+  showingTable: boolean;
+  supportsGraph: boolean | null;
+  supportsLogs: boolean | null;
+  supportsTable: boolean | null;
+  tableResult: any;
+}
+
 export interface ExploreUrlState {
   datasource: string;
   queries: Query[];