From be172d3e4a4c22e6f389a85aaa0fec02fe9ff75a Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 12 Jan 2019 23:22:28 +0100 Subject: [PATCH] Save state in URL and fix tests --- public/app/core/utils/explore.test.ts | 90 +++++++------------ public/app/core/utils/explore.ts | 7 +- public/app/features/explore/Explore.tsx | 17 ++-- public/app/features/explore/Wrapper.tsx | 48 +++++----- public/app/features/explore/state/actions.ts | 60 +++++++++++-- public/app/features/explore/state/reducers.ts | 14 ++- 6 files changed, 131 insertions(+), 105 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index a3b08516d16..32135eab90a 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -6,26 +6,13 @@ import { clearHistory, hasNonEmptyQuery, } from './explore'; -import { ExploreState } from 'app/types/explore'; +import { ExploreUrlState } from 'app/types/explore'; import store from 'app/core/store'; -const DEFAULT_EXPLORE_STATE: ExploreState = { +const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, - datasourceError: null, - datasourceLoading: null, - datasourceMissing: false, - exploreDatasources: [], - graphInterval: 1000, - history: [], - initialQueries: [], - queryTransactions: [], + queries: [], range: DEFAULT_RANGE, - showingGraph: true, - showingLogs: true, - showingTable: true, - supportsGraph: null, - supportsLogs: null, - supportsTable: null, }; describe('state functions', () => { @@ -68,21 +55,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + @@ -93,21 +78,19 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now-5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; expect(serializeStateToUrlParam(state, true)).toBe( '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' @@ -119,35 +102,24 @@ describe('state functions', () => { it('can parse the serialized state into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, - initialDatasource: 'foo', + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], range: { from: 'now - 5h', to: 'now', }, - initialQueries: [ - { - refId: '1', - expr: 'metric{test="a/b"}', - }, - { - refId: '2', - expr: 'super{foo="x/z"}', - }, - ], }; const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); - // Account for datasource vs datasourceName - const { datasource, queries, ...rest } = parsed; - const resultState = { - ...rest, - datasource: DEFAULT_EXPLORE_STATE.datasource, - initialDatasource: datasource, - initialQueries: queries, - }; - - expect(state).toMatchObject(resultState); + expect(state).toMatchObject(parsed); }); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 026d1ba324c..b0dcf2117d0 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -142,7 +142,7 @@ export function buildQueryTransaction( }; } -const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; +export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { @@ -169,11 +169,6 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - // const urlState: ExploreUrlState = { - // datasource: state.initialDatasource, - // queries: state.initialQueries.map(clearQueryKeys), - // range: state.range, - // }; if (compact) { return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index a3177bebe79..b0a17884fc3 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -13,6 +13,8 @@ import store from 'app/core/store'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; +import { LogsModel } from 'app/core/logs_model'; +import TableModel from 'app/core/table_model'; import { addQueryRow, @@ -45,8 +47,6 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import { Alert } from './Error'; import TimePicker, { parseTime } from './TimePicker'; -import { LogsModel } from 'app/core/logs_model'; -import TableModel from 'app/core/table_model'; interface ExploreProps { StartPage?: any; @@ -74,6 +74,7 @@ interface ExploreProps { initialDatasource?: string; initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; @@ -149,8 +150,9 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - const { exploreId, split, urlState } = this.props; - if (!split) { + const { exploreId, initialized, urlState } = this.props; + // 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 initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); @@ -277,11 +279,6 @@ export class Explore extends React.PureComponent { } }, 500); - // saveState = () => { - // const { stateKey, onSaveState } = this.props; - // onSaveState(stateKey, this.cloneState()); - // }; - render() { const { StartPage, @@ -478,6 +475,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, @@ -504,6 +502,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { graphResult, initialDatasource, initialQueries, + initialized, history, logsHighlighterExpressions, logsResult, diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 04f189749bf..7ea8f228af8 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,51 +3,56 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreId } from 'app/types/explore'; +import { ExploreId, ExploreUrlState } from 'app/types/explore'; +import { parseUrlState } from 'app/core/utils/explore'; +import { initializeExploreSplit } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; interface WrapperProps { - backendSrv?: any; - datasourceSrv?: any; + initializeExploreSplit: typeof initializeExploreSplit; split: boolean; updateLocation: typeof updateLocation; - // urlStates: { [key: string]: string }; + urlStates: { [key: string]: string }; } export class Wrapper extends Component { - // urlStates: { [key: string]: string }; + initialSplit: boolean; + urlStates: { [key: string]: ExploreUrlState }; constructor(props: WrapperProps) { super(props); - // this.urlStates = props.urlStates; + this.urlStates = {}; + const { left, right } = props.urlStates; + if (props.urlStates.left) { + this.urlStates.leftState = parseUrlState(left); + } + if (props.urlStates.right) { + this.urlStates.rightState = parseUrlState(right); + this.initialSplit = true; + } } - // onSaveState = (key: string, state: ExploreState) => { - // const urlState = serializeStateToUrlParam(state, true); - // this.urlStates[key] = urlState; - // this.props.updateLocation({ - // query: this.urlStates, - // }); - // }; + componentDidMount() { + if (this.initialSplit) { + this.props.initializeExploreSplit(); + } + } render() { const { split } = this.props; - // State overrides for props from first Explore - // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + const { leftState, rightState } = this.urlStates; return (
- + {split && ( - + )}
@@ -56,12 +61,13 @@ export class Wrapper extends Component { } const mapStateToProps = (state: StoreState) => { - // urlStates: state.location.query, + const urlStates = state.location.query; const { split } = state.explore; - return { split }; + return { split, urlStates }; }; const mapDispatchToProps = { + initializeExploreSplit, updateLocation, }; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 145b7506e79..979c80395c3 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -4,14 +4,17 @@ import { RawTimeRange, TimeRange } from '@grafana/ui'; import { LAST_USED_DATASOURCE_KEY, + clearQueryKeys, ensureQueries, generateEmptyQuery, hasNonEmptyQuery, makeTimeSeriesList, updateHistory, buildQueryTransaction, + serializeStateToUrlParam, } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; import store from 'app/core/store'; import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; @@ -25,6 +28,7 @@ import { QueryTransaction, QueryHint, QueryHintGetter, + ExploreUrlState, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; import { ExploreItemState } from './reducers'; @@ -44,6 +48,7 @@ export enum ActionTypes { ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', + InitializeExploreSplit = 'INITIALIZE_EXPLORE_SPLIT', LoadDatasourceFailure = 'LOAD_DATASOURCE_FAILURE', LoadDatasourceMissing = 'LOAD_DATASOURCE_MISSING', LoadDatasourcePending = 'LOAD_DATASOURCE_PENDING', @@ -58,6 +63,7 @@ export enum ActionTypes { ScanRange = 'SCAN_RANGE', ScanStart = 'SCAN_START', ScanStop = 'SCAN_STOP', + StateSave = 'STATE_SAVE', } export interface AddQueryRowAction { @@ -123,6 +129,12 @@ export interface ClickTableButtonAction { exploreId: ExploreId; } +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; + expressions: string[]; +} + export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; exploreId: ExploreId; @@ -134,10 +146,8 @@ export interface InitializeExploreAction { range: RawTimeRange; } -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - exploreId: ExploreId; - expressions: string[]; +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; } export interface LoadDatasourceFailureAction { @@ -224,6 +234,10 @@ export interface ScanStopAction { exploreId: ExploreId; } +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + export type Action = | AddQueryRowAction | ChangeQueryAction @@ -238,6 +252,7 @@ export type Action = | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction + | InitializeExploreSplitAction | LoadDatasourceFailureAction | LoadDatasourceMissingAction | LoadDatasourcePendingAction @@ -301,15 +316,14 @@ export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { dispatch(scanStop(exploreId)); dispatch({ type: ActionTypes.ClickClear, exploreId }); - // TODO save state + dispatch(stateSave()); }; } export function clickCloseSplit(): ThunkResult { return dispatch => { dispatch({ type: ActionTypes.ClickCloseSplit }); - // When closing split, remove URL state for split part - // TODO save state + dispatch(stateSave()); }; } @@ -353,7 +367,7 @@ export function clickSplit(): ThunkResult { initialQueries: leftState.modifiedQueries.slice(), }; dispatch({ type: ActionTypes.ClickSplit, itemState }); - // TODO save state + dispatch(stateSave()); }; } @@ -412,6 +426,12 @@ export function initializeExplore( }; } +export function initializeExploreSplit() { + return async dispatch => { + dispatch({ type: ActionTypes.InitializeExploreSplit }); + }; +} + export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, exploreId, @@ -733,7 +753,7 @@ export function runQueries(exploreId: ExploreId) { if (showingLogs && supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } - // TODO save state + dispatch(stateSave()); }; } @@ -792,3 +812,25 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes export function scanStop(exploreId: ExploreId): ScanStopAction { return { type: ActionTypes.ScanStop, exploreId }; } + +export function stateSave() { + return (dispatch, getState) => { + const { left, right, split } = getState().explore; + const urlStates: { [index: string]: string } = {}; + const leftUrlState: ExploreUrlState = { + datasource: left.datasourceInstance.name, + queries: left.modifiedQueries.map(clearQueryKeys), + range: left.range, + }; + urlStates.left = serializeStateToUrlParam(leftUrlState, true); + if (split) { + const rightUrlState: ExploreUrlState = { + datasource: right.datasourceInstance.name, + queries: right.modifiedQueries.map(clearQueryKeys), + range: right.range, + }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); + } + dispatch(updateLocation({ query: urlStates })); + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index cea4d766c58..dda3b37fdae 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -36,6 +36,7 @@ export interface ExploreItemState { history: HistoryItem[]; initialDatasource?: string; initialQueries: DataQuery[]; + initialized: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifiedQueries: DataQuery[]; @@ -74,6 +75,7 @@ const makeExploreItemState = (): ExploreItemState => ({ exploreDatasources: [], history: [], initialQueries: [], + initialized: false, modifiedQueries: [], queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, @@ -89,7 +91,7 @@ const makeExploreItemState = (): ExploreItemState => ({ }); const initialExploreState: ExploreState = { - split: false, + split: null, left: makeExploreItemState(), right: makeExploreItemState(), }; @@ -236,6 +238,7 @@ const itemReducer = (state, action: Action): ExploreItemState => { range, initialDatasource: action.datasource, initialQueries: action.queries, + initialized: true, modifiedQueries: action.queries.slice(), }; } @@ -436,6 +439,13 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp right: action.itemState, }; } + + case ActionTypes.InitializeExploreSplit: { + return { + ...state, + split: true, + }; + } } const { exploreId } = action as any; @@ -447,6 +457,8 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp }; } + console.error('Unhandled action', action.type); + return state; };