Commit 2c5400c6 by Hugo Häggmark Committed by kay delaney

Explore: Parses and updates TimeSrv in one place in Explore (#17677)

* Wip: Adds timeEpic

* Refactor: Introduces absoluteRange in Explore state

* Refactor: Removes changeTime action

* Tests: Adds tests for timeEpic

* Refactor: Spells AbsoluteRange correctly
parent 6be1606b
......@@ -112,6 +112,9 @@ export class TimeSrv {
private routeUpdated() {
const params = this.$location.search();
if (params.left) {
return; // explore handles this;
}
const urlRange = this.timeRangeForUrl();
// check if url has time range
if (params.from && params.to) {
......
......@@ -21,13 +21,13 @@ import TimePicker from './TimePicker';
// Actions
import {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
scanStart,
setQueries,
refreshExplore,
reconnectDatasource,
updateTimeRange,
} from './state/actions';
// Types
......@@ -60,7 +60,6 @@ import { scanStopAction } from './state/actionTypes';
interface ExploreProps {
StartPage?: ComponentClass<ExploreStartPageProps>;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
datasourceError: string;
datasourceInstance: DataSourceApi;
datasourceLoading: boolean | null;
......@@ -88,6 +87,7 @@ interface ExploreProps {
queryErrors: DataQueryError[];
mode: ExploreMode;
isLive: boolean;
updateTimeRange: typeof updateTimeRange;
}
/**
......@@ -158,11 +158,12 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.el = el;
};
onChangeTime = (range: RawTimeRange, changedByScanner?: boolean) => {
if (this.props.scanning && !changedByScanner) {
onChangeTime = (rawRange: RawTimeRange, changedByScanner?: boolean) => {
const { updateTimeRange, exploreId, scanning } = this.props;
if (scanning && !changedByScanner) {
this.onStopScanning();
}
this.props.changeTime(this.props.exploreId, range);
updateTimeRange({ exploreId, rawRange });
};
// Use this in help pages to set page to a single query
......@@ -348,7 +349,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
const mapDispatchToProps = {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
reconnectDatasource,
......@@ -356,6 +356,7 @@ const mapDispatchToProps = {
scanStart,
scanStopAction,
setQueries,
updateTimeRange,
};
export default hot(module)(
......
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { TimeRange, TimeZone, AbsoluteTimeRange, LoadingState } from '@grafana/ui';
import { TimeZone, AbsoluteTimeRange, LoadingState } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { toggleGraph, changeTime } from './state/actions';
import { toggleGraph, updateTimeRange } from './state/actions';
import Graph from './Graph';
import Panel from './Panel';
import { getTimeZone } from '../profile/state/selectors';
import { toUtc, dateTime } from '@grafana/ui/src/utils/moment_wrapper';
interface GraphContainerProps {
exploreId: ExploreId;
graphResult?: any[];
loading: boolean;
range: TimeRange;
absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone;
showingGraph: boolean;
showingTable: boolean;
split: boolean;
toggleGraph: typeof toggleGraph;
changeTime: typeof changeTime;
updateTimeRange: typeof updateTimeRange;
width: number;
}
......@@ -31,20 +30,25 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
this.props.toggleGraph(this.props.exploreId, this.props.showingGraph);
};
onChangeTime = (absRange: AbsoluteTimeRange) => {
const { exploreId, timeZone, changeTime } = this.props;
const range = {
from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to),
};
onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
const { exploreId, updateTimeRange } = this.props;
changeTime(exploreId, range);
updateTimeRange({ exploreId, absoluteRange });
};
render() {
const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width, timeZone } = this.props;
const {
exploreId,
graphResult,
loading,
showingGraph,
showingTable,
absoluteRange,
split,
width,
timeZone,
} = this.props;
const graphHeight = showingGraph && showingTable ? 200 : 400;
const timeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
return (
<Panel label="Graph" collapsible isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
......@@ -54,7 +58,7 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
height={graphHeight}
id={`explore-graph-${exploreId}`}
onChangeTime={this.onChangeTime}
range={timeRange}
range={absoluteRange}
timeZone={timeZone}
split={split}
width={width}
......@@ -69,14 +73,22 @@ function mapStateToProps(state: StoreState, { exploreId }) {
const explore = state.explore;
const { split } = explore;
const item: ExploreItemState = explore[exploreId];
const { graphResult, loadingState, range, showingGraph, showingTable } = item;
const { graphResult, loadingState, showingGraph, showingTable, absoluteRange } = item;
const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
return { graphResult, loading, range, showingGraph, showingTable, split, timeZone: getTimeZone(state.user) };
return {
graphResult,
loading,
showingGraph,
showingTable,
split,
timeZone: getTimeZone(state.user),
absoluteRange,
};
}
const mapDispatchToProps = {
toggleGraph,
changeTime,
updateTimeRange,
};
export default hot(module)(
......
......@@ -7,7 +7,6 @@ import {
Switch,
LogLevel,
TimeZone,
TimeRange,
AbsoluteTimeRange,
LogsMetaKind,
LogsModel,
......@@ -58,7 +57,7 @@ interface Props {
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
range: TimeRange;
absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone;
scanning?: boolean;
scanRange?: RawTimeRange;
......@@ -167,7 +166,7 @@ export default class Logs extends PureComponent<Props, State> {
highlighterExpressions,
loading = false,
onClickLabel,
range,
absoluteRange,
timeZone,
scanning,
scanRange,
......@@ -206,10 +205,6 @@ export default class Logs extends PureComponent<Props, State> {
const timeSeries = data.series
? data.series.map(series => new TimeSeries(series))
: [new TimeSeries({ datapoints: [] })];
const absRange = {
from: range.from.valueOf(),
to: range.to.valueOf(),
};
return (
<div className="logs-panel">
......@@ -218,7 +213,7 @@ export default class Logs extends PureComponent<Props, State> {
data={timeSeries}
height={100}
width={width}
range={absRange}
range={absoluteRange}
timeZone={timeZone}
id={`explore-logs-graph-${exploreId}`}
onChangeTime={this.props.onChangeTime}
......
......@@ -3,12 +3,9 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import {
RawTimeRange,
TimeRange,
LogLevel,
TimeZone,
AbsoluteTimeRange,
toUtc,
dateTime,
DataSourceApi,
LogsModel,
LogRowModel,
......@@ -19,7 +16,7 @@ import {
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { changeDedupStrategy, changeTime } from './state/actions';
import { changeDedupStrategy, updateTimeRange } from './state/actions';
import Logs from './Logs';
import Panel from './Panel';
import { toggleLogLevelAction, changeRefreshIntervalAction } from 'app/features/explore/state/actionTypes';
......@@ -39,7 +36,6 @@ interface LogsContainerProps {
onClickLabel: (key: string, value: string) => void;
onStartScanning: () => void;
onStopScanning: () => void;
range: TimeRange;
timeZone: TimeZone;
scanning?: boolean;
scanRange?: RawTimeRange;
......@@ -48,20 +44,17 @@ interface LogsContainerProps {
dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
width: number;
changeTime: typeof changeTime;
isLive: boolean;
stopLive: typeof changeRefreshIntervalAction;
updateTimeRange: typeof updateTimeRange;
absoluteRange: AbsoluteTimeRange;
}
export class LogsContainer extends Component<LogsContainerProps> {
onChangeTime = (absRange: AbsoluteTimeRange) => {
const { exploreId, timeZone, changeTime } = this.props;
const range = {
from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to),
};
changeTime(exploreId, range);
onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
const { exploreId, updateTimeRange } = this.props;
updateTimeRange({ exploreId, absoluteRange });
};
onStopLive = () => {
......@@ -111,7 +104,7 @@ export class LogsContainer extends Component<LogsContainerProps> {
onClickLabel,
onStartScanning,
onStopScanning,
range,
absoluteRange,
timeZone,
scanning,
scanRange,
......@@ -143,7 +136,7 @@ export class LogsContainer extends Component<LogsContainerProps> {
onStopScanning={onStopScanning}
onDedupStrategyChange={this.handleDedupStrategyChange}
onToggleLogLevel={this.hangleToggleLogLevel}
range={range}
absoluteRange={absoluteRange}
timeZone={timeZone}
scanning={scanning}
scanRange={scanRange}
......@@ -165,9 +158,9 @@ function mapStateToProps(state: StoreState, { exploreId }) {
loadingState,
scanning,
scanRange,
range,
datasourceInstance,
isLive,
absoluteRange,
} = item;
const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
const { dedupStrategy } = exploreItemUIStateSelector(item);
......@@ -181,21 +174,21 @@ function mapStateToProps(state: StoreState, { exploreId }) {
logsResult,
scanning,
scanRange,
range,
timeZone,
dedupStrategy,
hiddenLogLevels,
dedupedResult,
datasourceInstance,
isLive,
absoluteRange,
};
}
const mapDispatchToProps = {
changeDedupStrategy,
toggleLogLevelAction,
changeTime,
stopLive: changeRefreshIntervalAction,
updateTimeRange,
};
export default hot(module)(
......
......@@ -14,6 +14,7 @@ import {
TimeSeries,
DataQueryResponseData,
LoadingState,
AbsoluteTimeRange,
} from '@grafana/ui/src/types';
import {
ExploreId,
......@@ -73,11 +74,6 @@ export interface ChangeSizePayload {
height: number;
}
export interface ChangeTimePayload {
exploreId: ExploreId;
range: TimeRange;
}
export interface ChangeRefreshIntervalPayload {
exploreId: ExploreId;
refreshInterval: string;
......@@ -233,7 +229,6 @@ export interface LoadExploreDataSourcesPayload {
export interface RunQueriesPayload {
exploreId: ExploreId;
range: TimeRange;
}
export interface ResetQueryErrorPayload {
......@@ -274,6 +269,13 @@ export interface LimitMessageRatePayload {
export interface ChangeRangePayload {
exploreId: ExploreId;
range: TimeRange;
absoluteRange: AbsoluteTimeRange;
}
export interface UpdateTimeRangePayload {
exploreId: ExploreId;
rawRange?: RawTimeRange;
absoluteRange?: AbsoluteTimeRange;
}
/**
......@@ -306,11 +308,6 @@ export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore
/**
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
*/
export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
/**
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
*/
export const changeRefreshIntervalAction = actionCreatorFactory<ChangeRefreshIntervalPayload>(
'explore/CHANGE_REFRESH_INTERVAL'
).create();
......@@ -490,6 +487,8 @@ export const limitMessageRatePayloadAction = actionCreatorFactory<LimitMessageRa
export const changeRangeAction = actionCreatorFactory<ChangeRangePayload>('explore/CHANGE_RANGE').create();
export const updateTimeRangeAction = actionCreatorFactory<UpdateTimeRangePayload>('explore/UPDATE_TIMERANGE').create();
export type HigherOrderAction =
| ActionOf<SplitCloseActionPayload>
| SplitOpenAction
......
......@@ -4,7 +4,6 @@ import { thunkTester } from 'test/core/thunk/thunkTester';
import {
initializeExploreAction,
InitializeExplorePayload,
changeTimeAction,
updateUIStateAction,
setQueriesAction,
testDataSourcePendingAction,
......@@ -12,6 +11,7 @@ import {
testDataSourceFailureAction,
loadDatasourcePendingAction,
loadDatasourceReadyAction,
updateTimeRangeAction,
} from './actionTypes';
import { Emitter } from 'app/core/core';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
......@@ -118,15 +118,15 @@ describe('refreshExplore', () => {
});
describe('and update range is set', () => {
it('then it should dispatch changeTimeAction', async () => {
it('then it should dispatch updateTimeRangeAction', async () => {
const { exploreId, range, initialState } = setup({ range: true });
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions[0].type).toEqual(changeTimeAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, range });
expect(dispatchedActions[0].type).toEqual(updateTimeRangeAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, rawRange: range.raw });
});
});
......
......@@ -24,6 +24,7 @@ import {
DataSourceSelectItem,
QueryFixAction,
LogsDedupStrategy,
AbsoluteTimeRange,
} from '@grafana/ui';
import { ExploreId, RangeScanner, ExploreUIState, QueryTransaction, ExploreMode } from 'app/types/explore';
import {
......@@ -33,7 +34,6 @@ import {
ChangeRefreshIntervalPayload,
changeSizeAction,
ChangeSizePayload,
changeTimeAction,
clearQueriesAction,
initializeExploreAction,
loadDatasourceMissingAction,
......@@ -61,6 +61,7 @@ import {
scanRangeAction,
runQueriesAction,
stateSaveAction,
updateTimeRangeAction,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
import { getTimeZone } from 'app/features/profile/state/selectors';
......@@ -164,17 +165,16 @@ export function changeSize(
return changeSizeAction({ exploreId, height, width });
}
/**
* Change the time range of Explore. Usually called from the Time picker or a graph interaction.
*/
export function changeTime(exploreId: ExploreId, rawRange: RawTimeRange): ThunkResult<void> {
return (dispatch, getState) => {
const timeZone = getTimeZone(getState().user);
const range = getTimeRange(timeZone, rawRange);
dispatch(changeTimeAction({ exploreId, range }));
dispatch(runQueries(exploreId));
export const updateTimeRange = (options: {
exploreId: ExploreId;
rawRange?: RawTimeRange;
absoluteRange?: AbsoluteTimeRange;
}): ThunkResult<void> => {
return dispatch => {
dispatch(updateTimeRangeAction({ ...options }));
dispatch(runQueries(options.exploreId));
};
}
};
/**
* Change the refresh interval of Explore. Called from the Refresh picker.
......@@ -402,12 +402,8 @@ export function modifyQueries(
*/
export function runQueries(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const { range } = getState().explore[exploreId];
const timeZone = getTimeZone(getState().user);
const updatedRange = getTimeRange(timeZone, range.raw);
dispatch(runQueriesAction({ exploreId, range: updatedRange }));
dispatch(updateTimeRangeAction({ exploreId }));
dispatch(runQueriesAction({ exploreId }));
};
}
......@@ -548,7 +544,7 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
}
if (update.range) {
dispatch(changeTimeAction({ exploreId, range }));
dispatch(updateTimeRangeAction({ exploreId, rawRange: range.raw }));
}
// need to refresh ui state
......
......@@ -3,7 +3,14 @@ import { Observable, Subject } from 'rxjs';
import { mergeMap, catchError, takeUntil, filter } from 'rxjs/operators';
import _, { isString } from 'lodash';
import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
import { DataStreamState, LoadingState, DataQueryResponse, SeriesData, DataQueryResponseData } from '@grafana/ui';
import {
DataStreamState,
LoadingState,
DataQueryResponse,
SeriesData,
DataQueryResponseData,
AbsoluteTimeRange,
} from '@grafana/ui';
import * as dateMath from '@grafana/ui/src/utils/datemath';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
......@@ -115,15 +122,24 @@ export const runQueriesBatchEpic: Epic<ActionOf<any>, ActionOf<any>, StoreState>
if (state === LoadingState.Streaming) {
if (event.request && event.request.range) {
let newRange = event.request.range;
let absoluteRange: AbsoluteTimeRange = {
from: newRange.from.valueOf(),
to: newRange.to.valueOf(),
};
if (isString(newRange.raw.from)) {
newRange = {
from: dateMath.parse(newRange.raw.from, false),
to: dateMath.parse(newRange.raw.to, true),
raw: newRange.raw,
};
absoluteRange = {
from: newRange.from.valueOf(),
to: newRange.to.valueOf(),
};
}
outerObservable.next(changeRangeAction({ exploreId, range: newRange }));
outerObservable.next(changeRangeAction({ exploreId, range: newRange, absoluteRange }));
}
outerObservable.next(
limitMessageRatePayloadAction({
exploreId,
......
......@@ -13,7 +13,7 @@ describe('runQueriesEpic', () => {
const { exploreId, state, datasourceInterval, containerWidth } = mockExploreState({ queries });
epicTester(runQueriesEpic, state)
.whenActionIsDispatched(runQueriesAction({ exploreId, range: null }))
.whenActionIsDispatched(runQueriesAction({ exploreId }))
.thenResultingActionsEqual(
runQueriesBatchAction({
exploreId,
......@@ -33,7 +33,7 @@ describe('runQueriesEpic', () => {
});
epicTester(runQueriesEpic, state)
.whenActionIsDispatched(runQueriesAction({ exploreId, range: null }))
.whenActionIsDispatched(runQueriesAction({ exploreId }))
.thenResultingActionsEqual(
runQueriesBatchAction({
exploreId,
......@@ -50,7 +50,7 @@ describe('runQueriesEpic', () => {
const { exploreId, state } = mockExploreState({ queries });
epicTester(runQueriesEpic, state)
.whenActionIsDispatched(runQueriesAction({ exploreId, range: null }))
.whenActionIsDispatched(runQueriesAction({ exploreId }))
.thenResultingActionsEqual(clearQueriesAction({ exploreId }), stateSaveAction());
});
});
......@@ -63,7 +63,7 @@ describe('runQueriesEpic', () => {
});
epicTester(runQueriesEpic, state)
.whenActionIsDispatched(runQueriesAction({ exploreId, range: null }))
.whenActionIsDispatched(runQueriesAction({ exploreId }))
.thenNoActionsWhereDispatched();
});
});
......
import { dateTime, DefaultTimeZone } from '@grafana/ui';
import { epicTester } from 'test/core/redux/epicTester';
import { mockExploreState } from 'test/mocks/mockExploreState';
import { timeEpic } from './timeEpic';
import { updateTimeRangeAction, changeRangeAction } from '../actionTypes';
import { EpicDependencies } from 'app/store/configureStore';
const from = dateTime('2019-01-01 10:00:00.000Z');
const to = dateTime('2019-01-01 16:00:00.000Z');
const rawFrom = 'now-6h';
const rawTo = 'now';
const rangeMock = {
from,
to,
raw: {
from: rawFrom,
to: rawTo,
},
};
describe('timeEpic', () => {
describe('when updateTimeRangeAction is dispatched', () => {
describe('and no rawRange is supplied', () => {
describe('and no absoluteRange is supplied', () => {
it('then the correct actions are dispatched', () => {
const { exploreId, state, range } = mockExploreState({ range: rangeMock });
const absoluteRange = { from: range.from.valueOf(), to: range.to.valueOf() };
const stateToTest = { ...state, user: { timeZone: 'browser', orgId: -1 } };
const getTimeRange = jest.fn().mockReturnValue(rangeMock);
const dependencies: Partial<EpicDependencies> = {
getTimeRange,
};
epicTester(timeEpic, stateToTest, dependencies)
.whenActionIsDispatched(updateTimeRangeAction({ exploreId }))
.thenDependencyWasCalledTimes(1, 'getTimeSrv', 'init')
.thenDependencyWasCalledTimes(1, 'getTimeRange')
.thenDependencyWasCalledWith([DefaultTimeZone, rangeMock.raw], 'getTimeRange')
.thenResultingActionsEqual(
changeRangeAction({
exploreId,
range,
absoluteRange,
})
);
});
});
describe('and absoluteRange is supplied', () => {
it('then the correct actions are dispatched', () => {
const { exploreId, state, range } = mockExploreState({ range: rangeMock });
const absoluteRange = { from: range.from.valueOf(), to: range.to.valueOf() };
const stateToTest = { ...state, user: { timeZone: 'browser', orgId: -1 } };
const getTimeRange = jest.fn().mockReturnValue(rangeMock);
const dependencies: Partial<EpicDependencies> = {
getTimeRange,
};
epicTester(timeEpic, stateToTest, dependencies)
.whenActionIsDispatched(updateTimeRangeAction({ exploreId, absoluteRange }))
.thenDependencyWasCalledTimes(1, 'getTimeSrv', 'init')
.thenDependencyWasCalledTimes(1, 'getTimeRange')
.thenDependencyWasCalledWith([DefaultTimeZone, { from: null, to: null }], 'getTimeRange')
.thenDependencyWasCalledTimes(2, 'dateTime')
.thenResultingActionsEqual(
changeRangeAction({
exploreId,
range,
absoluteRange,
})
);
});
});
});
describe('and rawRange is supplied', () => {
describe('and no absoluteRange is supplied', () => {
it('then the correct actions are dispatched', () => {
const { exploreId, state, range } = mockExploreState({ range: rangeMock });
const rawRange = { from: 'now-5m', to: 'now' };
const absoluteRange = { from: range.from.valueOf(), to: range.to.valueOf() };
const stateToTest = { ...state, user: { timeZone: 'browser', orgId: -1 } };
const getTimeRange = jest.fn().mockReturnValue(rangeMock);
const dependencies: Partial<EpicDependencies> = {
getTimeRange,
};
epicTester(timeEpic, stateToTest, dependencies)
.whenActionIsDispatched(updateTimeRangeAction({ exploreId, rawRange }))
.thenDependencyWasCalledTimes(1, 'getTimeSrv', 'init')
.thenDependencyWasCalledTimes(1, 'getTimeRange')
.thenDependencyWasCalledWith([DefaultTimeZone, rawRange], 'getTimeRange')
.thenResultingActionsEqual(
changeRangeAction({
exploreId,
range,
absoluteRange,
})
);
});
});
});
});
});
import { Epic } from 'redux-observable';
import { map } from 'rxjs/operators';
import { AbsoluteTimeRange, RawTimeRange } from '@grafana/ui';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { StoreState } from 'app/types/store';
import { updateTimeRangeAction, UpdateTimeRangePayload, changeRangeAction } from '../actionTypes';
export const timeEpic: Epic<ActionOf<any>, ActionOf<any>, StoreState> = (
action$,
state$,
{ getTimeSrv, getTimeRange, getTimeZone, toUtc, dateTime }
) => {
return action$.ofType(updateTimeRangeAction.type).pipe(
map((action: ActionOf<UpdateTimeRangePayload>) => {
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = action.payload;
const itemState = state$.value.explore[exploreId];
const timeZone = getTimeZone(state$.value.user);
const { range: rangeInState } = itemState;
let rawRange: RawTimeRange = rangeInState.raw;
if (absRange) {
rawRange = {
from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from),
to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to),
};
}
if (actionRange) {
rawRange = actionRange;
}
const range = getTimeRange(timeZone, rawRange);
const absoluteRange: AbsoluteTimeRange = { from: range.from.valueOf(), to: range.to.valueOf() };
getTimeSrv().init({
time: range.raw,
refresh: false,
getTimezone: () => timeZone.raw,
timeRangeUpdated: () => undefined,
});
return changeRangeAction({ exploreId, range, absoluteRange });
})
);
};
......@@ -4,7 +4,6 @@ import {
exploreReducer,
makeInitialUpdateState,
initialExploreState,
DEFAULT_RANGE,
} from './reducers';
import {
ExploreId,
......@@ -32,7 +31,7 @@ import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { updateLocation } from 'app/core/actions/location';
import { serializeStateToUrlParam } from 'app/core/utils/explore';
import TableModel from 'app/core/table_model';
import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy, LoadingState, dateTime } from '@grafana/ui';
import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy, LoadingState } from '@grafana/ui';
describe('Explore item reducer', () => {
describe('scanning', () => {
......@@ -193,16 +192,12 @@ describe('Explore item reducer', () => {
intervalMs: 1000,
},
showingStartPage: false,
range: {
from: dateTime(),
to: dateTime(),
raw: DEFAULT_RANGE,
},
range: null,
};
reducerTester()
.givenReducer(itemReducer, initalState)
.whenActionIsDispatched(runQueriesAction({ exploreId: ExploreId.left, range: expectedState.range }))
.whenActionIsDispatched(runQueriesAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual(expectedState);
});
});
......
......@@ -36,7 +36,6 @@ import {
addQueryRowAction,
changeQueryAction,
changeSizeAction,
changeTimeAction,
changeRefreshIntervalAction,
clearQueriesAction,
highlightLogsExpressionAction,
......@@ -95,6 +94,10 @@ export const makeExploreItemState = (): ExploreItemState => ({
to: null,
raw: DEFAULT_RANGE,
},
absoluteRange: {
from: null,
to: null,
},
scanning: false,
scanRange: null,
showingGraph: true,
......@@ -175,12 +178,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
},
})
.addMapper({
filter: changeTimeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, range: action.payload.range };
},
})
.addMapper({
filter: changeRefreshIntervalAction,
mapper: (state, action): ExploreItemState => {
const { refreshInterval } = action.payload;
......@@ -520,8 +517,8 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
})
.addMapper({
filter: runQueriesAction,
mapper: (state, action): ExploreItemState => {
const { range } = action.payload;
mapper: (state): ExploreItemState => {
const { range } = state;
const { datasourceInstance, containerWidth } = state;
let interval = '1s';
if (datasourceInstance && datasourceInstance.interval) {
......@@ -575,9 +572,11 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
.addMapper({
filter: changeRangeAction,
mapper: (state, action): ExploreItemState => {
const { range, absoluteRange } = action.payload;
return {
...state,
range: action.payload.range,
range,
absoluteRange,
};
},
})
......
......@@ -28,11 +28,24 @@ import {
DataSourceJsonData,
DataQueryRequest,
DataStreamObserver,
TimeZone,
RawTimeRange,
TimeRange,
DateTimeInput,
FormatInput,
DateTime,
toUtc,
dateTime,
} from '@grafana/ui';
import { Observable } from 'rxjs';
import { getQueryResponse } from 'app/core/utils/explore';
import { StoreState } from 'app/types/store';
import { toggleLogActionsMiddleware } from 'app/core/middlewares/application';
import { timeEpic } from 'app/features/explore/state/epics/timeEpic';
import { TimeSrv, getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { UserState } from 'app/types/user';
import { getTimeRange } from 'app/core/utils/explore';
import { getTimeZone } from 'app/features/profile/state/selectors';
const rootReducers = {
...sharedReducers,
......@@ -59,7 +72,8 @@ export const rootEpic: any = combineEpics(
runQueriesEpic,
runQueriesBatchEpic,
processQueryResultsEpic,
processQueryErrorsEpic
processQueryErrorsEpic,
timeEpic
);
export interface EpicDependencies {
......@@ -68,10 +82,20 @@ export interface EpicDependencies {
options: DataQueryRequest<DataQuery>,
observer?: DataStreamObserver
) => Observable<DataQueryResponse>;
getTimeSrv: () => TimeSrv;
getTimeRange: (timeZone: TimeZone, rawRange: RawTimeRange) => TimeRange;
getTimeZone: (state: UserState) => TimeZone;
toUtc: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
dateTime: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
}
const dependencies: EpicDependencies = {
getQueryResponse,
getTimeSrv,
getTimeRange,
getTimeZone,
toUtc,
dateTime,
};
const epicMiddleware = createEpicMiddleware({ dependencies });
......
......@@ -12,6 +12,7 @@ import {
LogsModel,
LogsDedupStrategy,
LoadingState,
AbsoluteTimeRange,
} from '@grafana/ui';
import { Emitter } from 'app/core/core';
......@@ -189,6 +190,8 @@ export interface ExploreItemState {
* Time range for this Explore. Managed by the time picker and used by all query runs.
*/
range: TimeRange;
absoluteRange: AbsoluteTimeRange;
/**
* Scanner function that calculates a new range, triggers a query run, and returns the new range.
*/
......
......@@ -8,15 +8,19 @@ import {
DataStreamObserver,
DataQueryResponse,
DataStreamState,
DefaultTimeZone,
} from '@grafana/ui';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { StoreState } from 'app/types/store';
import { EpicDependencies } from 'app/store/configureStore';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DEFAULT_RANGE } from 'app/core/utils/explore';
export const epicTester = (
epic: Epic<ActionOf<any>, ActionOf<any>, StoreState, EpicDependencies>,
state?: Partial<StoreState>
state?: Partial<StoreState>,
dependencies?: Partial<EpicDependencies>
) => {
const resultingActions: Array<ActionOf<any>> = [];
const action$ = new Subject<ActionOf<any>>();
......@@ -35,12 +39,35 @@ export const epicTester = (
}
return queryResponse$;
};
const init = jest.fn();
const getTimeSrv = (): TimeSrv => {
const timeSrvMock: TimeSrv = {} as TimeSrv;
const dependencies: EpicDependencies = {
return Object.assign(timeSrvMock, { init });
};
const getTimeRange = jest.fn().mockReturnValue(DEFAULT_RANGE);
const getTimeZone = jest.fn().mockReturnValue(DefaultTimeZone);
const toUtc = jest.fn().mockReturnValue(null);
const dateTime = jest.fn().mockReturnValue(null);
const defaultDependencies: EpicDependencies = {
getQueryResponse,
getTimeSrv,
getTimeRange,
getTimeZone,
toUtc,
dateTime,
};
epic(actionObservable$, stateObservable$, dependencies).subscribe({ next: action => resultingActions.push(action) });
const theDependencies: EpicDependencies = { ...defaultDependencies, ...dependencies };
epic(actionObservable$, stateObservable$, theDependencies).subscribe({
next: action => resultingActions.push(action),
});
const whenActionIsDispatched = (action: ActionOf<any>) => {
action$.next(action);
......@@ -78,6 +105,32 @@ export const epicTester = (
return instance;
};
const getDependencyMock = (dependency: string, method?: string) => {
const dep = theDependencies[dependency];
let mock = null;
if (dep instanceof Function) {
mock = method ? dep()[method] : dep();
} else {
mock = method ? dep[method] : dep;
}
return mock;
};
const thenDependencyWasCalledTimes = (times: number, dependency: string, method?: string) => {
const mock = getDependencyMock(dependency, method);
expect(mock).toBeCalledTimes(times);
return instance;
};
const thenDependencyWasCalledWith = (args: any[], dependency: string, method?: string) => {
const mock = getDependencyMock(dependency, method);
expect(mock).toBeCalledWith(...args);
return instance;
};
const instance = {
whenActionIsDispatched,
whenQueryReceivesResponse,
......@@ -85,6 +138,8 @@ export const epicTester = (
whenQueryObserverReceivesEvent,
thenResultingActionsEqual,
thenNoActionsWhereDispatched,
thenDependencyWasCalledTimes,
thenDependencyWasCalledWith,
};
return instance;
......
......@@ -3,6 +3,7 @@ import { DataSourceApi } from '@grafana/ui/src/types/datasource';
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
import { makeExploreItemState } from 'app/features/explore/state/reducers';
import { StoreState } from 'app/types';
import { TimeRange, dateTime } from '@grafana/ui';
export const mockExploreState = (options: any = {}) => {
const isLive = options.isLive || false;
......@@ -31,6 +32,14 @@ export const mockExploreState = (options: any = {}) => {
},
interval: datasourceInterval,
};
const range: TimeRange = options.range || {
from: dateTime('2019-01-01 10:00:00.000Z'),
to: dateTime('2019-01-01 16:00:00.000Z'),
raw: {
from: 'now-6h',
to: 'now',
},
};
const urlReplaced = options.urlReplaced || false;
const left: ExploreItemState = options.left || {
...makeExploreItemState(),
......@@ -45,6 +54,7 @@ export const mockExploreState = (options: any = {}) => {
scanner,
scanning,
urlReplaced,
range,
};
const right: ExploreItemState = options.right || {
...makeExploreItemState(),
......@@ -59,6 +69,7 @@ export const mockExploreState = (options: any = {}) => {
scanner,
scanning,
urlReplaced,
range,
};
const split: boolean = options.split || false;
const explore: ExploreState = {
......@@ -82,5 +93,6 @@ export const mockExploreState = (options: any = {}) => {
refreshInterval,
state,
scanner,
range,
};
};
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