Commit 1236b7b9 by David Committed by GitHub

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

Explore: Add entry to panel menu to jump to Explore
parents b170bde7 8a53ec61
...@@ -10,6 +10,7 @@ import Graph from './Graph'; ...@@ -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) { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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 });
} }
......
...@@ -6,13 +6,16 @@ class QueryRow extends PureComponent<any, any> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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>
); );
} }
} }
...@@ -3,6 +3,7 @@ import _ from 'lodash'; ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
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 => { ...@@ -9,6 +14,4 @@ const _stripBaseFromUrl = url => {
return urlWithoutBase; return urlWithoutBase;
}; };
export default { export default { stripBaseFromUrl };
stripBaseFromUrl: _stripBaseFromUrl,
};
...@@ -6,6 +6,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; ...@@ -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 { ...@@ -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);
......
...@@ -22,6 +22,7 @@ export class PanelCtrl { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
......
...@@ -19,6 +19,7 @@ export class PrometheusDatasource { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
...@@ -12,7 +12,7 @@ class PluginListCtrl extends PanelCtrl { ...@@ -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);
......
...@@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -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);
......
...@@ -29,6 +29,7 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso ...@@ -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]);
......
...@@ -111,7 +111,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { ...@@ -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'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment