diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index ba1cb6807a6..40261ee635a 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -10,6 +10,7 @@ import Graph from './Graph'; import Table from './Table'; 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) => { @@ -43,7 +44,7 @@ function parseInitialQueries(initial) { return []; } try { - const parsed = JSON.parse(initial); + const parsed = JSON.parse(decodePathComponent(initial)); return parsed.queries.map(q => q.query); } catch (e) { console.error(e); diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 55d968fd981..94bf9efb31b 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import { encodePathComponent } from 'app/core/utils/location_util'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -13,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location) { + constructor(private $rootScope, private $location, private datasourceSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { 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 this.bind('p r', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index f8d6aa4ee5f..735272285ff 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,6 +1,11 @@ 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 = ''; +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; const urlWithoutBase = @@ -9,6 +14,4 @@ const _stripBaseFromUrl = url => { return urlWithoutBase; }; -export default { - stripBaseFromUrl: _stripBaseFromUrl, -}; +export default { stripBaseFromUrl }; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 9e9598e1732..acf46a193e8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/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 dateMath from 'app/core/utils/datemath'; +import { encodePathComponent } from 'app/core/utils/location_util'; import { metricsTabDirective } from './metrics_tab'; @@ -309,6 +310,24 @@ class MetricsPanelCtrl extends PanelCtrl { 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) { target.refId = this.dashboard.getNextQueryLetter(this.panel); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index cdd2bc2be60..67725ec5fec 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -99,12 +99,6 @@ export class PanelCtrl { this.changeView(false, false); } - explore() { - // TS hack :< - const initialState = JSON.stringify(this['datasource'].getExploreState(this.panel)); - this.$location.url(`/explore/${initialState}`); - } - initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); @@ -162,16 +156,6 @@ export class PanelCtrl { }); } - // TS hack :< - if ('datasource' in this && this['datasource'].supportsExplore) { - menu.push({ - text: 'Explore', - click: 'ctrl.explore();', - icon: 'fa fa-fw fa-rocket', - shortcut: 'x', - }); - } - menu.push({ text: 'Share', click: 'ctrl.sharePanel();', @@ -179,6 +163,9 @@ export class PanelCtrl { shortcut: 'p s', }); + // Additional items from sub-class + menu.push(...this.getAdditionalMenuItems()); + let extendedMenu = this.getExtendedMenu(); menu.push({ text: 'More ...', @@ -227,6 +214,11 @@ export class PanelCtrl { return menu; } + // Override in sub-class to add items before extended menu + getAdditionalMenuItems() { + return []; + } + otherPanelInFullscreenMode() { return this.dashboard.meta.fullscreen && !this.fullscreen; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 221bffbda67..5d9510b4238 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -326,11 +326,18 @@ export class PrometheusDatasource { } getExploreState(panel) { - if (!panel.targets) { - return {}; + 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, + }; } - const queries = panel.targets.map(t => ({ query: t.expr, format: t.format })); - return { queries }; + return state; } getPrometheusTime(date, roundUp) {