mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
6be1606bd8
commit
2c5400c61e
@ -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)(
|
||||
|
@ -1,28 +1,27 @@
|
||||
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),
|
||||
};
|
||||
onChangeTime = (absoluteRange: AbsoluteTimeRange) => {
|
||||
const { exploreId, updateTimeRange } = this.props;
|
||||
|
||||
changeTime(exploreId, range);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -303,11 +305,6 @@ export const changeQueryAction = actionCreatorFactory<ChangeQueryPayload>('explo
|
||||
*/
|
||||
export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore/CHANGE_SIZE').create();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
105
public/app/features/explore/state/epics/timeEpic.test.ts
Normal file
105
public/app/features/explore/state/epics/timeEpic.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
46
public/app/features/explore/state/epics/timeEpic.ts
Normal file
46
public/app/features/explore/state/epics/timeEpic.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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,
|
||||
@ -174,12 +177,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
return { ...state, mode };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: changeTimeAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
return { ...state, range: action.payload.range };
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: changeRefreshIntervalAction,
|
||||
mapper: (state, action): ExploreItemState => {
|
||||
@ -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 = {
|
||||
getQueryResponse,
|
||||
return Object.assign(timeSrvMock, { init });
|
||||
};
|
||||
|
||||
epic(actionObservable$, stateObservable$, dependencies).subscribe({ next: action => resultingActions.push(action) });
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user