mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Restoring explore panels state from URL
This commit is contained in:
parent
f9bab9585a
commit
6ab9355146
@ -100,7 +100,7 @@ describe('state functions', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(serializeStateToUrlParam(state, true)).toBe(
|
expect(serializeStateToUrlParam(state, true)).toBe(
|
||||||
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
|
'["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -125,7 +125,28 @@ describe('state functions', () => {
|
|||||||
};
|
};
|
||||||
const serialized = serializeStateToUrlParam(state);
|
const serialized = serializeStateToUrlParam(state);
|
||||||
const parsed = parseUrlState(serialized);
|
const parsed = parseUrlState(serialized);
|
||||||
|
expect(state).toMatchObject(parsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can parse the compact serialized state into the original state', () => {
|
||||||
|
const state = {
|
||||||
|
...DEFAULT_EXPLORE_STATE,
|
||||||
|
datasource: 'foo',
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
expr: 'metric{test="a/b"}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expr: 'super{foo="x/z"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
range: {
|
||||||
|
from: 'now - 5h',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const serialized = serializeStateToUrlParam(state, true);
|
||||||
|
const parsed = parseUrlState(serialized);
|
||||||
expect(state).toMatchObject(parsed);
|
expect(state).toMatchObject(parsed);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
ResultType,
|
ResultType,
|
||||||
QueryIntervals,
|
QueryIntervals,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
ExploreUrlUIState,
|
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
@ -154,11 +153,13 @@ export function buildQueryTransaction(
|
|||||||
|
|
||||||
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
|
||||||
|
|
||||||
|
const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr');
|
||||||
|
const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
|
||||||
|
|
||||||
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(decodeURI(initial));
|
const parsed = JSON.parse(decodeURI(initial));
|
||||||
// debugger
|
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
if (parsed.length <= 3) {
|
if (parsed.length <= 3) {
|
||||||
throw new Error('Error parsing compact URL state for Explore.');
|
throw new Error('Error parsing compact URL state for Explore.');
|
||||||
@ -168,8 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
to: parsed[1],
|
to: parsed[1],
|
||||||
};
|
};
|
||||||
const datasource = parsed[2];
|
const datasource = parsed[2];
|
||||||
const queries = parsed.slice(3);
|
let queries = [],
|
||||||
return { datasource, queries, range, ui: DEFAULT_UI_STATE };
|
ui;
|
||||||
|
|
||||||
|
parsed.slice(3).forEach(segment => {
|
||||||
|
if (isMetricSegment(segment)) {
|
||||||
|
queries = [...queries, segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUISegment(segment)) {
|
||||||
|
ui = {
|
||||||
|
showingGraph: segment.ui[0],
|
||||||
|
showingLogs: segment.ui[1],
|
||||||
|
showingTable: segment.ui[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { datasource, queries, range, ui };
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -179,14 +196,15 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
|
|||||||
return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE };
|
return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE };
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializeUIState = (state: ExploreUrlUIState) => {
|
|
||||||
return Object.keys(state).map((key) => ({ [key]: state[key] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries, ...serializeUIState(urlState.ui)]);
|
return JSON.stringify([
|
||||||
|
urlState.range.from,
|
||||||
|
urlState.range.to,
|
||||||
|
urlState.datasource,
|
||||||
|
...urlState.queries,
|
||||||
|
{ ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return JSON.stringify(urlState);
|
return JSON.stringify(urlState);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
|
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
|
||||||
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
|
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
|
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ interface ExploreProps {
|
|||||||
supportsGraph: boolean | null;
|
supportsGraph: boolean | null;
|
||||||
supportsLogs: boolean | null;
|
supportsLogs: boolean | null;
|
||||||
supportsTable: boolean | null;
|
supportsTable: boolean | null;
|
||||||
urlState: ExploreUrlState;
|
urlState?: ExploreUrlState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,18 +107,20 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
// Don't initialize on split, but need to initialize urlparameters when present
|
// Don't initialize on split, but need to initialize urlparameters when present
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
// Load URL state and parse range
|
// Load URL state and parse range
|
||||||
const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState;
|
const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState;
|
||||||
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY);
|
||||||
const initialQueries: DataQuery[] = ensureQueries(queries);
|
const initialQueries: DataQuery[] = ensureQueries(queries);
|
||||||
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
|
||||||
const width = this.el ? this.el.offsetWidth : 0;
|
const width = this.el ? this.el.offsetWidth : 0;
|
||||||
|
|
||||||
this.props.initializeExplore(
|
this.props.initializeExplore(
|
||||||
exploreId,
|
exploreId,
|
||||||
initialDatasource,
|
initialDatasource,
|
||||||
initialQueries,
|
initialQueries,
|
||||||
initialRange,
|
initialRange,
|
||||||
width,
|
width,
|
||||||
this.exploreEvents
|
this.exploreEvents,
|
||||||
|
ui
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
RangeScanner,
|
RangeScanner,
|
||||||
ResultType,
|
ResultType,
|
||||||
QueryTransaction,
|
QueryTransaction,
|
||||||
|
ExploreUIState,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
@ -106,6 +107,7 @@ export interface InitializeExploreAction {
|
|||||||
exploreDatasources: DataSourceSelectItem[];
|
exploreDatasources: DataSourceSelectItem[];
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
|
ui: ExploreUIState;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
ResultType,
|
ResultType,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
QueryTransaction,
|
QueryTransaction,
|
||||||
|
ExploreUIState,
|
||||||
} from 'app/types/explore';
|
} from 'app/types/explore';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -154,7 +155,8 @@ export function initializeExplore(
|
|||||||
queries: DataQuery[],
|
queries: DataQuery[],
|
||||||
range: RawTimeRange,
|
range: RawTimeRange,
|
||||||
containerWidth: number,
|
containerWidth: number,
|
||||||
eventBridge: Emitter
|
eventBridge: Emitter,
|
||||||
|
ui: ExploreUIState
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
|
||||||
@ -175,6 +177,7 @@ export function initializeExplore(
|
|||||||
exploreDatasources,
|
exploreDatasources,
|
||||||
queries,
|
queries,
|
||||||
range,
|
range,
|
||||||
|
ui,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,10 +261,7 @@ export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): Que
|
|||||||
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
|
||||||
* e.g., Prometheus -> Loki queries.
|
* e.g., Prometheus -> Loki queries.
|
||||||
*/
|
*/
|
||||||
export const loadDatasourceSuccess = (
|
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => {
|
||||||
exploreId: ExploreId,
|
|
||||||
instance: any,
|
|
||||||
): LoadDatasourceSuccessAction => {
|
|
||||||
// Capabilities
|
// Capabilities
|
||||||
const supportsGraph = instance.meta.metrics;
|
const supportsGraph = instance.meta.metrics;
|
||||||
const supportsLogs = instance.meta.logs;
|
const supportsLogs = instance.meta.logs;
|
||||||
@ -766,6 +766,11 @@ export function stateSave() {
|
|||||||
datasource: left.datasourceInstance.name,
|
datasource: left.datasourceInstance.name,
|
||||||
queries: left.modifiedQueries.map(clearQueryKeys),
|
queries: left.modifiedQueries.map(clearQueryKeys),
|
||||||
range: left.range,
|
range: left.range,
|
||||||
|
ui: {
|
||||||
|
showingGraph: left.showingGraph,
|
||||||
|
showingLogs: left.showingLogs,
|
||||||
|
showingTable: left.showingTable,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
|
||||||
if (split) {
|
if (split) {
|
||||||
@ -773,48 +778,64 @@ export function stateSave() {
|
|||||||
datasource: right.datasourceInstance.name,
|
datasource: right.datasourceInstance.name,
|
||||||
queries: right.modifiedQueries.map(clearQueryKeys),
|
queries: right.modifiedQueries.map(clearQueryKeys),
|
||||||
range: right.range,
|
range: right.range,
|
||||||
|
ui: {
|
||||||
|
showingGraph: right.showingGraph,
|
||||||
|
showingLogs: right.showingLogs,
|
||||||
|
showingTable: right.showingTable,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateLocation({ query: urlStates }));
|
dispatch(updateLocation({ query: urlStates }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
|
||||||
|
* queries won't be run
|
||||||
*/
|
*/
|
||||||
export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
|
const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.ToggleTable | ActionTypes.ToggleLogs) => (
|
||||||
|
exploreId: ExploreId
|
||||||
|
) => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
|
let shouldRunQueries;
|
||||||
if (getState().explore[exploreId].showingGraph) {
|
dispatch({ type, payload: { exploreId } });
|
||||||
|
dispatch(stateSave());
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ActionTypes.ToggleGraph:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingGraph;
|
||||||
|
break;
|
||||||
|
case ActionTypes.ToggleLogs:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingLogs;
|
||||||
|
break;
|
||||||
|
case ActionTypes.ToggleTable:
|
||||||
|
shouldRunQueries = getState().explore[exploreId].showingTable;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRunQueries) {
|
||||||
dispatch(runQueries(exploreId));
|
dispatch(runQueries(exploreId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||||
|
*/
|
||||||
|
export const toggleGraph = togglePanelActionCreator(ActionTypes.ToggleGraph);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
|
||||||
*/
|
*/
|
||||||
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
|
export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs);
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
|
|
||||||
if (getState().explore[exploreId].showingLogs) {
|
|
||||||
dispatch(runQueries(exploreId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
||||||
*/
|
*/
|
||||||
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
|
export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable);
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
|
|
||||||
if (getState().explore[exploreId].showingTable) {
|
|
||||||
dispatch(runQueries(exploreId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets state for explore.
|
* Resets state for explore.
|
||||||
|
@ -163,7 +163,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ActionTypes.InitializeExplore: {
|
case ActionTypes.InitializeExplore: {
|
||||||
const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
|
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
@ -173,6 +173,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
|
|||||||
initialQueries: queries,
|
initialQueries: queries,
|
||||||
initialized: true,
|
initialized: true,
|
||||||
modifiedQueries: queries.slice(),
|
modifiedQueries: queries.slice(),
|
||||||
|
...ui,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ export interface ExploreItemState {
|
|||||||
tableResult?: TableModel;
|
tableResult?: TableModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUrlUIState {
|
export interface ExploreUIState {
|
||||||
showingTable: boolean;
|
showingTable: boolean;
|
||||||
showingGraph: boolean;
|
showingGraph: boolean;
|
||||||
showingLogs: boolean;
|
showingLogs: boolean;
|
||||||
@ -241,7 +241,7 @@ export interface ExploreUrlState {
|
|||||||
datasource: string;
|
datasource: string;
|
||||||
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
ui: ExploreUrlUIState;
|
ui: ExploreUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||||
|
Loading…
Reference in New Issue
Block a user