mirror of
https://github.com/grafana/grafana.git
synced 2025-01-26 16:27:02 -06:00
Explore: Adds ability to save a panel's query from Explore (#17982)
* Explore: Adds ability to return to origin dashboard
This commit is contained in:
parent
991f77cee1
commit
a838d2b30a
@ -61,7 +61,7 @@ export class RefreshPicker extends PureComponent<Props> {
|
|||||||
<div className={cssClasses}>
|
<div className={cssClasses}>
|
||||||
<div className="refresh-picker-buttons">
|
<div className="refresh-picker-buttons">
|
||||||
<Tooltip placement="top" content={tooltip}>
|
<Tooltip placement="top" content={tooltip}>
|
||||||
<button className="btn btn--radius-right-0 navbar-button navbar-button--refresh" onClick={onRefresh}>
|
<button className="btn btn--radius-right-0 navbar-button navbar-button--border-right-0" onClick={onRefresh}>
|
||||||
<i className="fa fa-refresh" />
|
<i className="fa fa-refresh" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-button--refresh {
|
.navbar-button--border-right-0 {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ export interface Props<T> {
|
|||||||
onOpenMenu?: () => void;
|
onOpenMenu?: () => void;
|
||||||
onCloseMenu?: () => void;
|
onCloseMenu?: () => void;
|
||||||
tabSelectsValue?: boolean;
|
tabSelectsValue?: boolean;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ButtonSelect<T> extends PureComponent<Props<T>> {
|
export class ButtonSelect<T> extends PureComponent<Props<T>> {
|
||||||
@ -65,14 +66,16 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
|
|||||||
onOpenMenu,
|
onOpenMenu,
|
||||||
onCloseMenu,
|
onCloseMenu,
|
||||||
tabSelectsValue,
|
tabSelectsValue,
|
||||||
|
autoFocus = true,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const combinedComponents = {
|
const combinedComponents = {
|
||||||
...components,
|
...components,
|
||||||
Control: ButtonComponent({ label, className, iconClass }),
|
Control: ButtonComponent({ label, className, iconClass }),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
autoFocus
|
autoFocus={autoFocus}
|
||||||
backspaceRemovesValue={false}
|
backspaceRemovesValue={false}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
@ -263,14 +263,15 @@ export class BackendSrv implements BackendService {
|
|||||||
return this.get(`/api/folders/${uid}`);
|
return this.get(`/api/folders/${uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDashboard(dash: DashboardModel, options: any) {
|
saveDashboard(
|
||||||
options = options || {};
|
dash: DashboardModel,
|
||||||
|
{ message = '', folderId, overwrite = false }: { message?: string; folderId?: number; overwrite?: boolean } = {}
|
||||||
|
) {
|
||||||
return this.post('/api/dashboards/db/', {
|
return this.post('/api/dashboards/db/', {
|
||||||
dashboard: dash,
|
dashboard: dash,
|
||||||
folderId: options.folderId,
|
folderId,
|
||||||
overwrite: options.overwrite === true,
|
overwrite,
|
||||||
message: options.message || '',
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,6 @@ export class KeybindingSrv {
|
|||||||
if (dashboard.meta.focusPanelId) {
|
if (dashboard.meta.focusPanelId) {
|
||||||
appEvents.emit('panel-change-view', {
|
appEvents.emit('panel-change-view', {
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
edit: null,
|
|
||||||
panelId: dashboard.meta.focusPanelId,
|
panelId: dashboard.meta.focusPanelId,
|
||||||
toggle: true,
|
toggle: true,
|
||||||
});
|
});
|
||||||
@ -199,7 +198,7 @@ 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);
|
||||||
const url = await getExploreUrl(panel.targets, datasource, this.datasourceSrv, this.timeSrv);
|
const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv);
|
||||||
if (url) {
|
if (url) {
|
||||||
this.$timeout(() => this.$location.url(url));
|
this.$timeout(() => this.$location.url(url));
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
|||||||
showingLogs: true,
|
showingLogs: true,
|
||||||
dedupStrategy: LogsDedupStrategy.none,
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
},
|
},
|
||||||
|
originPanelId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('state functions', () => {
|
describe('state functions', () => {
|
||||||
|
@ -18,7 +18,7 @@ import { renderUrl } from 'app/core/utils/url';
|
|||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { getNextRefIdChar } from './query';
|
import { getNextRefIdChar } from './query';
|
||||||
// Types
|
// Types
|
||||||
import { DataQuery, DataSourceApi, DataQueryError, DataQueryRequest } from '@grafana/ui';
|
import { DataQuery, DataSourceApi, DataQueryError, DataQueryRequest, PanelModel } from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
ExploreUrlState,
|
ExploreUrlState,
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
@ -29,6 +29,7 @@ import {
|
|||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { PanelQueryState } from '../../features/dashboard/state/PanelQueryState';
|
import { PanelQueryState } from '../../features/dashboard/state/PanelQueryState';
|
||||||
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
from: 'now-1h',
|
from: 'now-1h',
|
||||||
@ -55,7 +56,13 @@ export const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DAT
|
|||||||
* @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
|
* @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
|
* @param timeSrv Time service to get the current dashboard range from
|
||||||
*/
|
*/
|
||||||
export async function getExploreUrl(panelTargets: any[], panelDatasource: any, datasourceSrv: any, timeSrv: any) {
|
export async function getExploreUrl(
|
||||||
|
panel: PanelModel,
|
||||||
|
panelTargets: DataQuery[],
|
||||||
|
panelDatasource: any,
|
||||||
|
datasourceSrv: any,
|
||||||
|
timeSrv: TimeSrv
|
||||||
|
) {
|
||||||
let exploreDatasource = panelDatasource;
|
let exploreDatasource = panelDatasource;
|
||||||
let exploreTargets: DataQuery[] = panelTargets;
|
let exploreTargets: DataQuery[] = panelTargets;
|
||||||
let url: string;
|
let url: string;
|
||||||
@ -86,7 +93,7 @@ export async function getExploreUrl(panelTargets: any[], panelDatasource: any, d
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exploreState = JSON.stringify(state);
|
const exploreState = JSON.stringify({ ...state, originPanelId: panel.id });
|
||||||
url = renderUrl('/explore', { left: exploreState });
|
url = renderUrl('/explore', { left: exploreState });
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
@ -198,6 +205,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
range: DEFAULT_RANGE,
|
range: DEFAULT_RANGE,
|
||||||
ui: DEFAULT_UI_STATE,
|
ui: DEFAULT_UI_STATE,
|
||||||
mode: null,
|
mode: null,
|
||||||
|
originPanelId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
@ -234,7 +242,8 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
}
|
}
|
||||||
: DEFAULT_UI_STATE;
|
: DEFAULT_UI_STATE;
|
||||||
|
|
||||||
return { datasource, queries, range, ui, mode };
|
const originPanelId = parsedSegments.filter(segment => isSegment(segment, 'originPanelId'))[0];
|
||||||
|
return { datasource, queries, range, ui, mode, originPanelId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { DashboardSrv } from '../../services/DashboardSrv';
|
import { DashboardSrv } from '../../services/DashboardSrv';
|
||||||
|
import { CloneOptions } from '../../state/DashboardModel';
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -95,7 +96,7 @@ export class SaveDashboardModalCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: any = {
|
const options: CloneOptions = {
|
||||||
saveVariables: this.saveVariables,
|
saveVariables: this.saveVariables,
|
||||||
saveTimerange: this.saveTimerange,
|
saveTimerange: this.saveTimerange,
|
||||||
message: this.message,
|
message: this.message,
|
||||||
@ -105,11 +106,10 @@ export class SaveDashboardModalCtrl {
|
|||||||
const saveModel = dashboard.getSaveModelClone(options);
|
const saveModel = dashboard.getSaveModelClone(options);
|
||||||
|
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
|
||||||
return this.dashboardSrv.save(saveModel, options).then(this.postSave.bind(this, options));
|
return this.dashboardSrv.save(saveModel, options).then(this.postSave.bind(this, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
postSave(options: any) {
|
postSave(options?: { saveVariables?: boolean; saveTimerange?: boolean }) {
|
||||||
if (options.saveVariables) {
|
if (options.saveVariables) {
|
||||||
this.dashboardSrv.getCurrent().resetOriginalVariables();
|
this.dashboardSrv.getCurrent().resetOriginalVariables();
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class ChangeTracker {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (originalCopyDelay) {
|
if (originalCopyDelay && !dashboard.meta.fromExplore) {
|
||||||
this.$timeout(() => {
|
this.$timeout(() => {
|
||||||
// wait for different services to patch the dashboard (missing properties)
|
// wait for different services to patch the dashboard (missing properties)
|
||||||
this.original = dashboard.getSaveModelClone();
|
this.original = dashboard.getSaveModelClone();
|
||||||
|
@ -7,6 +7,13 @@ import { DashboardMeta } from 'app/types';
|
|||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { ILocationService } from 'angular';
|
import { ILocationService } from 'angular';
|
||||||
|
|
||||||
|
interface DashboardSaveOptions {
|
||||||
|
folderId?: number;
|
||||||
|
overwrite?: boolean;
|
||||||
|
message?: string;
|
||||||
|
makeEditable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class DashboardSrv {
|
export class DashboardSrv {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
|
|
||||||
@ -37,54 +44,53 @@ export class DashboardSrv {
|
|||||||
removePanel(dashboard, dashboard.getPanelById(panelId), true);
|
removePanel(dashboard, dashboard.getPanelById(panelId), true);
|
||||||
};
|
};
|
||||||
|
|
||||||
onPanelChangeView = (options: any) => {
|
onPanelChangeView = ({
|
||||||
|
fullscreen = false,
|
||||||
|
edit = false,
|
||||||
|
panelId,
|
||||||
|
}: {
|
||||||
|
fullscreen?: boolean;
|
||||||
|
edit?: boolean;
|
||||||
|
panelId?: number;
|
||||||
|
}) => {
|
||||||
const urlParams = this.$location.search();
|
const urlParams = this.$location.search();
|
||||||
|
|
||||||
// handle toggle logic
|
// handle toggle logic
|
||||||
if (options.fullscreen === urlParams.fullscreen) {
|
// I hate using these truthy converters (!!) but in this case
|
||||||
// I hate using these truthy converters (!!) but in this case
|
// I think it's appropriate. edit can be null/false/undefined and
|
||||||
// I think it's appropriate. edit can be null/false/undefined and
|
// here i want all of those to compare the same
|
||||||
// here i want all of those to compare the same
|
if (fullscreen === urlParams.fullscreen && edit === !!urlParams.edit) {
|
||||||
if (!!options.edit === !!urlParams.edit) {
|
const paramsToRemove = ['fullscreen', 'edit', 'panelId', 'tab'];
|
||||||
delete urlParams.fullscreen;
|
for (const key of paramsToRemove) {
|
||||||
delete urlParams.edit;
|
delete urlParams[key];
|
||||||
delete urlParams.panelId;
|
|
||||||
delete urlParams.tab;
|
|
||||||
this.$location.search(urlParams);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$location.search(urlParams);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.fullscreen) {
|
const newUrlParams = {
|
||||||
urlParams.fullscreen = true;
|
...urlParams,
|
||||||
} else {
|
fullscreen: fullscreen || undefined,
|
||||||
delete urlParams.fullscreen;
|
edit: edit || undefined,
|
||||||
}
|
tab: edit ? urlParams.tab : undefined,
|
||||||
|
panelId,
|
||||||
|
};
|
||||||
|
|
||||||
if (options.edit) {
|
Object.keys(newUrlParams).forEach(key => {
|
||||||
urlParams.edit = true;
|
if (newUrlParams[key] === undefined) {
|
||||||
} else {
|
delete newUrlParams[key];
|
||||||
delete urlParams.edit;
|
}
|
||||||
delete urlParams.tab;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (options.panelId || options.panelId === 0) {
|
this.$location.search(newUrlParams);
|
||||||
urlParams.panelId = options.panelId;
|
|
||||||
} else {
|
|
||||||
delete urlParams.panelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$location.search(urlParams);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSaveDashboardError(
|
handleSaveDashboardError(
|
||||||
clone: any,
|
clone: any,
|
||||||
options: { overwrite?: any },
|
options: DashboardSaveOptions,
|
||||||
err: { data: { status: string; message: any }; isHandled: boolean }
|
err: { data: { status: string; message: any }; isHandled: boolean }
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
|
||||||
options.overwrite = true;
|
|
||||||
|
|
||||||
if (err.data && err.data.status === 'version-mismatch') {
|
if (err.data && err.data.status === 'version-mismatch') {
|
||||||
err.isHandled = true;
|
err.isHandled = true;
|
||||||
|
|
||||||
@ -129,16 +135,16 @@ export class DashboardSrv {
|
|||||||
this.showSaveAsModal();
|
this.showSaveAsModal();
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
this.save(clone, { overwrite: true });
|
this.save(clone, { ...options, overwrite: true });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postSave(clone: DashboardModel, data: { version: number; url: string }) {
|
postSave(data: { version: number; url: string }) {
|
||||||
this.dashboard.version = data.version;
|
this.dashboard.version = data.version;
|
||||||
|
|
||||||
// important that these happens before location redirect below
|
// important that these happen before location redirect below
|
||||||
this.$rootScope.appEvent('dashboard-saved', this.dashboard);
|
this.$rootScope.appEvent('dashboard-saved', this.dashboard);
|
||||||
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
this.$rootScope.appEvent('alert-success', ['Dashboard saved']);
|
||||||
|
|
||||||
@ -152,17 +158,19 @@ export class DashboardSrv {
|
|||||||
return this.dashboard;
|
return this.dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(clone: any, options: { overwrite?: any; folderId?: any }) {
|
save(clone: any, options?: DashboardSaveOptions) {
|
||||||
options = options || {};
|
|
||||||
options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId;
|
options.folderId = options.folderId >= 0 ? options.folderId : this.dashboard.meta.folderId || clone.folderId;
|
||||||
|
|
||||||
return this.backendSrv
|
return this.backendSrv
|
||||||
.saveDashboard(clone, options)
|
.saveDashboard(clone, options)
|
||||||
.then(this.postSave.bind(this, clone))
|
.then((data: any) => this.postSave(data))
|
||||||
.catch(this.handleSaveDashboardError.bind(this, clone, options));
|
.catch(this.handleSaveDashboardError.bind(this, clone, { folderId: options.folderId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDashboard(options?: { overwrite?: any; folderId?: any; makeEditable?: any }, clone?: DashboardModel) {
|
saveDashboard(
|
||||||
|
clone?: DashboardModel,
|
||||||
|
{ makeEditable = false, folderId, overwrite = false, message }: DashboardSaveOptions = {}
|
||||||
|
) {
|
||||||
if (clone) {
|
if (clone) {
|
||||||
this.setCurrent(this.create(clone, this.dashboard.meta));
|
this.setCurrent(this.create(clone, this.dashboard.meta));
|
||||||
}
|
}
|
||||||
@ -171,7 +179,7 @@ export class DashboardSrv {
|
|||||||
return this.showDashboardProvisionedModal();
|
return this.showDashboardProvisionedModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.dashboard.meta.canSave && options.makeEditable !== true) {
|
if (!(this.dashboard.meta.canSave || makeEditable)) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +191,7 @@ export class DashboardSrv {
|
|||||||
return this.showSaveModal();
|
return this.showSaveModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.save(this.dashboard.getSaveModelClone(), options);
|
return this.save(this.dashboard.getSaveModelClone(), { folderId, overwrite, message });
|
||||||
}
|
}
|
||||||
|
|
||||||
saveJSONDashboard(json: string) {
|
saveJSONDashboard(json: string) {
|
||||||
|
@ -4,6 +4,7 @@ import { initDashboard, InitDashboardArgs } from './initDashboard';
|
|||||||
import { DashboardRouteInfo } from 'app/types';
|
import { DashboardRouteInfo } from 'app/types';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { dashboardInitFetching, dashboardInitCompleted, dashboardInitServices } from './actions';
|
import { dashboardInitFetching, dashboardInitCompleted, dashboardInitServices } from './actions';
|
||||||
|
import { resetExploreAction } from 'app/features/explore/state/actionTypes';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ interface ScenarioContext {
|
|||||||
unsavedChangesSrv: any;
|
unsavedChangesSrv: any;
|
||||||
variableSrv: any;
|
variableSrv: any;
|
||||||
dashboardSrv: any;
|
dashboardSrv: any;
|
||||||
|
loaderSrv: any;
|
||||||
keybindingSrv: any;
|
keybindingSrv: any;
|
||||||
backendSrv: any;
|
backendSrv: any;
|
||||||
setup: (fn: () => void) => void;
|
setup: (fn: () => void) => void;
|
||||||
@ -33,6 +35,33 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
const variableSrv = { init: jest.fn() };
|
const variableSrv = { init: jest.fn() };
|
||||||
const dashboardSrv = { setCurrent: jest.fn() };
|
const dashboardSrv = { setCurrent: jest.fn() };
|
||||||
const keybindingSrv = { setupDashboardBindings: jest.fn() };
|
const keybindingSrv = { setupDashboardBindings: jest.fn() };
|
||||||
|
const loaderSrv = {
|
||||||
|
loadDashboard: jest.fn(() => ({
|
||||||
|
meta: {
|
||||||
|
canStar: false,
|
||||||
|
canShare: false,
|
||||||
|
isNew: true,
|
||||||
|
folderId: 0,
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
title: 'My cool dashboard',
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
type: 'add-panel',
|
||||||
|
gridPos: { x: 0, y: 0, w: 12, h: 9 },
|
||||||
|
title: 'Panel Title',
|
||||||
|
id: 2,
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
expr: 'old expr',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const injectorMock = {
|
const injectorMock = {
|
||||||
get: (name: string) => {
|
get: (name: string) => {
|
||||||
@ -41,6 +70,8 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
return timeSrv;
|
return timeSrv;
|
||||||
case 'annotationsSrv':
|
case 'annotationsSrv':
|
||||||
return annotationsSrv;
|
return annotationsSrv;
|
||||||
|
case 'dashboardLoaderSrv':
|
||||||
|
return loaderSrv;
|
||||||
case 'unsavedChangesSrv':
|
case 'unsavedChangesSrv':
|
||||||
return unsavedChangesSrv;
|
return unsavedChangesSrv;
|
||||||
case 'dashboardSrv':
|
case 'dashboardSrv':
|
||||||
@ -71,12 +102,19 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
variableSrv,
|
variableSrv,
|
||||||
dashboardSrv,
|
dashboardSrv,
|
||||||
keybindingSrv,
|
keybindingSrv,
|
||||||
|
loaderSrv,
|
||||||
actions: [],
|
actions: [],
|
||||||
storeState: {
|
storeState: {
|
||||||
location: {
|
location: {
|
||||||
query: {},
|
query: {},
|
||||||
},
|
},
|
||||||
user: {},
|
user: {},
|
||||||
|
explore: {
|
||||||
|
left: {
|
||||||
|
originPanelId: undefined,
|
||||||
|
queries: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup: (fn: () => void) => {
|
setup: (fn: () => void) => {
|
||||||
setupFn = fn;
|
setupFn = fn;
|
||||||
@ -121,7 +159,7 @@ describeInitScenario('Initializing new dashboard', ctx => {
|
|||||||
expect(ctx.actions[3].payload.title).toBe('New dashboard');
|
expect(ctx.actions[3].payload.title).toBe('New dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should Initializing services', () => {
|
it('Should initialize services', () => {
|
||||||
expect(ctx.timeSrv.init).toBeCalled();
|
expect(ctx.timeSrv.init).toBeCalled();
|
||||||
expect(ctx.annotationsSrv.init).toBeCalled();
|
expect(ctx.annotationsSrv.init).toBeCalled();
|
||||||
expect(ctx.variableSrv.init).toBeCalled();
|
expect(ctx.variableSrv.init).toBeCalled();
|
||||||
@ -146,3 +184,68 @@ describeInitScenario('Initializing home dashboard', ctx => {
|
|||||||
expect(ctx.actions[1].payload.path).toBe('/u/123/my-home');
|
expect(ctx.actions[1].payload.path).toBe('/u/123/my-home');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describeInitScenario('Initializing existing dashboard', ctx => {
|
||||||
|
const mockQueries = [
|
||||||
|
{
|
||||||
|
context: 'explore',
|
||||||
|
key: 'jdasldsa98dsa9',
|
||||||
|
refId: 'A',
|
||||||
|
expr: 'new expr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context: 'explore',
|
||||||
|
key: 'fdsjkfds78fd',
|
||||||
|
refId: 'B',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedQueries = mockQueries.map(query => ({ refId: query.refId, expr: query.expr }));
|
||||||
|
|
||||||
|
ctx.setup(() => {
|
||||||
|
ctx.storeState.user.orgId = 12;
|
||||||
|
ctx.storeState.explore.left.originPanelId = 2;
|
||||||
|
ctx.storeState.explore.left.queries = mockQueries;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send action dashboardInitFetching', () => {
|
||||||
|
expect(ctx.actions[0].type).toBe(dashboardInitFetching.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send action dashboardInitServices ', () => {
|
||||||
|
expect(ctx.actions[1].type).toBe(dashboardInitServices.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should update location with orgId query param', () => {
|
||||||
|
expect(ctx.actions[2].type).toBe('UPDATE_LOCATION');
|
||||||
|
expect(ctx.actions[2].payload.query.orgId).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send resetExploreAction when coming from explore', () => {
|
||||||
|
expect(ctx.actions[3].type).toBe(resetExploreAction.type);
|
||||||
|
expect(ctx.actions[3].payload.force).toBe(true);
|
||||||
|
expect(ctx.dashboardSrv.setCurrent).lastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
panels: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
targets: expectedQueries,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send action dashboardInitCompleted', () => {
|
||||||
|
expect(ctx.actions[4].type).toBe(dashboardInitCompleted.type);
|
||||||
|
expect(ctx.actions[4].payload.title).toBe('My cool dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should initialize services', () => {
|
||||||
|
expect(ctx.timeSrv.init).toBeCalled();
|
||||||
|
expect(ctx.annotationsSrv.init).toBeCalled();
|
||||||
|
expect(ctx.variableSrv.init).toBeCalled();
|
||||||
|
expect(ctx.unsavedChangesSrv.init).toBeCalled();
|
||||||
|
expect(ctx.keybindingSrv.setupDashboardBindings).toBeCalled();
|
||||||
|
expect(ctx.dashboardSrv.setCurrent).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -21,8 +21,10 @@ import {
|
|||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult, DashboardDTO } from 'app/types';
|
import { DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult, DashboardDTO, ExploreItemState } from 'app/types';
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
|
import { resetExploreAction } from 'app/features/explore/state/actionTypes';
|
||||||
|
import { DataQuery } from '@grafana/ui';
|
||||||
|
|
||||||
export interface InitDashboardArgs {
|
export interface InitDashboardArgs {
|
||||||
$injector: any;
|
$injector: any;
|
||||||
@ -171,6 +173,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
|||||||
timeSrv.init(dashboard);
|
timeSrv.init(dashboard);
|
||||||
annotationsSrv.init(dashboard);
|
annotationsSrv.init(dashboard);
|
||||||
|
|
||||||
|
const left = storeState.explore && storeState.explore.left;
|
||||||
|
dashboard.meta.fromExplore = !!(left && left.originPanelId);
|
||||||
|
|
||||||
// template values service needs to initialize completely before
|
// template values service needs to initialize completely before
|
||||||
// the rest of the dashboard can load
|
// the rest of the dashboard can load
|
||||||
try {
|
try {
|
||||||
@ -198,8 +203,13 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dashboard.meta.fromExplore) {
|
||||||
|
updateQueriesWhenComingFromExplore(dispatch, dashboard, left);
|
||||||
|
}
|
||||||
|
|
||||||
// legacy srv state
|
// legacy srv state
|
||||||
dashboardSrv.setCurrent(dashboard);
|
dashboardSrv.setCurrent(dashboard);
|
||||||
|
|
||||||
// yay we are done
|
// yay we are done
|
||||||
dispatch(dashboardInitCompleted(dashboard));
|
dispatch(dashboardInitCompleted(dashboard));
|
||||||
};
|
};
|
||||||
@ -231,3 +241,28 @@ function getNewDashboardModelData(urlFolderId?: string): any {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateQueriesWhenComingFromExplore(
|
||||||
|
dispatch: ThunkDispatch,
|
||||||
|
dashboard: DashboardModel,
|
||||||
|
left: ExploreItemState
|
||||||
|
) {
|
||||||
|
// When returning to the origin panel from explore, if we're doing
|
||||||
|
// so with changes all the explore state is reset _except_ the queries
|
||||||
|
// and the origin panel ID.
|
||||||
|
const panelArrId = dashboard.panels.findIndex(panel => panel.id === left.originPanelId);
|
||||||
|
|
||||||
|
if (panelArrId > -1) {
|
||||||
|
dashboard.panels[panelArrId].targets = left.queries.map((query: DataQuery & { context?: string }) => {
|
||||||
|
delete query.context;
|
||||||
|
delete query.key;
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.startRefresh();
|
||||||
|
|
||||||
|
// Force-reset explore so that on subsequent dashboard loads we aren't
|
||||||
|
// taking the modified queries from explore again.
|
||||||
|
dispatch(resetExploreAction({ force: true }));
|
||||||
|
}
|
||||||
|
@ -90,6 +90,7 @@ interface ExploreProps {
|
|||||||
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
|
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
|
||||||
toggleGraph: typeof toggleGraph;
|
toggleGraph: typeof toggleGraph;
|
||||||
queryResponse: PanelData;
|
queryResponse: PanelData;
|
||||||
|
originPanelId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,7 +127,16 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, mode, initialUI } = this.props;
|
const {
|
||||||
|
initialized,
|
||||||
|
exploreId,
|
||||||
|
initialDatasource,
|
||||||
|
initialQueries,
|
||||||
|
initialRange,
|
||||||
|
mode,
|
||||||
|
initialUI,
|
||||||
|
originPanelId,
|
||||||
|
} = this.props;
|
||||||
const width = this.el ? this.el.offsetWidth : 0;
|
const width = this.el ? this.el.offsetWidth : 0;
|
||||||
|
|
||||||
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
||||||
@ -139,7 +149,8 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
mode,
|
mode,
|
||||||
width,
|
width,
|
||||||
this.exploreEvents,
|
this.exploreEvents,
|
||||||
initialUI
|
initialUI,
|
||||||
|
originPanelId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,7 +362,8 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
queryResponse,
|
queryResponse,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState;
|
const { datasource, queries, range: urlRange, mode: urlMode, ui, originPanelId } = (urlState ||
|
||||||
|
{}) as ExploreUrlState;
|
||||||
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
|
||||||
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
|
||||||
const initialRange = urlRange ? getTimeRangeFromUrlMemoized(urlRange, timeZone).raw : DEFAULT_RANGE;
|
const initialRange = urlRange ? getTimeRangeFromUrlMemoized(urlRange, timeZone).raw : DEFAULT_RANGE;
|
||||||
@ -397,6 +409,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
showingTable,
|
showingTable,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
|
originPanelId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
import omitBy from 'lodash/omitBy';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { ExploreId, ExploreMode } from 'app/types/explore';
|
import { ExploreId, ExploreMode } from 'app/types/explore';
|
||||||
import { DataSourceSelectItem, ToggleButtonGroup, ToggleButton } from '@grafana/ui';
|
import { DataSourceSelectItem, ToggleButtonGroup, ToggleButton, DataQuery, Tooltip, ButtonSelect } from '@grafana/ui';
|
||||||
import { RawTimeRange, TimeZone, TimeRange, SelectableValue } from '@grafana/data';
|
import { RawTimeRange, TimeZone, TimeRange, SelectableValue } from '@grafana/data';
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
@ -16,8 +18,12 @@ import {
|
|||||||
splitOpen,
|
splitOpen,
|
||||||
changeRefreshInterval,
|
changeRefreshInterval,
|
||||||
changeMode,
|
changeMode,
|
||||||
|
clearOrigin,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||||
|
import kbn from '../../core/utils/kbn';
|
||||||
import { ExploreTimeControls } from './ExploreTimeControls';
|
import { ExploreTimeControls } from './ExploreTimeControls';
|
||||||
|
|
||||||
enum IconSide {
|
enum IconSide {
|
||||||
@ -71,6 +77,8 @@ interface StateProps {
|
|||||||
selectedModeOption: SelectableValue<ExploreMode>;
|
selectedModeOption: SelectableValue<ExploreMode>;
|
||||||
hasLiveOption: boolean;
|
hasLiveOption: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
|
originPanelId: number;
|
||||||
|
queries: DataQuery[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
@ -81,6 +89,8 @@ interface DispatchProps {
|
|||||||
split: typeof splitOpen;
|
split: typeof splitOpen;
|
||||||
changeRefreshInterval: typeof changeRefreshInterval;
|
changeRefreshInterval: typeof changeRefreshInterval;
|
||||||
changeMode: typeof changeMode;
|
changeMode: typeof changeMode;
|
||||||
|
clearOrigin: typeof clearOrigin;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & OwnProps;
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
@ -112,6 +122,31 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
changeMode(exploreId, mode);
|
changeMode(exploreId, mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
returnToPanel = async ({ withChanges = false } = {}) => {
|
||||||
|
const { originPanelId } = this.props;
|
||||||
|
|
||||||
|
const dashboardSrv = getDashboardSrv();
|
||||||
|
const dash = dashboardSrv.getCurrent();
|
||||||
|
const titleSlug = kbn.slugifyForUrl(dash.title);
|
||||||
|
|
||||||
|
if (!withChanges) {
|
||||||
|
this.props.clearOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashViewOptions = {
|
||||||
|
fullscreen: withChanges || dash.meta.fullscreen,
|
||||||
|
edit: withChanges || dash.meta.isEditing,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.updateLocation({
|
||||||
|
path: `/d/${dash.uid}/:${titleSlug}`,
|
||||||
|
query: {
|
||||||
|
...omitBy(dashViewOptions, v => !v),
|
||||||
|
panelId: originPanelId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
@ -130,8 +165,15 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
selectedModeOption,
|
selectedModeOption,
|
||||||
hasLiveOption,
|
hasLiveOption,
|
||||||
isLive,
|
isLive,
|
||||||
|
originPanelId,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const originDashboardIsEditable = Number.isInteger(originPanelId);
|
||||||
|
const panelReturnClasses = classNames('btn', 'navbar-button', {
|
||||||
|
'btn--radius-right-0': originDashboardIsEditable,
|
||||||
|
'navbar-button navbar-button--border-right-0': originDashboardIsEditable,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
|
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
|
||||||
<div className="explore-toolbar-item">
|
<div className="explore-toolbar-item">
|
||||||
@ -187,6 +229,24 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{Number.isInteger(originPanelId) && !splitted && (
|
||||||
|
<div className="explore-toolbar-content-item">
|
||||||
|
<Tooltip content={'Return to panel'} placement="bottom">
|
||||||
|
<button className={panelReturnClasses} onClick={() => this.returnToPanel()}>
|
||||||
|
<i className="fa fa-arrow-left" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{originDashboardIsEditable && (
|
||||||
|
<ButtonSelect
|
||||||
|
className="navbar-button--attached btn--radius-left-0$"
|
||||||
|
options={[{ label: 'Return to panel with changes', value: '' }]}
|
||||||
|
onChange={() => this.returnToPanel({ withChanges: true })}
|
||||||
|
maxMenuHeight={380}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{exploreId === 'left' && !splitted ? (
|
{exploreId === 'left' && !splitted ? (
|
||||||
<div className="explore-toolbar-content-item">
|
<div className="explore-toolbar-content-item">
|
||||||
{createResponsiveButton({
|
{createResponsiveButton({
|
||||||
@ -285,6 +345,8 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
supportedModes,
|
supportedModes,
|
||||||
mode,
|
mode,
|
||||||
isLive,
|
isLive,
|
||||||
|
originPanelId,
|
||||||
|
queries,
|
||||||
} = exploreItem;
|
} = exploreItem;
|
||||||
const selectedDatasource = datasourceInstance
|
const selectedDatasource = datasourceInstance
|
||||||
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
||||||
@ -307,17 +369,21 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
selectedModeOption,
|
selectedModeOption,
|
||||||
hasLiveOption,
|
hasLiveOption,
|
||||||
isLive,
|
isLive,
|
||||||
|
originPanelId,
|
||||||
|
queries,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps: DispatchProps = {
|
const mapDispatchToProps: DispatchProps = {
|
||||||
changeDatasource,
|
changeDatasource,
|
||||||
|
updateLocation,
|
||||||
changeRefreshInterval,
|
changeRefreshInterval,
|
||||||
clearAll: clearQueries,
|
clearAll: clearQueries,
|
||||||
runQueries,
|
runQueries,
|
||||||
closeSplit: splitClose,
|
closeSplit: splitClose,
|
||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
changeMode: changeMode,
|
changeMode: changeMode,
|
||||||
|
clearOrigin,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExploreToolbar = hot(module)(
|
export const ExploreToolbar = hot(module)(
|
||||||
|
@ -16,7 +16,7 @@ interface WrapperProps {
|
|||||||
|
|
||||||
export class Wrapper extends Component<WrapperProps> {
|
export class Wrapper extends Component<WrapperProps> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.resetExploreAction();
|
this.props.resetExploreAction({});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -4,7 +4,7 @@ import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, PanelDa
|
|||||||
|
|
||||||
import { LogLevel, TimeRange, LoadingState, AbsoluteTimeRange } from '@grafana/data';
|
import { LogLevel, TimeRange, LoadingState, AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode } from 'app/types/explore';
|
||||||
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
|
|
||||||
/** Higher order actions
|
/** Higher order actions
|
||||||
*
|
*
|
||||||
@ -61,6 +61,14 @@ export interface ClearQueriesPayload {
|
|||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClearOriginPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClearRefreshIntervalPayload {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HighlightLogsExpressionPayload {
|
export interface HighlightLogsExpressionPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
expressions: string[];
|
expressions: string[];
|
||||||
@ -74,6 +82,7 @@ export interface InitializeExplorePayload {
|
|||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
mode: ExploreMode;
|
mode: ExploreMode;
|
||||||
ui: ExploreUIState;
|
ui: ExploreUIState;
|
||||||
|
originPanelId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadDatasourceMissingPayload {
|
export interface LoadDatasourceMissingPayload {
|
||||||
@ -202,6 +211,10 @@ export interface SetPausedStatePayload {
|
|||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResetExplorePayload {
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a query row after the row with the given index.
|
* Adds a query row after the row with the given index.
|
||||||
*/
|
*/
|
||||||
@ -236,6 +249,11 @@ export const changeRefreshIntervalAction = actionCreatorFactory<ChangeRefreshInt
|
|||||||
*/
|
*/
|
||||||
export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
|
export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear origin panel id.
|
||||||
|
*/
|
||||||
|
export const clearOriginAction = actionCreatorFactory<ClearOriginPayload>('explore/CLEAR_ORIGIN').create();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight expressions in the log results
|
* Highlight expressions in the log results
|
||||||
*/
|
*/
|
||||||
@ -351,7 +369,7 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
|
|||||||
/**
|
/**
|
||||||
* Resets state for explore.
|
* Resets state for explore.
|
||||||
*/
|
*/
|
||||||
export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
|
export const resetExploreAction = actionCreatorFactory<ResetExplorePayload>('explore/RESET_EXPLORE').create();
|
||||||
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
|
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
|
||||||
export const testDataSourcePendingAction = actionCreatorFactory<TestDatasourcePendingPayload>(
|
export const testDataSourcePendingAction = actionCreatorFactory<TestDatasourcePendingPayload>(
|
||||||
'explore/TEST_DATASOURCE_PENDING'
|
'explore/TEST_DATASOURCE_PENDING'
|
||||||
|
@ -69,6 +69,7 @@ import {
|
|||||||
historyUpdatedAction,
|
historyUpdatedAction,
|
||||||
queryEndedAction,
|
queryEndedAction,
|
||||||
queryStreamUpdatedAction,
|
queryStreamUpdatedAction,
|
||||||
|
clearOriginAction,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
@ -208,6 +209,12 @@ export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearOrigin(): ThunkResult<void> {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch(clearOriginAction({ exploreId: ExploreId.left }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all explore data sources and sets the chosen datasource.
|
* Loads all explore data sources and sets the chosen datasource.
|
||||||
* If there are no datasources a missing datasource action is dispatched.
|
* If there are no datasources a missing datasource action is dispatched.
|
||||||
@ -250,7 +257,8 @@ export function initializeExplore(
|
|||||||
mode: ExploreMode,
|
mode: ExploreMode,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
eventBridge: Emitter,
|
eventBridge: Emitter,
|
||||||
ui: ExploreUIState
|
ui: ExploreUIState,
|
||||||
|
originPanelId: number
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const timeZone = getTimeZone(getState().user);
|
const timeZone = getTimeZone(getState().user);
|
||||||
@ -265,6 +273,7 @@ export function initializeExplore(
|
|||||||
range,
|
range,
|
||||||
mode,
|
mode,
|
||||||
ui,
|
ui,
|
||||||
|
originPanelId,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(updateTime({ exploreId }));
|
dispatch(updateTime({ exploreId }));
|
||||||
@ -722,7 +731,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { urlState, update, containerWidth, eventBridge } = itemState;
|
const { urlState, update, containerWidth, eventBridge } = itemState;
|
||||||
const { datasource, queries, range: urlRange, mode, ui } = urlState;
|
const { datasource, queries, range: urlRange, mode, ui, originPanelId } = urlState;
|
||||||
const refreshQueries: DataQuery[] = [];
|
const refreshQueries: DataQuery[] = [];
|
||||||
for (let index = 0; index < queries.length; index++) {
|
for (let index = 0; index < queries.length; index++) {
|
||||||
const query = queries[index];
|
const query = queries[index];
|
||||||
@ -734,7 +743,19 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
// need to refresh datasource
|
// need to refresh datasource
|
||||||
if (update.datasource) {
|
if (update.datasource) {
|
||||||
const initialQueries = ensureQueries(queries);
|
const initialQueries = ensureQueries(queries);
|
||||||
dispatch(initializeExplore(exploreId, datasource, initialQueries, range, mode, containerWidth, eventBridge, ui));
|
dispatch(
|
||||||
|
initializeExplore(
|
||||||
|
exploreId,
|
||||||
|
datasource,
|
||||||
|
initialQueries,
|
||||||
|
range,
|
||||||
|
mode,
|
||||||
|
containerWidth,
|
||||||
|
eventBridge,
|
||||||
|
ui,
|
||||||
|
originPanelId
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,10 @@ import {
|
|||||||
scanStopAction,
|
scanStopAction,
|
||||||
queryStartAction,
|
queryStartAction,
|
||||||
changeRangeAction,
|
changeRangeAction,
|
||||||
|
clearOriginAction,
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
import {
|
||||||
addQueryRowAction,
|
addQueryRowAction,
|
||||||
changeQueryAction,
|
changeQueryAction,
|
||||||
changeSizeAction,
|
changeSizeAction,
|
||||||
@ -235,6 +239,15 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.addMapper({
|
||||||
|
filter: clearOriginAction,
|
||||||
|
mapper: (state): ExploreItemState => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
originPanelId: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: highlightLogsExpressionAction,
|
filter: highlightLogsExpressionAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
@ -245,7 +258,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
.addMapper({
|
.addMapper({
|
||||||
filter: initializeExploreAction,
|
filter: initializeExploreAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { containerWidth, eventBridge, queries, range, mode, ui } = action.payload;
|
const { containerWidth, eventBridge, queries, range, mode, ui, originPanelId } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
@ -256,6 +269,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
initialized: true,
|
initialized: true,
|
||||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||||
...ui,
|
...ui,
|
||||||
|
originPanelId,
|
||||||
update: makeInitialUpdateState(),
|
update: makeInitialUpdateState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -265,6 +279,9 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { datasourceInstance } = action.payload;
|
const { datasourceInstance } = action.payload;
|
||||||
const [supportedModes, mode] = getModesForDatasource(datasourceInstance, state.mode);
|
const [supportedModes, mode] = getModesForDatasource(datasourceInstance, state.mode);
|
||||||
|
|
||||||
|
const originPanelId = state.urlState && state.urlState.originPanelId;
|
||||||
|
|
||||||
// Custom components
|
// Custom components
|
||||||
const StartPage = datasourceInstance.components.ExploreStartPage;
|
const StartPage = datasourceInstance.components.ExploreStartPage;
|
||||||
stopQueryState(state.queryState, 'Datasource changed');
|
stopQueryState(state.queryState, 'Datasource changed');
|
||||||
@ -283,6 +300,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
queryKeys: [],
|
queryKeys: [],
|
||||||
supportedModes,
|
supportedModes,
|
||||||
mode,
|
mode,
|
||||||
|
originPanelId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -704,7 +722,18 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ActionTypes.ResetExplore: {
|
case ActionTypes.ResetExplore: {
|
||||||
return initialExploreState;
|
if (action.payload.force || !Number.isInteger(state.left.originPanelId)) {
|
||||||
|
return initialExploreState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialExploreState,
|
||||||
|
left: {
|
||||||
|
...initialExploreItemState,
|
||||||
|
queries: state.left.queries,
|
||||||
|
originPanelId: state.left.originPanelId,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case updateLocation.type: {
|
case updateLocation.type: {
|
||||||
|
@ -255,11 +255,18 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
text: 'Explore',
|
text: 'Explore',
|
||||||
icon: 'gicon gicon-explore',
|
icon: 'gicon gicon-explore',
|
||||||
shortcut: 'x',
|
shortcut: 'x',
|
||||||
href: await getExploreUrl(this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv),
|
href: await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async explore() {
|
||||||
|
const url = await getExploreUrl(this.panel, this.panel.targets, this.datasource, this.datasourceSrv, this.timeSrv);
|
||||||
|
if (url) {
|
||||||
|
this.$timeout(() => this.$location.url(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MetricsPanelCtrl };
|
export { MetricsPanelCtrl };
|
||||||
|
@ -25,10 +25,10 @@ import {
|
|||||||
DataQueryResponseData,
|
DataQueryResponseData,
|
||||||
DataStreamState,
|
DataStreamState,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { ExploreUrlState } from 'app/types/explore';
|
|
||||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
import { ExploreUrlState } from 'app/types';
|
||||||
|
|
||||||
export interface PromDataQueryResponse {
|
export interface PromDataQueryResponse {
|
||||||
data: {
|
data: {
|
||||||
@ -603,15 +603,16 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
|
|||||||
getExploreState(queries: PromQuery[]): Partial<ExploreUrlState> {
|
getExploreState(queries: PromQuery[]): Partial<ExploreUrlState> {
|
||||||
let state: Partial<ExploreUrlState> = { datasource: this.name };
|
let state: Partial<ExploreUrlState> = { datasource: this.name };
|
||||||
if (queries && queries.length > 0) {
|
if (queries && queries.length > 0) {
|
||||||
const expandedQueries = queries.map(query => ({
|
const expandedQueries = queries.map(query => {
|
||||||
...query,
|
const expandedQuery = {
|
||||||
expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr),
|
...query,
|
||||||
context: 'explore',
|
expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr),
|
||||||
|
context: 'explore',
|
||||||
|
};
|
||||||
|
|
||||||
|
return expandedQuery;
|
||||||
|
});
|
||||||
|
|
||||||
// null out values we don't support in Explore yet
|
|
||||||
legendFormat: null,
|
|
||||||
step: null,
|
|
||||||
}));
|
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
queries: expandedQueries,
|
queries: expandedQueries,
|
||||||
|
@ -22,6 +22,7 @@ export interface DashboardMeta {
|
|||||||
url?: string;
|
url?: string;
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
|
fromExplore?: boolean;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
canMakeEditable?: boolean;
|
canMakeEditable?: boolean;
|
||||||
submenuEnabled?: boolean;
|
submenuEnabled?: boolean;
|
||||||
|
@ -266,6 +266,7 @@ export interface ExploreItemState {
|
|||||||
queryState: PanelQueryState;
|
queryState: PanelQueryState;
|
||||||
|
|
||||||
queryResponse: PanelData;
|
queryResponse: PanelData;
|
||||||
|
originPanelId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUpdateState {
|
export interface ExploreUpdateState {
|
||||||
@ -289,6 +290,7 @@ export interface ExploreUrlState {
|
|||||||
mode: ExploreMode;
|
mode: ExploreMode;
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
ui: ExploreUIState;
|
ui: ExploreUIState;
|
||||||
|
originPanelId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||||
|
Loading…
Reference in New Issue
Block a user