Restoring explore panels state from URL

This commit is contained in:
Dominik Prokop 2019-02-01 12:33:15 +01:00
parent f9bab9585a
commit 6ab9355146
7 changed files with 109 additions and 44 deletions

View File

@ -100,7 +100,7 @@ describe('state functions', () => {
},
};
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 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);
});
});

View File

@ -20,7 +20,6 @@ import {
ResultType,
QueryIntervals,
QueryOptions,
ExploreUrlUIState,
} from 'app/types/explore';
export const DEFAULT_RANGE = {
@ -154,11 +153,13 @@ export function buildQueryTransaction(
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 {
if (initial) {
try {
const parsed = JSON.parse(decodeURI(initial));
// debugger
if (Array.isArray(parsed)) {
if (parsed.length <= 3) {
throw new Error('Error parsing compact URL state for Explore.');
@ -168,8 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
to: parsed[1],
};
const datasource = parsed[2];
const queries = parsed.slice(3);
return { datasource, queries, range, ui: DEFAULT_UI_STATE };
let queries = [],
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;
} catch (e) {
@ -179,14 +196,15 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
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 {
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);
}

View File

@ -32,7 +32,7 @@ import {
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
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 { ExploreToolbar } from './ExploreToolbar';
@ -61,7 +61,7 @@ interface ExploreProps {
supportsGraph: boolean | null;
supportsLogs: 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
if (!initialized) {
// 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 initialQueries: DataQuery[] = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
const width = this.el ? this.el.offsetWidth : 0;
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
initialRange,
width,
this.exploreEvents
this.exploreEvents,
ui
);
}
}

View File

@ -8,6 +8,7 @@ import {
RangeScanner,
ResultType,
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
export enum ActionTypes {
@ -106,6 +107,7 @@ export interface InitializeExploreAction {
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
ui: ExploreUIState;
};
}

View File

@ -38,6 +38,7 @@ import {
ResultType,
QueryOptions,
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
import {
@ -154,7 +155,8 @@ export function initializeExplore(
queries: DataQuery[],
range: RawTimeRange,
containerWidth: number,
eventBridge: Emitter
eventBridge: Emitter,
ui: ExploreUIState
): ThunkResult<void> {
return async dispatch => {
const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
@ -175,6 +177,7 @@ export function initializeExplore(
exploreDatasources,
queries,
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,
* e.g., Prometheus -> Loki queries.
*/
export const loadDatasourceSuccess = (
exploreId: ExploreId,
instance: any,
): LoadDatasourceSuccessAction => {
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => {
// Capabilities
const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs;
@ -766,6 +766,11 @@ export function stateSave() {
datasource: left.datasourceInstance.name,
queries: left.modifiedQueries.map(clearQueryKeys),
range: left.range,
ui: {
showingGraph: left.showingGraph,
showingLogs: left.showingLogs,
showingTable: left.showingTable,
},
};
urlStates.left = serializeStateToUrlParam(leftUrlState, true);
if (split) {
@ -773,48 +778,64 @@ export function stateSave() {
datasource: right.datasourceInstance.name,
queries: right.modifiedQueries.map(clearQueryKeys),
range: right.range,
ui: {
showingGraph: right.showingGraph,
showingLogs: right.showingLogs,
showingTable: right.showingTable,
},
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
}
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) => {
dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
if (getState().explore[exploreId].showingGraph) {
let shouldRunQueries;
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));
}
};
}
};
/**
* 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.
*/
export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
if (getState().explore[exploreId].showingLogs) {
dispatch(runQueries(exploreId));
}
};
}
export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs);
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
if (getState().explore[exploreId].showingTable) {
dispatch(runQueries(exploreId));
}
};
}
export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable);
/**
* Resets state for explore.

View File

@ -163,7 +163,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
}
case ActionTypes.InitializeExplore: {
const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload;
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
return {
...state,
containerWidth,
@ -173,6 +173,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
initialQueries: queries,
initialized: true,
modifiedQueries: queries.slice(),
...ui,
};
}

View File

@ -231,7 +231,7 @@ export interface ExploreItemState {
tableResult?: TableModel;
}
export interface ExploreUrlUIState {
export interface ExploreUIState {
showingTable: boolean;
showingGraph: boolean;
showingLogs: boolean;
@ -241,7 +241,7 @@ export interface ExploreUrlState {
datasource: string;
queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense
range: RawTimeRange;
ui: ExploreUrlUIState;
ui: ExploreUIState;
}
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {