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( 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);
}); });
}); });

View File

@ -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);
} }

View File

@ -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
); );
} }
} }

View File

@ -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;
}; };
} }

View File

@ -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.

View File

@ -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,
}; };
} }

View File

@ -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> {