mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #11770 from grafana/davkal/explore-panel-link
Explore: Add entry to panel menu to jump to Explore
This commit is contained in:
commit
1236b7b918
@ -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 });
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
export default { stripBaseFromUrl };
|
||||||
stripBaseFromUrl: _stripBaseFromUrl,
|
|
||||||
};
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** @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
|
|||||||
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) {
|
|||||||
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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user