diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index eae3f4d0a1f..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) => { @@ -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 { datasource: any; datasourceError: any; @@ -58,6 +72,7 @@ export class Explore extends React.Component { constructor(props) { super(props); + const initialQueries = parseInitialQueries(props.routeParams.initial); this.state = { datasource: null, datasourceError: null, @@ -65,7 +80,7 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, - queries: ensureQueries(), + queries: ensureQueries(initialQueries), requestOptions: null, showingGraph: true, showingTable: true, @@ -77,7 +92,7 @@ export class Explore extends React.Component { const datasource = await this.props.datasourceSrv.get(); const testResult = await datasource.testDatasource(); if (testResult.status === 'success') { - this.setState({ datasource, datasourceError: null, datasourceLoading: false }); + this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit()); } else { this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); } diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a0a9981368d..3940d16b2f6 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -6,13 +6,16 @@ class QueryRow extends PureComponent { constructor(props) { super(props); this.state = { - query: '', + edited: false, + query: props.query || '', }; } handleChangeQuery = value => { const { index, onChangeQuery } = this.props; - this.setState({ query: value }); + const { query } = this.state; + const edited = query !== value; + this.setState({ edited, query: value }); if (onChangeQuery) { onChangeQuery(value, index); } @@ -41,6 +44,7 @@ class QueryRow extends PureComponent { render() { const { request } = this.props; + const { edited, query } = this.state; return (
@@ -52,7 +56,12 @@ class QueryRow extends PureComponent {
- +
); @@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent { render() { const { className = '', queries, ...handlers } = this.props; return ( -
{queries.map((q, index) => )}
+
+ {queries.map((q, index) => )} +
); } } 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 da8adc4f908..67725ec5fec 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -22,6 +22,7 @@ export class PanelCtrl { editorTabs: any; $scope: any; $injector: any; + $location: any; $timeout: any; fullscreen: boolean; inspector: any; @@ -35,6 +36,7 @@ export class PanelCtrl { constructor($scope, $injector) { this.$injector = $injector; + this.$location = $injector.get('$location'); this.$scope = $scope; this.$timeout = $injector.get('$timeout'); this.editorTabIndex = 0; @@ -161,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 ...', @@ -209,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 6d654438271..1820cb1306d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -19,6 +19,7 @@ export class PrometheusDatasource { type: string; editorSrc: string; name: string; + supportsExplore: boolean; supportMetrics: boolean; url: string; directUrl: string; @@ -34,6 +35,7 @@ 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; @@ -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) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index e97b1a8fbf9..acfa69b171c 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -12,7 +12,7 @@ class PluginListCtrl extends PanelCtrl { panelDefaults = {}; /** @ngInject */ - constructor($scope, $injector, private backendSrv, private $location) { + constructor($scope, $injector, private backendSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c7f523a9591..28d3f308d68 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private $location, private linkSrv) { + constructor($scope, $injector, private linkSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index d6d34372090..db6938cc878 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -29,6 +29,7 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, + routeParams: $route.current.params, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 49690561728..6a61315f956 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -111,7 +111,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) - .when('/explore', { + .when('/explore/:initial?', { template: '', resolve: { component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'),