Commit eedbc485 by Hugo Häggmark Committed by GitHub

Makes it possible to navigate back/forward with browser buttons in Explore (#16150)

Make it possible to navigate back/forward with browser buttons in Explore 
parent bfc54b64
import { LocationUpdate } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux';
export enum CoreActionTypes {
UpdateLocation = 'UPDATE_LOCATION',
}
export type Action = UpdateLocationAction;
export interface UpdateLocationAction {
type: CoreActionTypes.UpdateLocation;
payload: LocationUpdate;
}
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
type: CoreActionTypes.UpdateLocation,
payload: location,
});
export const updateLocation = actionCreatorFactory<LocationUpdate>('UPDATE_LOCATION').create();
import { Action, CoreActionTypes } from 'app/core/actions/location';
import { LocationState } from 'app/types';
import { renderUrl } from 'app/core/utils/url';
import _ from 'lodash';
import { reducerFactory } from 'app/core/redux';
import { updateLocation } from 'app/core/actions';
export const initialState: LocationState = {
url: '',
......@@ -12,9 +13,10 @@ export const initialState: LocationState = {
lastUpdated: 0,
};
export const locationReducer = (state = initialState, action: Action): LocationState => {
switch (action.type) {
case CoreActionTypes.UpdateLocation: {
export const locationReducer = reducerFactory<LocationState>(initialState)
.addMapper({
filter: updateLocation,
mapper: (state, action): LocationState => {
const { path, routeParams, replace } = action.payload;
let query = action.payload.query || state.query;
......@@ -31,8 +33,6 @@ export const locationReducer = (state = initialState, action: Action): LocationS
replace: replace === true,
lastUpdated: new Date().getTime(),
};
}
}
return state;
};
},
})
.create();
......@@ -68,5 +68,9 @@ export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator):
return mock;
};
export const mockActionCreator = (creator: ActionCreator<any>) => {
return Object.assign(jest.fn(), creator);
};
// Should only be used by tests
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
......@@ -3,6 +3,8 @@ import { shallow } from 'enzyme';
import { AlertRuleList, Props } from './AlertRuleList';
import { AlertRule, NavModel } from '../../types';
import appEvents from '../../core/app_events';
import { mockActionCreator } from 'app/core/redux';
import { updateLocation } from 'app/core/actions';
jest.mock('../../core/app_events', () => ({
emit: jest.fn(),
......@@ -12,7 +14,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
alertRules: [] as AlertRule[],
updateLocation: jest.fn(),
updateLocation: mockActionCreator(updateLocation),
getAlertRulesAsync: jest.fn(),
setSearchQuery: jest.fn(),
togglePauseAlertRule: jest.fn(),
......
......@@ -3,8 +3,9 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
import { DashboardModel } from '../state';
import { cleanUpDashboard } from '../state/actions';
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock, mockActionCreator } from 'app/core/redux';
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
import { updateLocation } from 'app/core/actions';
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
......@@ -62,7 +63,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
initPhase: DashboardInitPhase.NotStarted,
isInitSlow: false,
initDashboard: jest.fn(),
updateLocation: jest.fn(),
updateLocation: mockActionCreator(updateLocation),
notifyApp: jest.fn(),
cleanUpDashboard: ctx.cleanUpDashboardMock,
dashboard: null,
......
......@@ -4,10 +4,9 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
import { UpdateLocationAction } from 'app/core/actions/location';
import { buildNavModel } from './navModel';
import { DataSourceSettings } from '@grafana/ui/src/types';
import { Plugin, StoreState } from 'app/types';
import { Plugin, StoreState, LocationUpdate } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux';
import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
......@@ -32,12 +31,12 @@ export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_N
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
export type Action =
| UpdateLocationAction
| UpdateNavIndexAction
| ActionOf<DataSourceSettings>
| ActionOf<DataSourceSettings[]>
| ActionOf<Plugin>
| ActionOf<Plugin[]>;
| ActionOf<Plugin[]>
| ActionOf<LocationUpdate>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
......
// Libraries
import React, { ComponentClass } from 'react';
import { hot } from 'react-hot-loader';
// @ts-ignore
import { connect } from 'react-redux';
// @ts-ignore
import _ from 'lodash';
import { AutoSizer } from 'react-virtualized';
......@@ -18,11 +20,19 @@ import TableContainer from './TableContainer';
import TimePicker, { parseTime } from './TimePicker';
// Actions
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
import {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
scanStart,
setQueries,
refreshExplore,
} from './state/actions';
// Types
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId, ExploreUpdateState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter';
......@@ -42,6 +52,8 @@ interface ExploreProps {
initialized: boolean;
modifyQueries: typeof modifyQueries;
range: RawTimeRange;
update: ExploreUpdateState;
refreshExplore: typeof refreshExplore;
scanner?: RangeScanner;
scanning?: boolean;
scanRange?: RawTimeRange;
......@@ -53,8 +65,8 @@ interface ExploreProps {
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
urlState: ExploreUrlState;
queryKeys: string[];
urlState: ExploreUrlState;
}
/**
......@@ -89,23 +101,22 @@ export class Explore extends React.PureComponent<ExploreProps> {
*/
timepickerRef: React.RefObject<TimePicker>;
constructor(props) {
constructor(props: ExploreProps) {
super(props);
this.exploreEvents = new Emitter();
this.timepickerRef = React.createRef();
}
async componentDidMount() {
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, 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;
componentDidMount() {
const { exploreId, urlState, initialized } = this.props;
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;
// initialize the whole explore first time we mount and if browser history contains a change in datasource
if (!initialized) {
this.props.initializeExplore(
exploreId,
initialDatasource,
......@@ -122,7 +133,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.exploreEvents.removeAllListeners();
}
getRef = el => {
componentDidUpdate(prevProps: ExploreProps) {
this.refreshExplore();
}
getRef = (el: any) => {
this.el = el;
};
......@@ -142,7 +157,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
};
onModifyQueries = (action, index?: number) => {
onModifyQueries = (action: any, index?: number) => {
const { datasourceInstance } = this.props;
if (datasourceInstance && datasourceInstance.modifyQuery) {
const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification);
......@@ -169,6 +184,14 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.props.scanStopAction({ exploreId: this.props.exploreId });
};
refreshExplore = () => {
const { exploreId, update } = this.props;
if (update.queries || update.ui || update.range || update.datasource) {
this.props.refreshExplore(exploreId);
}
};
render() {
const {
StartPage,
......@@ -241,7 +264,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
}
}
function mapStateToProps(state: StoreState, { exploreId }) {
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
const explore = state.explore;
const { split } = explore;
const item: ExploreItemState = explore[exploreId];
......@@ -258,6 +281,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
supportsLogs,
supportsTable,
queryKeys,
urlState,
update,
} = item;
return {
StartPage,
......@@ -273,6 +298,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
supportsLogs,
supportsTable,
queryKeys,
urlState,
update,
};
}
......@@ -281,6 +308,7 @@ const mapDispatchToProps = {
changeTime,
initializeExplore,
modifyQueries,
refreshExplore,
scanStart,
scanStopAction,
setQueries,
......
......@@ -2,65 +2,37 @@ import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { updateLocation } from 'app/core/actions';
import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import { ExploreId } from 'app/types/explore';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
import { CustomScrollbar } from '@grafana/ui';
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
import { resetExploreAction } from './state/actionTypes';
interface WrapperProps {
initializeExploreSplitAction: typeof initializeExploreSplitAction;
split: boolean;
updateLocation: typeof updateLocation;
resetExploreAction: typeof resetExploreAction;
urlStates: { [key: string]: string };
}
export class Wrapper extends Component<WrapperProps> {
initialSplit: boolean;
urlStates: { [key: string]: ExploreUrlState };
constructor(props: WrapperProps) {
super(props);
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;
}
}
componentDidMount() {
if (this.initialSplit) {
this.props.initializeExploreSplitAction();
}
}
componentWillUnmount() {
this.props.resetExploreAction();
}
render() {
const { split } = this.props;
const { leftState, rightState } = this.urlStates;
return (
<div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
<div className="explore-wrapper">
<ErrorBoundary>
<Explore exploreId={ExploreId.left} urlState={leftState} />
<Explore exploreId={ExploreId.left} />
</ErrorBoundary>
{split && (
<ErrorBoundary>
<Explore exploreId={ExploreId.right} urlState={rightState} />
<Explore exploreId={ExploreId.right} />
</ErrorBoundary>
)}
</div>
......@@ -71,14 +43,11 @@ export class Wrapper extends Component<WrapperProps> {
}
const mapStateToProps = (state: StoreState) => {
const urlStates = state.location.query;
const { split } = state.explore;
return { split, urlStates };
return { split };
};
const mapDispatchToProps = {
initializeExploreSplitAction,
updateLocation,
resetExploreAction,
};
......
......@@ -24,17 +24,11 @@ import { LogLevel } from 'app/core/logs_model';
*
*/
export enum ActionTypes {
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN',
ResetExplore = 'explore/RESET_EXPLORE',
}
export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit;
payload: {};
}
export interface SplitCloseAction {
type: ActionTypes.SplitClose;
payload: {};
......@@ -154,10 +148,6 @@ export interface RemoveQueryRowPayload {
index: number;
}
export interface RunQueriesEmptyPayload {
exploreId: ExploreId;
}
export interface ScanStartPayload {
exploreId: ExploreId;
scanner: RangeScanner;
......@@ -260,11 +250,6 @@ export const initializeExploreAction = actionCreatorFactory<InitializeExplorePay
).create();
/**
* Initialize the wrapper split state
*/
export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create();
/**
* Display an error that happened during the selection of a datasource
*/
export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
......@@ -342,7 +327,6 @@ export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransacti
*/
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create();
export const runQueriesEmptyAction = actionCreatorFactory<RunQueriesEmptyPayload>('explore/RUN_QUERIES_EMPTY').create();
/**
* Start a scan for more results using the given scanner.
......@@ -411,12 +395,7 @@ export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>(
export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
export type HigherOrderAction =
| InitializeExploreSplitAction
| SplitCloseAction
| SplitOpenAction
| ResetExploreAction
| ActionOf<any>;
export type HigherOrderAction = SplitCloseAction | SplitOpenAction | ResetExploreAction | ActionOf<any>;
export type Action =
| ActionOf<AddQueryRowPayload>
......@@ -435,7 +414,6 @@ export type Action =
| ActionOf<QueryTransactionStartPayload>
| ActionOf<QueryTransactionSuccessPayload>
| ActionOf<RemoveQueryRowPayload>
| ActionOf<RunQueriesEmptyPayload>
| ActionOf<ScanStartPayload>
| ActionOf<ScanRangePayload>
| ActionOf<SetQueriesPayload>
......
import { refreshExplore } from './actions';
import { ExploreId, ExploreUrlState, ExploreUpdateState } from 'app/types';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { LogsDedupStrategy } from 'app/core/logs_model';
import {
initializeExploreAction,
InitializeExplorePayload,
changeTimeAction,
updateUIStateAction,
setQueriesAction,
} from './actionTypes';
import { Emitter } from 'app/core/core';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { makeInitialUpdateState } from './reducers';
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
getExternal: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue({
testDatasource: jest.fn(),
init: jest.fn(),
}),
}),
}));
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
const exploreId = ExploreId.left;
const containerWidth = 1920;
const eventBridge = {} as Emitter;
const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
const range = { from: 'now', to: 'now' };
const urlState: ExploreUrlState = { datasource: 'some-datasource', queries: [], range, ui };
const updateDefaults = makeInitialUpdateState();
const update = { ...updateDefaults, ...updateOverides };
const initialState = {
explore: {
[exploreId]: {
initialized: true,
urlState,
containerWidth,
eventBridge,
update,
datasourceInstance: { name: 'some-datasource' },
queries: [],
range,
ui,
},
},
};
return {
initialState,
exploreId,
range,
ui,
containerWidth,
eventBridge,
};
};
describe('refreshExplore', () => {
describe('when explore is initialized', () => {
describe('and update datasource is set', () => {
it('then it should dispatch initializeExplore', () => {
const { exploreId, ui, range, initialState, containerWidth, eventBridge } = setup({ datasource: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
const initializeExplore = dispatchedActions[0] as ActionOf<InitializeExplorePayload>;
const { type, payload } = initializeExplore;
expect(type).toEqual(initializeExploreAction.type);
expect(payload.containerWidth).toEqual(containerWidth);
expect(payload.eventBridge).toEqual(eventBridge);
expect(payload.exploreDatasources).toEqual([]);
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on
expect(payload.range).toEqual(range);
expect(payload.ui).toEqual(ui);
return true;
});
});
});
describe('and update range is set', () => {
it('then it should dispatch changeTimeAction', () => {
const { exploreId, range, initialState } = setup({ range: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(changeTimeAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, range });
return true;
});
});
});
describe('and update ui is set', () => {
it('then it should dispatch updateUIStateAction', () => {
const { exploreId, initialState, ui } = setup({ ui: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(updateUIStateAction.type);
expect(dispatchedActions[0].payload).toEqual({ ...ui, exploreId });
return true;
});
});
});
describe('and update queries is set', () => {
it('then it should dispatch setQueriesAction', () => {
const { exploreId, initialState } = setup({ queries: true });
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenDispatchedActionsAreEqual(dispatchedActions => {
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
return true;
});
});
});
});
describe('when update is not initialized', () => {
it('then it should not dispatch any actions', () => {
const exploreId = ExploreId.left;
const initialState = { explore: { [exploreId]: { initialized: false } } };
thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId)
.thenThereAreNoDispatchedActions();
});
});
});
......@@ -16,6 +16,7 @@ import {
updateHistory,
buildQueryTransaction,
serializeStateToUrlParam,
parseUrlState,
} from 'app/core/utils/explore';
// Actions
......@@ -54,7 +55,6 @@ import {
queryTransactionStartAction,
queryTransactionSuccessAction,
scanRangeAction,
runQueriesEmptyAction,
scanStartAction,
setQueriesAction,
splitCloseAction,
......@@ -67,9 +67,11 @@ import {
ToggleLogsPayload,
ToggleTablePayload,
updateUIStateAction,
runQueriesAction,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
import { LogsDedupStrategy } from 'app/core/logs_model';
import { parseTime } from '../TimePicker';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
......@@ -518,7 +520,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
} = getState().explore[exploreId];
if (!hasNonEmptyQuery(queries)) {
dispatch(runQueriesEmptyAction({ exploreId }));
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location
return;
}
......@@ -527,6 +529,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
// but we're using the datasource interval limit for now
const interval = datasourceInstance.interval;
dispatch(runQueriesAction());
// Keep table queries first since they need to return quickly
if ((ignoreUIState || showingTable) && supportsTable) {
dispatch(
......@@ -657,11 +660,15 @@ export function splitClose(): ThunkResult<void> {
export function splitOpen(): ThunkResult<void> {
return (dispatch, getState) => {
// Clone left state to become the right state
const leftState = getState().explore.left;
const leftState = getState().explore[ExploreId.left];
const queryState = getState().location.query[ExploreId.left] as string;
const urlState = parseUrlState(queryState);
const itemState = {
...leftState,
queryTransactions: [],
queries: leftState.queries.slice(),
exploreId: ExploreId.right,
urlState,
};
dispatch(splitOpenAction({ itemState }));
dispatch(stateSave());
......@@ -766,3 +773,44 @@ export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy)
dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
};
};
export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const itemState = getState().explore[exploreId];
if (!itemState.initialized) {
return;
}
const { urlState, update, containerWidth, eventBridge } = itemState;
const { datasource, queries, range, ui } = urlState;
const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
// need to refresh datasource
if (update.datasource) {
const initialQueries = ensureQueries(queries);
const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
dispatch(initializeExplore(exploreId, datasource, initialQueries, initialRange, containerWidth, eventBridge, ui));
return;
}
if (update.range) {
dispatch(changeTimeAction({ exploreId, range: refreshRange as TimeRange }));
}
// need to refresh ui state
if (update.ui) {
dispatch(updateUIStateAction({ ...ui, exploreId }));
}
// need to refresh queries
if (update.queries) {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
}
// always run queries when refresh is needed
if (update.queries || update.ui || update.range) {
dispatch(runQueries(exploreId));
}
};
}
// @ts-ignore
import _ from 'lodash';
import {
calculateResultsFromQueryTransactions,
generateEmptyQuery,
getIntervals,
ensureQueries,
getQueryKeys,
parseUrlState,
DEFAULT_UI_STATE,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
import { ExploreItemState, ExploreState, QueryTransaction, ExploreId, ExploreUpdateState } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types';
import { HigherOrderAction, ActionTypes } from './actionTypes';
......@@ -28,7 +32,6 @@ import {
queryTransactionStartAction,
queryTransactionSuccessAction,
removeQueryRowAction,
runQueriesEmptyAction,
scanRangeAction,
scanStartAction,
scanStopAction,
......@@ -40,6 +43,8 @@ import {
updateUIStateAction,
toggleLogLevelAction,
} from './actionTypes';
import { updateLocation } from 'app/core/actions/location';
import { LocationUpdate } from 'app/types';
export const DEFAULT_RANGE = {
from: 'now-6h',
......@@ -49,6 +54,12 @@ export const DEFAULT_RANGE = {
// Millies step for helper bar charts
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
export const makeInitialUpdateState = (): ExploreUpdateState => ({
datasource: false,
queries: false,
range: false,
ui: false,
});
/**
* Returns a fresh Explore area state
*/
......@@ -76,6 +87,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
supportsLogs: null,
supportsTable: null,
queryKeys: [],
urlState: null,
update: makeInitialUpdateState(),
});
/**
......@@ -195,6 +208,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
initialized: true,
queryKeys: getQueryKeys(queries, state.datasourceInstance),
...ui,
update: makeInitialUpdateState(),
};
},
})
......@@ -208,13 +222,23 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
.addMapper({
filter: loadDatasourceFailureAction,
mapper: (state, action): ExploreItemState => {
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
return {
...state,
datasourceError: action.payload.error,
datasourceLoading: false,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
filter: loadDatasourceMissingAction,
mapper: (state): ExploreItemState => {
return { ...state, datasourceMissing: true, datasourceLoading: false };
return {
...state,
datasourceMissing: true,
datasourceLoading: false,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
......@@ -253,6 +277,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
datasourceError: null,
logsHighlighterExpressions: undefined,
queryTransactions: [],
update: makeInitialUpdateState(),
};
},
})
......@@ -262,7 +287,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
const { queries, queryTransactions } = state;
const { modification, index, modifier } = action.payload;
let nextQueries: DataQuery[];
let nextQueryTransactions;
let nextQueryTransactions: QueryTransaction[];
if (index === undefined) {
// Modify all queries
nextQueries = queries.map((query, i) => ({
......@@ -303,7 +328,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
filter: queryTransactionFailureAction,
mapper: (state, action): ExploreItemState => {
const { queryTransactions } = action.payload;
return { ...state, queryTransactions, showingStartPage: false };
return {
...state,
queryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
......@@ -319,7 +349,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
return {
...state,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
......@@ -333,7 +368,14 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
queryIntervals.intervalMs
);
return { ...state, ...results, history, queryTransactions, showingStartPage: false };
return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
update: makeInitialUpdateState(),
};
},
})
.addMapper({
......@@ -368,12 +410,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
},
})
.addMapper({
filter: runQueriesEmptyAction,
mapper: (state): ExploreItemState => {
return { ...state, queryTransactions: [] };
},
})
.addMapper({
filter: scanRangeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, scanRange: action.payload.range };
......@@ -396,6 +432,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
scanning: false,
scanRange: undefined,
scanner: undefined,
update: makeInitialUpdateState(),
};
},
})
......@@ -482,6 +519,41 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
})
.create();
export const updateChildRefreshState = (
state: Readonly<ExploreItemState>,
payload: LocationUpdate,
exploreId: ExploreId
): ExploreItemState => {
const path = payload.path || '';
const queryState = payload.query[exploreId] as string;
if (!queryState) {
return state;
}
const urlState = parseUrlState(queryState);
if (!state.urlState || path !== '/explore') {
// we only want to refresh when browser back/forward
return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
}
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
const ui = _.isEqual(urlState ? urlState.ui : DEFAULT_UI_STATE, state.urlState.ui) === false;
return {
...state,
urlState,
update: {
...state.update,
datasource,
queries,
range,
ui,
},
};
};
/**
* Global Explore reducer that handles multiple Explore areas (left and right).
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
......@@ -493,16 +565,30 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
}
case ActionTypes.SplitOpen: {
return { ...state, split: true, right: action.payload.itemState };
}
case ActionTypes.InitializeExploreSplit: {
return { ...state, split: true };
return { ...state, split: true, right: { ...action.payload.itemState } };
}
case ActionTypes.ResetExplore: {
return initialExploreState;
}
case updateLocation.type: {
const { query } = action.payload;
if (!query || !query[ExploreId.left]) {
return state;
}
const split = query[ExploreId.right] ? true : false;
const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right];
return {
...state,
split,
[ExploreId.left]: updateChildRefreshState(leftState, action.payload, ExploreId.left),
[ExploreId.right]: updateChildRefreshState(rightState, action.payload, ExploreId.right),
};
}
}
if (action.payload) {
......
......@@ -248,6 +248,17 @@ export interface ExploreItemState {
* Currently hidden log series
*/
hiddenLogLevels?: LogLevel[];
urlState: ExploreUrlState;
update: ExploreUpdateState;
}
export interface ExploreUpdateState {
datasource: boolean;
queries: boolean;
range: boolean;
ui: boolean;
}
export interface ExploreUIState {
......
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
const mockStore = configureMockStore([thunk]);
export interface ThunkGiven {
givenThunk: (thunkFunction: any) => ThunkWhen;
}
export interface ThunkWhen {
whenThunkIsDispatched: (...args: any) => ThunkThen;
}
export interface ThunkThen {
thenDispatchedActionsEqual: (actions: Array<ActionOf<any>>) => ThunkWhen;
thenDispatchedActionsAreEqual: (callback: (actions: Array<ActionOf<any>>) => boolean) => ThunkWhen;
thenThereAreNoDispatchedActions: () => ThunkWhen;
}
export const thunkTester = (initialState: any): ThunkGiven => {
const store = mockStore(initialState);
let thunkUnderTest = null;
const givenThunk = (thunkFunction: any): ThunkWhen => {
thunkUnderTest = thunkFunction;
return instance;
};
function whenThunkIsDispatched(...args: any): ThunkThen {
store.dispatch(thunkUnderTest(...arguments));
return instance;
}
const thenDispatchedActionsEqual = (actions: Array<ActionOf<any>>): ThunkWhen => {
const resultingActions = store.getActions();
expect(resultingActions).toEqual(actions);
return instance;
};
const thenDispatchedActionsAreEqual = (callback: (dispathedActions: Array<ActionOf<any>>) => boolean): ThunkWhen => {
const resultingActions = store.getActions();
expect(callback(resultingActions)).toBe(true);
return instance;
};
const thenThereAreNoDispatchedActions = () => {
return thenDispatchedActionsEqual([]);
};
const instance = {
givenThunk,
whenThunkIsDispatched,
thenDispatchedActionsEqual,
thenDispatchedActionsAreEqual,
thenThereAreNoDispatchedActions,
};
return instance;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment