Commit 321c09ae by David Committed by GitHub

Merge pull request #13416 from grafana/davkal/11999-explore-from-mixed-panels

Explore: jump to explore from panels with mixed datasources
parents f37a60dc 68dfc569
...@@ -4,7 +4,7 @@ import _ from 'lodash'; ...@@ -4,7 +4,7 @@ import _ from 'lodash';
import config from 'app/core/config'; import config from 'app/core/config';
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 { renderUrl } from 'app/core/utils/url'; import { getExploreUrl } from 'app/core/utils/explore';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind'; import 'mousetrap-global-bind';
...@@ -15,7 +15,14 @@ export class KeybindingSrv { ...@@ -15,7 +15,14 @@ export class KeybindingSrv {
timepickerOpen = false; timepickerOpen = false;
/** @ngInject */ /** @ngInject */
constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { constructor(
private $rootScope,
private $location,
private $timeout,
private datasourceSrv,
private timeSrv,
private contextSrv
) {
// clear out all shortcuts on route change // clear out all shortcuts on route change
$rootScope.$on('$routeChangeSuccess', () => { $rootScope.$on('$routeChangeSuccess', () => {
Mousetrap.reset(); Mousetrap.reset();
...@@ -194,14 +201,9 @@ export class KeybindingSrv { ...@@ -194,14 +201,9 @@ export class KeybindingSrv {
if (dashboard.meta.focusPanelId) { if (dashboard.meta.focusPanelId) {
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
const datasource = await this.datasourceSrv.get(panel.datasource); const datasource = await this.datasourceSrv.get(panel.datasource);
if (datasource && datasource.supportsExplore) { const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv);
const range = this.timeSrv.timeRangeForUrl(); if (url) {
const state = { this.$timeout(() => this.$location.url(url));
...datasource.getExploreState(panel),
range,
};
const exploreState = JSON.stringify(state);
this.$location.url(renderUrl('/explore', { state: exploreState }));
} }
} }
}); });
......
import { serializeStateToUrlParam, parseUrlState } from './Wrapper'; import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore';
import { DEFAULT_RANGE } from './TimePicker'; import { ExploreState } from 'app/types/explore';
import { ExploreState } from './Explore';
const DEFAULT_EXPLORE_STATE: ExploreState = { const DEFAULT_EXPLORE_STATE: ExploreState = {
datasource: null, datasource: null,
...@@ -27,7 +26,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { ...@@ -27,7 +26,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
tableResult: null, tableResult: null,
}; };
describe('Wrapper state functions', () => { describe('state functions', () => {
describe('parseUrlState', () => { describe('parseUrlState', () => {
it('returns default state on empty string', () => { it('returns default state on empty string', () => {
expect(parseUrlState('')).toMatchObject({ expect(parseUrlState('')).toMatchObject({
...@@ -57,7 +56,7 @@ describe('Wrapper state functions', () => { ...@@ -57,7 +56,7 @@ describe('Wrapper state functions', () => {
}; };
expect(serializeStateToUrlParam(state)).toBe( expect(serializeStateToUrlParam(state)).toBe(
'{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' + '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' +
'{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}' '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now - 5h","to":"now"}}'
); );
}); });
}); });
......
import { renderUrl } from 'app/core/utils/url';
import { ExploreState, ExploreUrlState } from 'app/types/explore';
export const DEFAULT_RANGE = {
from: 'now-6h',
to: 'now',
};
/**
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
*
* @param panel Origin panel of the jump to Explore
* @param panelTargets The origin panel's query targets
* @param panelDatasource The origin panel's datasource
* @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
* @param timeSrv Time service to get the current dashboard range from
*/
export async function getExploreUrl(
panel: any,
panelTargets: any[],
panelDatasource: any,
datasourceSrv: any,
timeSrv: any
) {
let exploreDatasource = panelDatasource;
let exploreTargets = panelTargets;
let url;
// Mixed datasources need to choose only one datasource
if (panelDatasource.meta.id === 'mixed' && panelTargets) {
// Find first explore datasource among targets
let mixedExploreDatasource;
for (const t of panel.targets) {
const datasource = await datasourceSrv.get(t.datasource);
if (datasource && datasource.meta.explore) {
mixedExploreDatasource = datasource;
break;
}
}
// Add all its targets
if (mixedExploreDatasource) {
exploreDatasource = mixedExploreDatasource;
exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name);
}
}
if (exploreDatasource && exploreDatasource.meta.explore) {
const range = timeSrv.timeRangeForUrl();
const state = {
...exploreDatasource.getExploreState(exploreTargets),
range,
};
const exploreState = JSON.stringify(state);
url = renderUrl('/explore', { state: exploreState });
}
return url;
}
export function parseUrlState(initial: string | undefined): ExploreUrlState {
if (initial) {
try {
return JSON.parse(decodeURI(initial));
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
export function serializeStateToUrlParam(state: ExploreState): string {
const urlState: ExploreUrlState = {
datasource: state.datasourceName,
queries: state.queries.map(q => ({ query: q.query })),
range: state.range,
};
return JSON.stringify(urlState);
}
import config from 'app/core/config'; import config from 'app/core/config';
// 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 => { export const stripBaseFromUrl = url => {
const appSubUrl = config.appSubUrl; const appSubUrl = config.appSubUrl;
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
......
...@@ -2,19 +2,20 @@ import React from 'react'; ...@@ -2,19 +2,20 @@ import React from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import Select from 'react-select'; import Select from 'react-select';
import { Query, Range, ExploreUrlState } from 'app/types/explore'; import { ExploreState, ExploreUrlState } from 'app/types/explore';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors'; import colors from 'app/core/utils/colors';
import store from 'app/core/store'; import store from 'app/core/store';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import { parse as parseDate } from 'app/core/utils/datemath'; import { parse as parseDate } from 'app/core/utils/datemath';
import { DEFAULT_RANGE } from 'app/core/utils/explore';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import Graph from './Graph'; import Graph from './Graph';
import Logs from './Logs'; import Logs from './Logs';
import Table from './Table'; import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker'; import TimePicker from './TimePicker';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
...@@ -58,31 +59,6 @@ interface ExploreProps { ...@@ -58,31 +59,6 @@ interface ExploreProps {
urlState: ExploreUrlState; urlState: ExploreUrlState;
} }
export interface ExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
datasourceName?: string;
graphResult: any;
history: any[];
latency: number;
loading: any;
logsResult: any;
queries: Query[];
queryErrors: any[];
queryHints: any[];
range: Range;
requestOptions: any;
showingGraph: boolean;
showingLogs: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult: any;
}
export class Explore extends React.PureComponent<ExploreProps, ExploreState> { export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
el: any; el: any;
......
...@@ -5,7 +5,6 @@ import * as dateMath from 'app/core/utils/datemath'; ...@@ -5,7 +5,6 @@ import * as dateMath from 'app/core/utils/datemath';
import * as rangeUtil from 'app/core/utils/rangeutil'; import * as rangeUtil from 'app/core/utils/rangeutil';
const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
to: 'now', to: 'now',
......
...@@ -3,31 +3,11 @@ import { hot } from 'react-hot-loader'; ...@@ -3,31 +3,11 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { ExploreUrlState } from 'app/types/explore'; import { ExploreState } from 'app/types/explore';
import Explore, { ExploreState } from './Explore'; import Explore from './Explore';
import { DEFAULT_RANGE } from './TimePicker';
export function parseUrlState(initial: string | undefined): ExploreUrlState {
if (initial) {
try {
return JSON.parse(decodeURI(initial));
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
export function serializeStateToUrlParam(state: ExploreState): string {
const urlState: ExploreUrlState = {
datasource: state.datasourceName,
queries: state.queries.map(q => ({ query: q.query })),
range: state.range,
};
return JSON.stringify(urlState);
}
interface WrapperProps { interface WrapperProps {
backendSrv?: any; backendSrv?: any;
......
...@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn'; ...@@ -6,7 +6,7 @@ import kbn from 'app/core/utils/kbn';
import { PanelCtrl } from 'app/features/panel/panel_ctrl'; 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 { renderUrl } from 'app/core/utils/url'; import { getExploreUrl } from 'app/core/utils/explore';
import { metricsTabDirective } from './metrics_tab'; import { metricsTabDirective } from './metrics_tab';
...@@ -314,7 +314,12 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -314,7 +314,12 @@ class MetricsPanelCtrl extends PanelCtrl {
getAdditionalMenuItems() { getAdditionalMenuItems() {
const items = []; const items = [];
if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) { if (
config.exploreEnabled &&
this.contextSrv.isEditor &&
this.datasource &&
(this.datasource.meta.explore || this.datasource.meta.id === 'mixed')
) {
items.push({ items.push({
text: 'Explore', text: 'Explore',
click: 'ctrl.explore();', click: 'ctrl.explore();',
...@@ -325,14 +330,11 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -325,14 +330,11 @@ class MetricsPanelCtrl extends PanelCtrl {
return items; return items;
} }
explore() { async explore() {
const range = this.timeSrv.timeRangeForUrl(); const url = await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv);
const state = { if (url) {
...this.datasource.getExploreState(this.panel), this.$timeout(() => this.$location.url(url));
range, }
};
const exploreState = JSON.stringify(state);
this.$location.url(renderUrl('/explore', { state: exploreState }));
} }
addQuery(target) { addQuery(target) {
......
...@@ -38,7 +38,7 @@ describe('MetricsPanelCtrl', () => { ...@@ -38,7 +38,7 @@ describe('MetricsPanelCtrl', () => {
describe('and has datasource set that supports explore and user has powers', () => { describe('and has datasource set that supports explore and user has powers', () => {
beforeEach(() => { beforeEach(() => {
ctrl.contextSrv = { isEditor: true }; ctrl.contextSrv = { isEditor: true };
ctrl.datasource = { supportsExplore: true }; ctrl.datasource = { meta: { explore: true } };
additionalItems = ctrl.getAdditionalMenuItems(); additionalItems = ctrl.getAdditionalMenuItems();
}); });
......
...@@ -8,7 +8,6 @@ import * as templatingVariable from 'app/features/templating/variable'; ...@@ -8,7 +8,6 @@ import * as templatingVariable from 'app/features/templating/variable';
export default class CloudWatchDatasource { export default class CloudWatchDatasource {
type: any; type: any;
name: any; name: any;
supportMetrics: any;
proxyUrl: any; proxyUrl: any;
defaultRegion: any; defaultRegion: any;
instanceSettings: any; instanceSettings: any;
...@@ -17,7 +16,6 @@ export default class CloudWatchDatasource { ...@@ -17,7 +16,6 @@ export default class CloudWatchDatasource {
constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) {
this.type = 'cloudwatch'; this.type = 'cloudwatch';
this.name = instanceSettings.name; this.name = instanceSettings.name;
this.supportMetrics = true;
this.proxyUrl = instanceSettings.url; this.proxyUrl = instanceSettings.url;
this.defaultRegion = instanceSettings.jsonData.defaultRegion; this.defaultRegion = instanceSettings.jsonData.defaultRegion;
this.instanceSettings = instanceSettings; this.instanceSettings = instanceSettings;
......
...@@ -16,8 +16,6 @@ export default class InfluxDatasource { ...@@ -16,8 +16,6 @@ export default class InfluxDatasource {
basicAuth: any; basicAuth: any;
withCredentials: any; withCredentials: any;
interval: any; interval: any;
supportAnnotations: boolean;
supportMetrics: boolean;
responseParser: any; responseParser: any;
/** @ngInject */ /** @ngInject */
...@@ -34,8 +32,6 @@ export default class InfluxDatasource { ...@@ -34,8 +32,6 @@ export default class InfluxDatasource {
this.basicAuth = instanceSettings.basicAuth; this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials; this.withCredentials = instanceSettings.withCredentials;
this.interval = (instanceSettings.jsonData || {}).timeInterval; this.interval = (instanceSettings.jsonData || {}).timeInterval;
this.supportAnnotations = true;
this.supportMetrics = true;
this.responseParser = new ResponseParser(); this.responseParser = new ResponseParser();
} }
......
...@@ -10,7 +10,6 @@ export default class OpenTsDatasource { ...@@ -10,7 +10,6 @@ export default class OpenTsDatasource {
basicAuth: any; basicAuth: any;
tsdbVersion: any; tsdbVersion: any;
tsdbResolution: any; tsdbResolution: any;
supportMetrics: any;
tagKeys: any; tagKeys: any;
aggregatorsPromise: any; aggregatorsPromise: any;
...@@ -26,7 +25,6 @@ export default class OpenTsDatasource { ...@@ -26,7 +25,6 @@ export default class OpenTsDatasource {
instanceSettings.jsonData = instanceSettings.jsonData || {}; instanceSettings.jsonData = instanceSettings.jsonData || {};
this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1; this.tsdbVersion = instanceSettings.jsonData.tsdbVersion || 1;
this.tsdbResolution = instanceSettings.jsonData.tsdbResolution || 1; this.tsdbResolution = instanceSettings.jsonData.tsdbResolution || 1;
this.supportMetrics = true;
this.tagKeys = {}; this.tagKeys = {};
this.aggregatorsPromise = null; this.aggregatorsPromise = null;
......
...@@ -149,8 +149,6 @@ export class PrometheusDatasource { ...@@ -149,8 +149,6 @@ export class PrometheusDatasource {
editorSrc: string; editorSrc: string;
name: string; name: string;
ruleMappings: { [index: string]: string }; ruleMappings: { [index: string]: string };
supportsExplore: boolean;
supportMetrics: boolean;
url: string; url: string;
directUrl: string; directUrl: string;
basicAuth: any; basicAuth: any;
...@@ -166,8 +164,6 @@ export class PrometheusDatasource { ...@@ -166,8 +164,6 @@ 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.url = instanceSettings.url; this.url = instanceSettings.url;
this.directUrl = instanceSettings.directUrl; this.directUrl = instanceSettings.directUrl;
this.basicAuth = instanceSettings.basicAuth; this.basicAuth = instanceSettings.basicAuth;
...@@ -522,10 +518,10 @@ export class PrometheusDatasource { ...@@ -522,10 +518,10 @@ export class PrometheusDatasource {
}); });
} }
getExploreState(panel) { getExploreState(targets: any[]) {
let state = {}; let state = {};
if (panel.targets) { if (targets && targets.length > 0) {
const queries = panel.targets.map(t => ({ const queries = targets.map(t => ({
query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr),
format: t.format, format: t.format,
})); }));
......
...@@ -9,6 +9,31 @@ export interface Query { ...@@ -9,6 +9,31 @@ export interface Query {
key?: string; key?: string;
} }
export interface ExploreState {
datasource: any;
datasourceError: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
datasourceName?: string;
graphResult: any;
history: any[];
latency: number;
loading: any;
logsResult: any;
queries: Query[];
queryErrors: any[];
queryHints: any[];
range: Range;
requestOptions: any;
showingGraph: boolean;
showingLogs: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult: any;
}
export interface ExploreUrlState { export interface ExploreUrlState {
datasource: string; datasource: string;
queries: Query[]; queries: Query[];
......
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