Просмотр исходного кода

Merge pull request #11770 from grafana/davkal/explore-panel-link

Explore: Add entry to panel menu to jump to Explore
David 7 лет назад
Родитель
Сommit
1236b7b918

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

@@ -10,6 +10,7 @@ import Graph from './Graph';
 import Table from './Table';
 import Table from './Table';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 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) => {
@@ -38,6 +39,19 @@ function makeTimeSeriesList(dataList, options) {
   });
   });
 }
 }
 
 
+function parseInitialQueries(initial) {
+  if (!initial) {
+    return [];
+  }
+  try {
+    const parsed = JSON.parse(decodePathComponent(initial));
+    return parsed.queries.map(q => q.query);
+  } catch (e) {
+    console.error(e);
+    return [];
+  }
+}
+
 interface IExploreState {
 interface IExploreState {
   datasource: any;
   datasource: any;
   datasourceError: any;
   datasourceError: any;
@@ -58,6 +72,7 @@ export class Explore extends React.Component<any, IExploreState> {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
+    const initialQueries = parseInitialQueries(props.routeParams.initial);
     this.state = {
     this.state = {
       datasource: null,
       datasource: null,
       datasourceError: null,
       datasourceError: null,
@@ -65,7 +80,7 @@ export class Explore extends React.Component<any, IExploreState> {
       graphResult: null,
       graphResult: null,
       latency: 0,
       latency: 0,
       loading: false,
       loading: false,
-      queries: ensureQueries(),
+      queries: ensureQueries(initialQueries),
       requestOptions: null,
       requestOptions: null,
       showingGraph: true,
       showingGraph: true,
       showingTable: true,
       showingTable: true,
@@ -77,7 +92,7 @@ export class Explore extends React.Component<any, IExploreState> {
     const datasource = await this.props.datasourceSrv.get();
     const datasource = await this.props.datasourceSrv.get();
     const testResult = await datasource.testDatasource();
     const testResult = await datasource.testDatasource();
     if (testResult.status === 'success') {
     if (testResult.status === 'success') {
-      this.setState({ datasource, datasourceError: null, datasourceLoading: false });
+      this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
     } else {
     } else {
       this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
       this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
     }
     }

+ 15 - 4
public/app/containers/Explore/QueryRows.tsx

@@ -6,13 +6,16 @@ class QueryRow extends PureComponent<any, any> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
-      query: '',
+      edited: false,
+      query: props.query || '',
     };
     };
   }
   }
 
 
   handleChangeQuery = value => {
   handleChangeQuery = value => {
     const { index, onChangeQuery } = this.props;
     const { index, onChangeQuery } = this.props;
-    this.setState({ query: value });
+    const { query } = this.state;
+    const edited = query !== value;
+    this.setState({ edited, query: value });
     if (onChangeQuery) {
     if (onChangeQuery) {
       onChangeQuery(value, index);
       onChangeQuery(value, index);
     }
     }
@@ -41,6 +44,7 @@ class QueryRow extends PureComponent<any, any> {
 
 
   render() {
   render() {
     const { request } = this.props;
     const { request } = this.props;
+    const { edited, query } = this.state;
     return (
     return (
       <div className="query-row">
       <div className="query-row">
         <div className="query-row-tools">
         <div className="query-row-tools">
@@ -52,7 +56,12 @@ class QueryRow extends PureComponent<any, any> {
           </button>
           </button>
         </div>
         </div>
         <div className="query-field-wrapper">
         <div className="query-field-wrapper">
-          <QueryField onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} request={request} />
+          <QueryField
+            initialQuery={edited ? null : query}
+            onPressEnter={this.handlePressEnter}
+            onQueryChange={this.handleChangeQuery}
+            request={request}
+          />
         </div>
         </div>
       </div>
       </div>
     );
     );
@@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent<any, any> {
   render() {
   render() {
     const { className = '', queries, ...handlers } = this.props;
     const { className = '', queries, ...handlers } = this.props;
     return (
     return (
-      <div className={className}>{queries.map((q, index) => <QueryRow key={q.key} index={index} {...handlers} />)}</div>
+      <div className={className}>
+        {queries.map((q, index) => <QueryRow key={q.key} index={index} query={q.query} {...handlers} />)}
+      </div>
     );
     );
   }
   }
 }
 }

+ 13 - 1
public/app/core/services/keybindingSrv.ts

@@ -3,6 +3,7 @@ import _ from 'lodash';
 
 
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
+import { encodePathComponent } from 'app/core/utils/location_util';
 
 
 import Mousetrap from 'mousetrap';
 import Mousetrap from 'mousetrap';
 import 'mousetrap-global-bind';
 import 'mousetrap-global-bind';
@@ -13,7 +14,7 @@ export class KeybindingSrv {
   timepickerOpen = false;
   timepickerOpen = false;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private $rootScope, private $location) {
+  constructor(private $rootScope, private $location, private datasourceSrv) {
     // clear out all shortcuts on route change
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
       Mousetrap.reset();
@@ -176,6 +177,17 @@ export class KeybindingSrv {
       }
       }
     });
     });
 
 
+    this.bind('x', async () => {
+      if (dashboard.meta.focusPanelId) {
+        const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
+        const datasource = await this.datasourceSrv.get(panel.datasource);
+        if (datasource && datasource.supportsExplore) {
+          const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel)));
+          this.$location.url(`/explore/${exploreState}`);
+        }
+      }
+    });
+
     // delete panel
     // delete panel
     this.bind('p r', () => {
     this.bind('p r', () => {
       if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
       if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {

+ 7 - 4
public/app/core/utils/location_util.ts

@@ -1,6 +1,11 @@
 import config from 'app/core/config';
 import config from 'app/core/config';
 
 
-const _stripBaseFromUrl = url => {
+// 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 appSubUrl = config.appSubUrl;
   const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
   const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
   const urlWithoutBase =
   const urlWithoutBase =
@@ -9,6 +14,4 @@ const _stripBaseFromUrl = url => {
   return urlWithoutBase;
   return urlWithoutBase;
 };
 };
 
 
-export default {
-  stripBaseFromUrl: _stripBaseFromUrl,
-};
+export default { stripBaseFromUrl };

+ 19 - 0
public/app/features/panel/metrics_panel_ctrl.ts

@@ -6,6 +6,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl';
 
 
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
+import { encodePathComponent } from 'app/core/utils/location_util';
 
 
 import { metricsTabDirective } from './metrics_tab';
 import { metricsTabDirective } from './metrics_tab';
 
 
@@ -309,6 +310,24 @@ class MetricsPanelCtrl extends PanelCtrl {
     this.refresh();
     this.refresh();
   }
   }
 
 
+  getAdditionalMenuItems() {
+    const items = [];
+    if (this.datasource.supportsExplore) {
+      items.push({
+        text: 'Explore',
+        click: 'ctrl.explore();',
+        icon: 'fa fa-fw fa-rocket',
+        shortcut: 'x',
+      });
+    }
+    return items;
+  }
+
+  explore() {
+    const exploreState = encodePathComponent(JSON.stringify(this.datasource.getExploreState(this.panel)));
+    this.$location.url(`/explore/${exploreState}`);
+  }
+
   addQuery(target) {
   addQuery(target) {
     target.refId = this.dashboard.getNextQueryLetter(this.panel);
     target.refId = this.dashboard.getNextQueryLetter(this.panel);
 
 

+ 10 - 0
public/app/features/panel/panel_ctrl.ts

@@ -22,6 +22,7 @@ export class PanelCtrl {
   editorTabs: any;
   editorTabs: any;
   $scope: any;
   $scope: any;
   $injector: any;
   $injector: any;
+  $location: any;
   $timeout: any;
   $timeout: any;
   fullscreen: boolean;
   fullscreen: boolean;
   inspector: any;
   inspector: any;
@@ -35,6 +36,7 @@ export class PanelCtrl {
 
 
   constructor($scope, $injector) {
   constructor($scope, $injector) {
     this.$injector = $injector;
     this.$injector = $injector;
+    this.$location = $injector.get('$location');
     this.$scope = $scope;
     this.$scope = $scope;
     this.$timeout = $injector.get('$timeout');
     this.$timeout = $injector.get('$timeout');
     this.editorTabIndex = 0;
     this.editorTabIndex = 0;
@@ -161,6 +163,9 @@ export class PanelCtrl {
       shortcut: 'p s',
       shortcut: 'p s',
     });
     });
 
 
+    // Additional items from sub-class
+    menu.push(...this.getAdditionalMenuItems());
+
     let extendedMenu = this.getExtendedMenu();
     let extendedMenu = this.getExtendedMenu();
     menu.push({
     menu.push({
       text: 'More ...',
       text: 'More ...',
@@ -209,6 +214,11 @@ export class PanelCtrl {
     return menu;
     return menu;
   }
   }
 
 
+  // Override in sub-class to add items before extended menu
+  getAdditionalMenuItems() {
+    return [];
+  }
+
   otherPanelInFullscreenMode() {
   otherPanelInFullscreenMode() {
     return this.dashboard.meta.fullscreen && !this.fullscreen;
     return this.dashboard.meta.fullscreen && !this.fullscreen;
   }
   }

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

@@ -19,6 +19,7 @@ export class PrometheusDatasource {
   type: string;
   type: string;
   editorSrc: string;
   editorSrc: string;
   name: string;
   name: string;
+  supportsExplore: boolean;
   supportMetrics: boolean;
   supportMetrics: boolean;
   url: string;
   url: string;
   directUrl: string;
   directUrl: string;
@@ -34,6 +35,7 @@ export class PrometheusDatasource {
     this.type = 'prometheus';
     this.type = 'prometheus';
     this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
     this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
     this.name = instanceSettings.name;
     this.name = instanceSettings.name;
+    this.supportsExplore = true;
     this.supportMetrics = true;
     this.supportMetrics = true;
     this.url = instanceSettings.url;
     this.url = instanceSettings.url;
     this.directUrl = instanceSettings.directUrl;
     this.directUrl = instanceSettings.directUrl;
@@ -324,6 +326,21 @@ export class PrometheusDatasource {
     });
     });
   }
   }
 
 
+  getExploreState(panel) {
+    let state = {};
+    if (panel.targets) {
+      const queries = panel.targets.map(t => ({
+        query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr),
+        format: t.format,
+      }));
+      state = {
+        ...state,
+        queries,
+      };
+    }
+    return state;
+  }
+
   getPrometheusTime(date, roundUp) {
   getPrometheusTime(date, roundUp) {
     if (_.isString(date)) {
     if (_.isString(date)) {
       date = dateMath.parse(date, roundUp);
       date = dateMath.parse(date, roundUp);

+ 1 - 1
public/app/plugins/panel/pluginlist/module.ts

@@ -12,7 +12,7 @@ class PluginListCtrl extends PanelCtrl {
   panelDefaults = {};
   panelDefaults = {};
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor($scope, $injector, private backendSrv, private $location) {
+  constructor($scope, $injector, private backendSrv) {
     super($scope, $injector);
     super($scope, $injector);
 
 
     _.defaults(this.panel, this.panelDefaults);
     _.defaults(this.panel, this.panelDefaults);

+ 1 - 1
public/app/plugins/panel/singlestat/module.ts

@@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
   };
   };
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor($scope, $injector, private $location, private linkSrv) {
+  constructor($scope, $injector, private linkSrv) {
     super($scope, $injector);
     super($scope, $injector);
     _.defaults(this.panel, this.panelDefaults);
     _.defaults(this.panel, this.panelDefaults);
 
 

+ 1 - 0
public/app/routes/ReactContainer.tsx

@@ -29,6 +29,7 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso
       const props = {
       const props = {
         backendSrv: backendSrv,
         backendSrv: backendSrv,
         datasourceSrv: datasourceSrv,
         datasourceSrv: datasourceSrv,
+        routeParams: $route.current.params,
       };
       };
 
 
       ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
       ReactDOM.render(WrapInProvider(store, component, props), elem[0]);

+ 1 - 1
public/app/routes/routes.ts

@@ -111,7 +111,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'FolderDashboardsCtrl',
       controller: 'FolderDashboardsCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',
     })
     })
-    .when('/explore', {
+    .when('/explore/:initial?', {
       template: '<react-container />',
       template: '<react-container />',
       resolve: {
       resolve: {
         component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'),
         component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'),