mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -1,17 +1,4 @@
|
|||||||
import { LocationUpdate } from 'app/types';
|
import { LocationUpdate } from 'app/types';
|
||||||
|
import { actionCreatorFactory } from 'app/core/redux';
|
||||||
|
|
||||||
export enum CoreActionTypes {
|
export const updateLocation = actionCreatorFactory<LocationUpdate>('UPDATE_LOCATION').create();
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Action, CoreActionTypes } from 'app/core/actions/location';
|
|
||||||
import { LocationState } from 'app/types';
|
import { LocationState } from 'app/types';
|
||||||
import { renderUrl } from 'app/core/utils/url';
|
import { renderUrl } from 'app/core/utils/url';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { reducerFactory } from 'app/core/redux';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
export const initialState: LocationState = {
|
export const initialState: LocationState = {
|
||||||
url: '',
|
url: '',
|
||||||
@@ -12,9 +13,10 @@ export const initialState: LocationState = {
|
|||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
export const locationReducer = reducerFactory<LocationState>(initialState)
|
||||||
switch (action.type) {
|
.addMapper({
|
||||||
case CoreActionTypes.UpdateLocation: {
|
filter: updateLocation,
|
||||||
|
mapper: (state, action): LocationState => {
|
||||||
const { path, routeParams, replace } = action.payload;
|
const { path, routeParams, replace } = action.payload;
|
||||||
let query = action.payload.query || state.query;
|
let query = action.payload.query || state.query;
|
||||||
|
|
||||||
@@ -31,8 +33,6 @@ export const locationReducer = (state = initialState, action: Action): LocationS
|
|||||||
replace: replace === true,
|
replace: replace === true,
|
||||||
lastUpdated: new Date().getTime(),
|
lastUpdated: new Date().getTime(),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
}
|
})
|
||||||
|
.create();
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -68,5 +68,9 @@ export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator):
|
|||||||
return mock;
|
return mock;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockActionCreator = (creator: ActionCreator<any>) => {
|
||||||
|
return Object.assign(jest.fn(), creator);
|
||||||
|
};
|
||||||
|
|
||||||
// Should only be used by tests
|
// Should only be used by tests
|
||||||
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
|
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { shallow } from 'enzyme';
|
|||||||
import { AlertRuleList, Props } from './AlertRuleList';
|
import { AlertRuleList, Props } from './AlertRuleList';
|
||||||
import { AlertRule, NavModel } from '../../types';
|
import { AlertRule, NavModel } from '../../types';
|
||||||
import appEvents from '../../core/app_events';
|
import appEvents from '../../core/app_events';
|
||||||
|
import { mockActionCreator } from 'app/core/redux';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
jest.mock('../../core/app_events', () => ({
|
jest.mock('../../core/app_events', () => ({
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
@@ -12,7 +14,7 @@ const setup = (propOverrides?: object) => {
|
|||||||
const props: Props = {
|
const props: Props = {
|
||||||
navModel: {} as NavModel,
|
navModel: {} as NavModel,
|
||||||
alertRules: [] as AlertRule[],
|
alertRules: [] as AlertRule[],
|
||||||
updateLocation: jest.fn(),
|
updateLocation: mockActionCreator(updateLocation),
|
||||||
getAlertRulesAsync: jest.fn(),
|
getAlertRulesAsync: jest.fn(),
|
||||||
setSearchQuery: jest.fn(),
|
setSearchQuery: jest.fn(),
|
||||||
togglePauseAlertRule: jest.fn(),
|
togglePauseAlertRule: jest.fn(),
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { shallow, ShallowWrapper } from 'enzyme';
|
|||||||
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
|
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
|
||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
import { cleanUpDashboard } from '../state/actions';
|
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 { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
|
|||||||
initPhase: DashboardInitPhase.NotStarted,
|
initPhase: DashboardInitPhase.NotStarted,
|
||||||
isInitSlow: false,
|
isInitSlow: false,
|
||||||
initDashboard: jest.fn(),
|
initDashboard: jest.fn(),
|
||||||
updateLocation: jest.fn(),
|
updateLocation: mockActionCreator(updateLocation),
|
||||||
notifyApp: jest.fn(),
|
notifyApp: jest.fn(),
|
||||||
cleanUpDashboard: ctx.cleanUpDashboardMock,
|
cleanUpDashboard: ctx.cleanUpDashboardMock,
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
|
|||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
||||||
import { UpdateLocationAction } from 'app/core/actions/location';
|
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { DataSourceSettings } from '@grafana/ui/src/types';
|
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 { actionCreatorFactory } from 'app/core/redux';
|
||||||
import { ActionOf, noPayloadActionCreatorFactory } from 'app/core/redux/actionCreatorFactory';
|
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 const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| UpdateLocationAction
|
|
||||||
| UpdateNavIndexAction
|
| UpdateNavIndexAction
|
||||||
| ActionOf<DataSourceSettings>
|
| ActionOf<DataSourceSettings>
|
||||||
| ActionOf<DataSourceSettings[]>
|
| ActionOf<DataSourceSettings[]>
|
||||||
| ActionOf<Plugin>
|
| ActionOf<Plugin>
|
||||||
| ActionOf<Plugin[]>;
|
| ActionOf<Plugin[]>
|
||||||
|
| ActionOf<LocationUpdate>;
|
||||||
|
|
||||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { ComponentClass } from 'react';
|
import React, { ComponentClass } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
|
// @ts-ignore
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
// @ts-ignore
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AutoSizer } from 'react-virtualized';
|
import { AutoSizer } from 'react-virtualized';
|
||||||
|
|
||||||
@@ -18,11 +20,19 @@ import TableContainer from './TableContainer';
|
|||||||
import TimePicker, { parseTime } from './TimePicker';
|
import TimePicker, { parseTime } from './TimePicker';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
|
import {
|
||||||
|
changeSize,
|
||||||
|
changeTime,
|
||||||
|
initializeExplore,
|
||||||
|
modifyQueries,
|
||||||
|
scanStart,
|
||||||
|
setQueries,
|
||||||
|
refreshExplore,
|
||||||
|
} from './state/actions';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
|
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 { StoreState } from 'app/types';
|
||||||
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } 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';
|
||||||
@@ -42,6 +52,8 @@ interface ExploreProps {
|
|||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
modifyQueries: typeof modifyQueries;
|
modifyQueries: typeof modifyQueries;
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
|
update: ExploreUpdateState;
|
||||||
|
refreshExplore: typeof refreshExplore;
|
||||||
scanner?: RangeScanner;
|
scanner?: RangeScanner;
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
@@ -53,8 +65,8 @@ interface ExploreProps {
|
|||||||
supportsGraph: boolean | null;
|
supportsGraph: boolean | null;
|
||||||
supportsLogs: boolean | null;
|
supportsLogs: boolean | null;
|
||||||
supportsTable: boolean | null;
|
supportsTable: boolean | null;
|
||||||
urlState: ExploreUrlState;
|
|
||||||
queryKeys: string[];
|
queryKeys: string[];
|
||||||
|
urlState: ExploreUrlState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,23 +101,22 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
*/
|
*/
|
||||||
timepickerRef: React.RefObject<TimePicker>;
|
timepickerRef: React.RefObject<TimePicker>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: ExploreProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.exploreEvents = new Emitter();
|
this.exploreEvents = new Emitter();
|
||||||
this.timepickerRef = React.createRef();
|
this.timepickerRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
const { exploreId, initialized, urlState } = this.props;
|
const { exploreId, urlState, initialized } = 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 { 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;
|
||||||
|
|
||||||
|
// initialize the whole explore first time we mount and if browser history contains a change in datasource
|
||||||
|
if (!initialized) {
|
||||||
this.props.initializeExplore(
|
this.props.initializeExplore(
|
||||||
exploreId,
|
exploreId,
|
||||||
initialDatasource,
|
initialDatasource,
|
||||||
@@ -122,7 +133,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
this.exploreEvents.removeAllListeners();
|
this.exploreEvents.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRef = el => {
|
componentDidUpdate(prevProps: ExploreProps) {
|
||||||
|
this.refreshExplore();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRef = (el: any) => {
|
||||||
this.el = el;
|
this.el = el;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +157,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onModifyQueries = (action, index?: number) => {
|
onModifyQueries = (action: any, index?: number) => {
|
||||||
const { datasourceInstance } = this.props;
|
const { datasourceInstance } = this.props;
|
||||||
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
if (datasourceInstance && datasourceInstance.modifyQuery) {
|
||||||
const modifier = (queries: DataQuery, modification: any) => datasourceInstance.modifyQuery(queries, modification);
|
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 });
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
StartPage,
|
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 explore = state.explore;
|
||||||
const { split } = explore;
|
const { split } = explore;
|
||||||
const item: ExploreItemState = explore[exploreId];
|
const item: ExploreItemState = explore[exploreId];
|
||||||
@@ -258,6 +281,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
supportsLogs,
|
supportsLogs,
|
||||||
supportsTable,
|
supportsTable,
|
||||||
queryKeys,
|
queryKeys,
|
||||||
|
urlState,
|
||||||
|
update,
|
||||||
} = item;
|
} = item;
|
||||||
return {
|
return {
|
||||||
StartPage,
|
StartPage,
|
||||||
@@ -273,6 +298,8 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
supportsLogs,
|
supportsLogs,
|
||||||
supportsTable,
|
supportsTable,
|
||||||
queryKeys,
|
queryKeys,
|
||||||
|
urlState,
|
||||||
|
update,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +308,7 @@ const mapDispatchToProps = {
|
|||||||
changeTime,
|
changeTime,
|
||||||
initializeExplore,
|
initializeExplore,
|
||||||
modifyQueries,
|
modifyQueries,
|
||||||
|
refreshExplore,
|
||||||
scanStart,
|
scanStart,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
setQueries,
|
setQueries,
|
||||||
|
|||||||
@@ -2,65 +2,37 @@ import React, { Component } from 'react';
|
|||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ExploreId, ExploreUrlState } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import { parseUrlState } from 'app/core/utils/explore';
|
|
||||||
|
|
||||||
import ErrorBoundary from './ErrorBoundary';
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
import Explore from './Explore';
|
import Explore from './Explore';
|
||||||
import { CustomScrollbar } from '@grafana/ui';
|
import { CustomScrollbar } from '@grafana/ui';
|
||||||
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
|
import { resetExploreAction } from './state/actionTypes';
|
||||||
|
|
||||||
interface WrapperProps {
|
interface WrapperProps {
|
||||||
initializeExploreSplitAction: typeof initializeExploreSplitAction;
|
|
||||||
split: boolean;
|
split: boolean;
|
||||||
updateLocation: typeof updateLocation;
|
|
||||||
resetExploreAction: typeof resetExploreAction;
|
resetExploreAction: typeof resetExploreAction;
|
||||||
urlStates: { [key: string]: string };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Wrapper extends Component<WrapperProps> {
|
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() {
|
componentWillUnmount() {
|
||||||
this.props.resetExploreAction();
|
this.props.resetExploreAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { split } = this.props;
|
const { split } = this.props;
|
||||||
const { leftState, rightState } = this.urlStates;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-scrollbar-wrapper">
|
<div className="page-scrollbar-wrapper">
|
||||||
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
|
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
|
||||||
<div className="explore-wrapper">
|
<div className="explore-wrapper">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Explore exploreId={ExploreId.left} urlState={leftState} />
|
<Explore exploreId={ExploreId.left} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
{split && (
|
{split && (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Explore exploreId={ExploreId.right} urlState={rightState} />
|
<Explore exploreId={ExploreId.right} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,14 +43,11 @@ export class Wrapper extends Component<WrapperProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => {
|
const mapStateToProps = (state: StoreState) => {
|
||||||
const urlStates = state.location.query;
|
|
||||||
const { split } = state.explore;
|
const { split } = state.explore;
|
||||||
return { split, urlStates };
|
return { split };
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
initializeExploreSplitAction,
|
|
||||||
updateLocation,
|
|
||||||
resetExploreAction,
|
resetExploreAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,11 @@ import { LogLevel } from 'app/core/logs_model';
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
|
|
||||||
SplitClose = 'explore/SPLIT_CLOSE',
|
SplitClose = 'explore/SPLIT_CLOSE',
|
||||||
SplitOpen = 'explore/SPLIT_OPEN',
|
SplitOpen = 'explore/SPLIT_OPEN',
|
||||||
ResetExplore = 'explore/RESET_EXPLORE',
|
ResetExplore = 'explore/RESET_EXPLORE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitializeExploreSplitAction {
|
|
||||||
type: ActionTypes.InitializeExploreSplit;
|
|
||||||
payload: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SplitCloseAction {
|
export interface SplitCloseAction {
|
||||||
type: ActionTypes.SplitClose;
|
type: ActionTypes.SplitClose;
|
||||||
payload: {};
|
payload: {};
|
||||||
@@ -154,10 +148,6 @@ export interface RemoveQueryRowPayload {
|
|||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunQueriesEmptyPayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScanStartPayload {
|
export interface ScanStartPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
scanner: RangeScanner;
|
scanner: RangeScanner;
|
||||||
@@ -259,11 +249,6 @@ export const initializeExploreAction = actionCreatorFactory<InitializeExplorePay
|
|||||||
'explore/INITIALIZE_EXPLORE'
|
'explore/INITIALIZE_EXPLORE'
|
||||||
).create();
|
).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
|
* Display an error that happened during the selection of a datasource
|
||||||
*/
|
*/
|
||||||
@@ -342,7 +327,6 @@ export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransacti
|
|||||||
*/
|
*/
|
||||||
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
|
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
|
||||||
export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').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.
|
* 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 resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
|
||||||
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
|
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
|
||||||
|
|
||||||
export type HigherOrderAction =
|
export type HigherOrderAction = SplitCloseAction | SplitOpenAction | ResetExploreAction | ActionOf<any>;
|
||||||
| InitializeExploreSplitAction
|
|
||||||
| SplitCloseAction
|
|
||||||
| SplitOpenAction
|
|
||||||
| ResetExploreAction
|
|
||||||
| ActionOf<any>;
|
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| ActionOf<AddQueryRowPayload>
|
| ActionOf<AddQueryRowPayload>
|
||||||
@@ -435,7 +414,6 @@ export type Action =
|
|||||||
| ActionOf<QueryTransactionStartPayload>
|
| ActionOf<QueryTransactionStartPayload>
|
||||||
| ActionOf<QueryTransactionSuccessPayload>
|
| ActionOf<QueryTransactionSuccessPayload>
|
||||||
| ActionOf<RemoveQueryRowPayload>
|
| ActionOf<RemoveQueryRowPayload>
|
||||||
| ActionOf<RunQueriesEmptyPayload>
|
|
||||||
| ActionOf<ScanStartPayload>
|
| ActionOf<ScanStartPayload>
|
||||||
| ActionOf<ScanRangePayload>
|
| ActionOf<ScanRangePayload>
|
||||||
| ActionOf<SetQueriesPayload>
|
| ActionOf<SetQueriesPayload>
|
||||||
|
|||||||
147
public/app/features/explore/state/actions.test.ts
Normal file
147
public/app/features/explore/state/actions.test.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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,
|
updateHistory,
|
||||||
buildQueryTransaction,
|
buildQueryTransaction,
|
||||||
serializeStateToUrlParam,
|
serializeStateToUrlParam,
|
||||||
|
parseUrlState,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
@@ -54,7 +55,6 @@ import {
|
|||||||
queryTransactionStartAction,
|
queryTransactionStartAction,
|
||||||
queryTransactionSuccessAction,
|
queryTransactionSuccessAction,
|
||||||
scanRangeAction,
|
scanRangeAction,
|
||||||
runQueriesEmptyAction,
|
|
||||||
scanStartAction,
|
scanStartAction,
|
||||||
setQueriesAction,
|
setQueriesAction,
|
||||||
splitCloseAction,
|
splitCloseAction,
|
||||||
@@ -67,9 +67,11 @@ import {
|
|||||||
ToggleLogsPayload,
|
ToggleLogsPayload,
|
||||||
ToggleTablePayload,
|
ToggleTablePayload,
|
||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
|
runQueriesAction,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { LogsDedupStrategy } from 'app/core/logs_model';
|
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
|
import { parseTime } from '../TimePicker';
|
||||||
|
|
||||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||||
|
|
||||||
@@ -518,7 +520,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
|||||||
} = getState().explore[exploreId];
|
} = getState().explore[exploreId];
|
||||||
|
|
||||||
if (!hasNonEmptyQuery(queries)) {
|
if (!hasNonEmptyQuery(queries)) {
|
||||||
dispatch(runQueriesEmptyAction({ exploreId }));
|
dispatch(clearQueriesAction({ exploreId }));
|
||||||
dispatch(stateSave()); // Remember to saves to state and update location
|
dispatch(stateSave()); // Remember to saves to state and update location
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -527,6 +529,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
|
|||||||
// but we're using the datasource interval limit for now
|
// but we're using the datasource interval limit for now
|
||||||
const interval = datasourceInstance.interval;
|
const interval = datasourceInstance.interval;
|
||||||
|
|
||||||
|
dispatch(runQueriesAction());
|
||||||
// Keep table queries first since they need to return quickly
|
// Keep table queries first since they need to return quickly
|
||||||
if ((ignoreUIState || showingTable) && supportsTable) {
|
if ((ignoreUIState || showingTable) && supportsTable) {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -657,11 +660,15 @@ export function splitClose(): ThunkResult<void> {
|
|||||||
export function splitOpen(): ThunkResult<void> {
|
export function splitOpen(): ThunkResult<void> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// Clone left state to become the right state
|
// 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 = {
|
const itemState = {
|
||||||
...leftState,
|
...leftState,
|
||||||
queryTransactions: [],
|
queryTransactions: [],
|
||||||
queries: leftState.queries.slice(),
|
queries: leftState.queries.slice(),
|
||||||
|
exploreId: ExploreId.right,
|
||||||
|
urlState,
|
||||||
};
|
};
|
||||||
dispatch(splitOpenAction({ itemState }));
|
dispatch(splitOpenAction({ itemState }));
|
||||||
dispatch(stateSave());
|
dispatch(stateSave());
|
||||||
@@ -766,3 +773,44 @@ export const changeDedupStrategy = (exploreId, dedupStrategy: LogsDedupStrategy)
|
|||||||
dispatch(updateExploreUIState(exploreId, { dedupStrategy }));
|
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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { itemReducer, makeExploreItemState } from './reducers';
|
import { itemReducer, makeExploreItemState, exploreReducer, makeInitialUpdateState } from './reducers';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState, ExploreUrlState } from 'app/types/explore';
|
||||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import { scanStartAction, scanStopAction } from './actionTypes';
|
import { scanStartAction, scanStopAction } from './actionTypes';
|
||||||
import { Reducer } from 'redux';
|
import { Reducer } from 'redux';
|
||||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
|
import { updateLocation } from 'app/core/actions/location';
|
||||||
|
import { LogsDedupStrategy } from 'app/core/logs_model';
|
||||||
|
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||||
|
|
||||||
describe('Explore item reducer', () => {
|
describe('Explore item reducer', () => {
|
||||||
describe('scanning', () => {
|
describe('scanning', () => {
|
||||||
@@ -45,3 +48,292 @@ describe('Explore item reducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setup = (urlStateOverrides?: any) => {
|
||||||
|
const update = makeInitialUpdateState();
|
||||||
|
const urlStateDefaults: ExploreUrlState = {
|
||||||
|
datasource: 'some-datasource',
|
||||||
|
queries: [],
|
||||||
|
range: {
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
|
showingGraph: false,
|
||||||
|
showingTable: false,
|
||||||
|
showingLogs: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const urlState: ExploreUrlState = { ...urlStateDefaults, ...urlStateOverrides };
|
||||||
|
const serializedUrlState = serializeStateToUrlParam(urlState);
|
||||||
|
const initalState = { split: false, left: { urlState, update }, right: { urlState, update } };
|
||||||
|
|
||||||
|
return {
|
||||||
|
initalState,
|
||||||
|
serializedUrlState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Explore reducer', () => {
|
||||||
|
describe('when updateLocation is dispatched', () => {
|
||||||
|
describe('and payload does not contain a query', () => {
|
||||||
|
it('then it should just return state', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, {})
|
||||||
|
.whenActionIsDispatched(updateLocation({ query: null }))
|
||||||
|
.thenStateShouldEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and payload contains a query', () => {
|
||||||
|
describe("but does not contain 'left'", () => {
|
||||||
|
it('then it should just return state', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, {})
|
||||||
|
.whenActionIsDispatched(updateLocation({ query: {} }))
|
||||||
|
.thenStateShouldEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and query contains a 'right'", () => {
|
||||||
|
it('then it should add split in state', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = { ...initalState, split: true };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, initalState)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
right: serializedUrlState,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and query contains a 'left'", () => {
|
||||||
|
describe('but urlState is not set in state', () => {
|
||||||
|
it('then it should just add urlState and update in state', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const stateWithoutUrlState = { ...initalState, left: { urlState: null } };
|
||||||
|
const expectedState = { ...initalState };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithoutUrlState)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("but '/explore' is missing in path", () => {
|
||||||
|
it('then it should just add urlState and update in state', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = { ...initalState };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, initalState)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/dashboard',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and '/explore' is in path", () => {
|
||||||
|
describe('and datasource differs', () => {
|
||||||
|
it('then it should return update datasource', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
update: {
|
||||||
|
...initalState.left.update,
|
||||||
|
datasource: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const stateWithDifferentDataSource = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
urlState: {
|
||||||
|
...initalState.left.urlState,
|
||||||
|
datasource: 'different datasource',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithDifferentDataSource)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and range differs', () => {
|
||||||
|
it('then it should return update range', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
update: {
|
||||||
|
...initalState.left.update,
|
||||||
|
range: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const stateWithDifferentDataSource = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
urlState: {
|
||||||
|
...initalState.left.urlState,
|
||||||
|
range: {
|
||||||
|
from: 'now',
|
||||||
|
to: 'now-6h',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithDifferentDataSource)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and queries differs', () => {
|
||||||
|
it('then it should return update queries', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
update: {
|
||||||
|
...initalState.left.update,
|
||||||
|
queries: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const stateWithDifferentDataSource = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
urlState: {
|
||||||
|
...initalState.left.urlState,
|
||||||
|
queries: [{ expr: '{__filename__="some.log"}' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithDifferentDataSource)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and ui differs', () => {
|
||||||
|
it('then it should return update ui', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
update: {
|
||||||
|
...initalState.left.update,
|
||||||
|
ui: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const stateWithDifferentDataSource = {
|
||||||
|
...initalState,
|
||||||
|
left: {
|
||||||
|
...initalState.left,
|
||||||
|
urlState: {
|
||||||
|
...initalState.left.urlState,
|
||||||
|
ui: {
|
||||||
|
...initalState.left.urlState.ui,
|
||||||
|
showingGraph: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, stateWithDifferentDataSource)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and nothing differs', () => {
|
||||||
|
fit('then it should return update ui', () => {
|
||||||
|
const { initalState, serializedUrlState } = setup();
|
||||||
|
const expectedState = { ...initalState };
|
||||||
|
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(exploreReducer, initalState)
|
||||||
|
.whenActionIsDispatched(
|
||||||
|
updateLocation({
|
||||||
|
query: {
|
||||||
|
left: serializedUrlState,
|
||||||
|
},
|
||||||
|
path: '/explore',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenStateShouldEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
calculateResultsFromQueryTransactions,
|
calculateResultsFromQueryTransactions,
|
||||||
generateEmptyQuery,
|
generateEmptyQuery,
|
||||||
getIntervals,
|
getIntervals,
|
||||||
ensureQueries,
|
ensureQueries,
|
||||||
getQueryKeys,
|
getQueryKeys,
|
||||||
|
parseUrlState,
|
||||||
|
DEFAULT_UI_STATE,
|
||||||
} from 'app/core/utils/explore';
|
} 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 { DataQuery } from '@grafana/ui/src/types';
|
||||||
|
|
||||||
import { HigherOrderAction, ActionTypes } from './actionTypes';
|
import { HigherOrderAction, ActionTypes } from './actionTypes';
|
||||||
@@ -28,7 +32,6 @@ import {
|
|||||||
queryTransactionStartAction,
|
queryTransactionStartAction,
|
||||||
queryTransactionSuccessAction,
|
queryTransactionSuccessAction,
|
||||||
removeQueryRowAction,
|
removeQueryRowAction,
|
||||||
runQueriesEmptyAction,
|
|
||||||
scanRangeAction,
|
scanRangeAction,
|
||||||
scanStartAction,
|
scanStartAction,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
@@ -40,6 +43,8 @@ import {
|
|||||||
updateUIStateAction,
|
updateUIStateAction,
|
||||||
toggleLogLevelAction,
|
toggleLogLevelAction,
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { updateLocation } from 'app/core/actions/location';
|
||||||
|
import { LocationUpdate } from 'app/types';
|
||||||
|
|
||||||
export const DEFAULT_RANGE = {
|
export const DEFAULT_RANGE = {
|
||||||
from: 'now-6h',
|
from: 'now-6h',
|
||||||
@@ -49,6 +54,12 @@ export const DEFAULT_RANGE = {
|
|||||||
// Millies step for helper bar charts
|
// Millies step for helper bar charts
|
||||||
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
|
export const makeInitialUpdateState = (): ExploreUpdateState => ({
|
||||||
|
datasource: false,
|
||||||
|
queries: false,
|
||||||
|
range: false,
|
||||||
|
ui: false,
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* Returns a fresh Explore area state
|
* Returns a fresh Explore area state
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +87,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
|
|||||||
supportsLogs: null,
|
supportsLogs: null,
|
||||||
supportsTable: null,
|
supportsTable: null,
|
||||||
queryKeys: [],
|
queryKeys: [],
|
||||||
|
urlState: null,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,6 +208,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
initialized: true,
|
initialized: true,
|
||||||
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
queryKeys: getQueryKeys(queries, state.datasourceInstance),
|
||||||
...ui,
|
...ui,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -208,13 +222,23 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
.addMapper({
|
.addMapper({
|
||||||
filter: loadDatasourceFailureAction,
|
filter: loadDatasourceFailureAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
|
return {
|
||||||
|
...state,
|
||||||
|
datasourceError: action.payload.error,
|
||||||
|
datasourceLoading: false,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: loadDatasourceMissingAction,
|
filter: loadDatasourceMissingAction,
|
||||||
mapper: (state): ExploreItemState => {
|
mapper: (state): ExploreItemState => {
|
||||||
return { ...state, datasourceMissing: true, datasourceLoading: false };
|
return {
|
||||||
|
...state,
|
||||||
|
datasourceMissing: true,
|
||||||
|
datasourceLoading: false,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
@@ -253,6 +277,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
datasourceError: null,
|
datasourceError: null,
|
||||||
logsHighlighterExpressions: undefined,
|
logsHighlighterExpressions: undefined,
|
||||||
queryTransactions: [],
|
queryTransactions: [],
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -262,7 +287,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
const { queries, queryTransactions } = state;
|
const { queries, queryTransactions } = state;
|
||||||
const { modification, index, modifier } = action.payload;
|
const { modification, index, modifier } = action.payload;
|
||||||
let nextQueries: DataQuery[];
|
let nextQueries: DataQuery[];
|
||||||
let nextQueryTransactions;
|
let nextQueryTransactions: QueryTransaction[];
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
// Modify all queries
|
// Modify all queries
|
||||||
nextQueries = queries.map((query, i) => ({
|
nextQueries = queries.map((query, i) => ({
|
||||||
@@ -303,7 +328,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
filter: queryTransactionFailureAction,
|
filter: queryTransactionFailureAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { queryTransactions } = action.payload;
|
const { queryTransactions } = action.payload;
|
||||||
return { ...state, queryTransactions, showingStartPage: false };
|
return {
|
||||||
|
...state,
|
||||||
|
queryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
@@ -319,7 +349,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
// Append new transaction
|
// Append new transaction
|
||||||
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
|
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
|
||||||
|
|
||||||
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
|
return {
|
||||||
|
...state,
|
||||||
|
queryTransactions: nextQueryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
@@ -333,7 +368,14 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
queryIntervals.intervalMs
|
queryIntervals.intervalMs
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...state, ...results, history, queryTransactions, showingStartPage: false };
|
return {
|
||||||
|
...state,
|
||||||
|
...results,
|
||||||
|
history,
|
||||||
|
queryTransactions,
|
||||||
|
showingStartPage: false,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
@@ -367,12 +409,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
|
||||||
filter: runQueriesEmptyAction,
|
|
||||||
mapper: (state): ExploreItemState => {
|
|
||||||
return { ...state, queryTransactions: [] };
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.addMapper({
|
.addMapper({
|
||||||
filter: scanRangeAction,
|
filter: scanRangeAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
@@ -396,6 +432,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
scanning: false,
|
scanning: false,
|
||||||
scanRange: undefined,
|
scanRange: undefined,
|
||||||
scanner: undefined,
|
scanner: undefined,
|
||||||
|
update: makeInitialUpdateState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -482,6 +519,41 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
})
|
})
|
||||||
.create();
|
.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).
|
* Global Explore reducer that handles multiple Explore areas (left and right).
|
||||||
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
|
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
|
||||||
@@ -493,16 +565,30 @@ export const exploreReducer = (state = initialExploreState, action: HigherOrderA
|
|||||||
}
|
}
|
||||||
|
|
||||||
case ActionTypes.SplitOpen: {
|
case ActionTypes.SplitOpen: {
|
||||||
return { ...state, split: true, right: action.payload.itemState };
|
return { ...state, split: true, right: { ...action.payload.itemState } };
|
||||||
}
|
|
||||||
|
|
||||||
case ActionTypes.InitializeExploreSplit: {
|
|
||||||
return { ...state, split: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case ActionTypes.ResetExplore: {
|
case ActionTypes.ResetExplore: {
|
||||||
return initialExploreState;
|
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) {
|
if (action.payload) {
|
||||||
|
|||||||
@@ -248,6 +248,17 @@ export interface ExploreItemState {
|
|||||||
* Currently hidden log series
|
* Currently hidden log series
|
||||||
*/
|
*/
|
||||||
hiddenLogLevels?: LogLevel[];
|
hiddenLogLevels?: LogLevel[];
|
||||||
|
|
||||||
|
urlState: ExploreUrlState;
|
||||||
|
|
||||||
|
update: ExploreUpdateState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExploreUpdateState {
|
||||||
|
datasource: boolean;
|
||||||
|
queries: boolean;
|
||||||
|
range: boolean;
|
||||||
|
ui: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExploreUIState {
|
export interface ExploreUIState {
|
||||||
|
|||||||
64
public/test/core/thunk/thunkTester.ts
Normal file
64
public/test/core/thunk/thunkTester.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user