diff --git a/packages/grafana-data/src/types/time.ts b/packages/grafana-data/src/types/time.ts index 0eb2c51929d..4887961b9a3 100644 --- a/packages/grafana-data/src/types/time.ts +++ b/packages/grafana-data/src/types/time.ts @@ -41,3 +41,9 @@ export interface TimeOptions { export type TimeFragment = string | DateTime; export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export const DefaultTimeRange: TimeRange = { + from: {} as DateTime, + to: {} as DateTime, + raw: { from: '6h', to: 'now' }, +}; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 3eeab063966..618bc3d8dae 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -16,6 +16,8 @@ export interface PanelData { series: DataFrame[]; request?: DataQueryRequest; error?: DataQueryError; + // Contains the range from the request or a shifted time range if a request uses relative time + timeRange: TimeRange; } export interface PanelProps { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index a6d5d1fa36c..0d7dadaa928 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -14,7 +14,7 @@ import templateSrv from 'app/features/templating/template_srv'; import config from 'app/core/config'; // Types import { DashboardModel, PanelModel } from '../state'; -import { LoadingState, ScopedVars, AbsoluteTimeRange, toUtc, toDataFrameDTO } from '@grafana/data'; +import { LoadingState, ScopedVars, AbsoluteTimeRange, toUtc, toDataFrameDTO, DefaultTimeRange } from '@grafana/data'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; @@ -52,6 +52,7 @@ export class PanelChrome extends PureComponent { data: { state: LoadingState.NotStarted, series: [], + timeRange: DefaultTimeRange, }, }; } @@ -66,6 +67,7 @@ export class PanelChrome extends PureComponent { if (this.hasPanelSnapshot) { this.setState({ data: { + ...this.state.data, state: LoadingState.Done, series: getProcessedDataFrames(panel.snapshotData), }, @@ -241,6 +243,7 @@ export class PanelChrome extends PureComponent { const PanelComponent = plugin.panel; const innerPanelHeight = calculateInnerPanelHeight(panel, height); + const timeRange = data.timeRange || this.timeSrv.timeRange(); return ( <> @@ -249,7 +252,7 @@ export class PanelChrome extends PureComponent { { data: { state: LoadingState.NotStarted, series: [], + timeRange: DefaultTimeRange, }, }; diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.test.ts b/public/app/features/dashboard/panel_editor/QueryEditorRow.test.ts index 2c98e45971c..708d9929230 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.test.ts +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.test.ts @@ -1,4 +1,4 @@ -import { LoadingState, toDataFrame } from '@grafana/data'; +import { LoadingState, toDataFrame, dateTime } from '@grafana/data'; import { PanelData, DataQueryRequest } from '@grafana/ui'; import { filterPanelDataToQuery } from './QueryEditorRow'; @@ -28,6 +28,7 @@ describe('filterPanelDataToQuery', () => { makePretendRequest('sub2'), makePretendRequest('sub3'), ]), + timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, }; it('should not have an error unless the refId matches', () => { diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index 31b0377a032..d598c5e914d 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -2,13 +2,11 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import _ from 'lodash'; - // Utils & Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { Emitter } from 'app/core/utils/emitter'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; - // Types import { PanelModel } from '../state/PanelModel'; import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, ErrorBoundaryAlert } from '@grafana/ui'; @@ -321,10 +319,13 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat state = LoadingState.Error; } + const timeRange = data.timeRange; + return { state, series, request, error, + timeRange, }; } diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 01c7408ea02..691d34f4cbc 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -1,27 +1,23 @@ // Libraries import React, { PureComponent } from 'react'; - // Utils & Services import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { StoreState } from 'app/types'; import { updateLocation } from 'app/core/actions'; - // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; - // Types -import { PanelModel } from '../state'; -import { DashboardModel } from '../state'; +import { PanelModel, DashboardModel } from '../state'; import { VizPickerSearch } from './VizPickerSearch'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; import { PanelPlugin, PanelPluginMeta, PanelData } from '@grafana/ui'; import { PanelCtrl } from 'app/plugins/sdk'; import { Unsubscribable } from 'rxjs'; -import { LoadingState } from '@grafana/data'; +import { LoadingState, DefaultTimeRange } from '@grafana/data'; interface Props { panel: PanelModel; @@ -57,6 +53,7 @@ export class VisualizationTab extends PureComponent { data: { state: LoadingState.NotStarted, series: [], + timeRange: DefaultTimeRange, }, }; } diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 7d8fcaed7df..c9854816920 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -1,17 +1,22 @@ // Libraries import _ from 'lodash'; - // Utils import kbn from 'app/core/utils/kbn'; import coreModule from 'app/core/core_module'; -import { dateMath } from '@grafana/data'; - // Types -import { TimeRange, RawTimeRange, TimeZone } from '@grafana/data'; +import { + dateMath, + DefaultTimeRange, + TimeRange, + RawTimeRange, + TimeZone, + toUtc, + dateTime, + isDateTime, +} from '@grafana/data'; import { ITimeoutService, ILocationService } from 'angular'; import { ContextSrv } from 'app/core/services/context_srv'; import { DashboardModel } from '../state/DashboardModel'; -import { toUtc, dateTime, isDateTime } from '@grafana/data'; import { getZoomedTimeRange, getShiftedTimeRange } from 'app/core/utils/timePicker'; export class TimeSrv { @@ -32,7 +37,7 @@ export class TimeSrv { private contextSrv: ContextSrv ) { // default time - this.time = { from: '6h', to: 'now' }; + this.time = DefaultTimeRange.raw; $rootScope.$on('zoom-out', this.zoomOut.bind(this)); $rootScope.$on('shift-time', this.shiftTime.bind(this)); diff --git a/public/app/features/dashboard/state/runRequest.test.ts b/public/app/features/dashboard/state/runRequest.test.ts index f305c1bd693..d48ea0590f6 100644 --- a/public/app/features/dashboard/state/runRequest.test.ts +++ b/public/app/features/dashboard/state/runRequest.test.ts @@ -2,6 +2,7 @@ import { DataFrame, LoadingState, dateTime } from '@grafana/data'; import { PanelData, DataSourceApi, DataQueryRequest, DataQueryResponse } from '@grafana/ui'; import { Subscriber, Observable, Subscription } from 'rxjs'; import { runRequest } from './runRequest'; +import { deepFreeze } from '../../../../test/core/redux/reducerTester'; jest.mock('app/core/services/backend_srv'); @@ -186,19 +187,56 @@ describe('runRequest', () => { runRequestScenario('If time range is relative', ctx => { ctx.setup(async () => { + // any changes to ctx.request.range will throw and state would become LoadingState.Error + deepFreeze(ctx.request.range); ctx.start(); + // wait a bit await sleep(20); ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] }); }); - it('should update returned request range', () => { - expect(ctx.results[0].request.range.to.valueOf()).not.toBe(ctx.fromStartTime); + it('should add the correct timeRange property and the request range should not be mutated', () => { + expect(ctx.results[0].timeRange.to.valueOf()).toBeDefined(); + expect(ctx.results[0].timeRange.to.valueOf()).not.toBe(ctx.toStartTime.valueOf()); + expect(ctx.results[0].timeRange.to.valueOf()).not.toBe(ctx.results[0].request.range.to.valueOf()); + + expectThatRangeHasNotMutated(ctx); + }); + }); + + runRequestScenario('If time range is not relative', ctx => { + ctx.setup(async () => { + ctx.request.range.raw.from = ctx.fromStartTime; + ctx.request.range.raw.to = ctx.toStartTime; + // any changes to ctx.request.range will throw and state would become LoadingState.Error + deepFreeze(ctx.request.range); + ctx.start(); + + // wait a bit + await sleep(20); + + ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] }); + }); + + it('should add the correct timeRange property and the request range should not be mutated', () => { + expect(ctx.results[0].timeRange).toBeDefined(); + expect(ctx.results[0].timeRange.to.valueOf()).toBe(ctx.toStartTime.valueOf()); + expect(ctx.results[0].timeRange.to.valueOf()).toBe(ctx.results[0].request.range.to.valueOf()); + + expectThatRangeHasNotMutated(ctx); }); }); }); +const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => { + // Make sure that the range for request is not changed and that deepfreeze hasn't thrown + expect(ctx.results[0].request.range.to.valueOf()).toBe(ctx.toStartTime.valueOf()); + expect(ctx.results[0].error).not.toBeDefined(); + expect(ctx.results[0].state).toBe(LoadingState.Done); +}; + async function sleep(ms: number) { return new Promise(resolve => { setTimeout(resolve, ms); diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts index 7000ede35b3..7dce1c59eea 100644 --- a/public/app/features/dashboard/state/runRequest.ts +++ b/public/app/features/dashboard/state/runRequest.ts @@ -34,14 +34,14 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ packets[packet.key || 'A'] = packet; // Update the time range - let timeRange = request.range; - if (isString(timeRange.raw.from)) { - timeRange = { - from: dateMath.parse(timeRange.raw.from, false), - to: dateMath.parse(timeRange.raw.to, true), - raw: timeRange.raw, - }; - } + const range = { ...request.range }; + const timeRange = isString(range.raw.from) + ? { + from: dateMath.parse(range.raw.from, false), + to: dateMath.parse(range.raw.to, true), + raw: range.raw, + } + : range; const combinedData = flatten( lodashMap(packets, (packet: DataQueryResponse) => { @@ -52,10 +52,8 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ const panelData = { state: packet.state || LoadingState.Done, series: combinedData, - request: { - ...request, - range: timeRange, - }, + request, + timeRange, }; return { packets, panelData }; @@ -75,6 +73,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) state: LoadingState.Loading, series: [], request: request, + timeRange: request.range, }, packets: {}, }; @@ -96,6 +95,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest) request.endTime = Date.now(); state = processResponsePacket(packet, state); + return state.panelData; }), // handle errors diff --git a/public/app/features/explore/QueryStatus.test.tsx b/public/app/features/explore/QueryStatus.test.tsx index 46debf76e8e..e3e44f9ffca 100644 --- a/public/app/features/explore/QueryStatus.test.tsx +++ b/public/app/features/explore/QueryStatus.test.tsx @@ -1,19 +1,19 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { LoadingState } from '@grafana/data'; +import { LoadingState, TimeRange } from '@grafana/data'; import { PanelData } from '@grafana/ui'; import QueryStatus from './QueryStatus'; describe('', () => { it('should render with a latency', () => { - const res: PanelData = { series: [], state: LoadingState.Done }; + const res: PanelData = { series: [], state: LoadingState.Done, timeRange: {} as TimeRange }; const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should not render when query has not started', () => { - const res: PanelData = { series: [], state: LoadingState.NotStarted }; + const res: PanelData = { series: [], state: LoadingState.NotStarted, timeRange: {} as TimeRange }; const wrapper = shallow(); expect(wrapper.getElement()).toBe(null); }); diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 79bd276e9ac..6069d8fcf5b 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -42,7 +42,7 @@ describe('Explore item reducer', () => { .givenReducer(itemReducer as Reducer>, initalState) .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left })) .thenStateShouldEqual({ - ...makeExploreItemState(), + ...initalState, scanning: true, }); }); @@ -57,7 +57,7 @@ describe('Explore item reducer', () => { .givenReducer(itemReducer as Reducer>, initalState) .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left })) .thenStateShouldEqual({ - ...makeExploreItemState(), + ...initalState, scanning: false, scanRange: undefined, }); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index d687435cb72..03a7a1a0bb6 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -10,7 +10,7 @@ import { refreshIntervalToSortOrder, } from 'app/core/utils/explore'; import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/explore'; -import { LoadingState, toLegacyResponseData } from '@grafana/data'; +import { LoadingState, toLegacyResponseData, DefaultTimeRange } from '@grafana/data'; import { DataQuery, DataSourceApi, PanelData, DataQueryRequest } from '@grafana/ui'; import { HigherOrderAction, @@ -121,6 +121,7 @@ export const createEmptyQueryResponse = (): PanelData => ({ request: {} as DataQueryRequest, series: [], error: null, + timeRange: DefaultTimeRange, }); /** diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index ee3c81e44f2..ee13a440830 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -131,15 +131,16 @@ class MetricsPanelCtrl extends PanelCtrl { } if (data.request) { - const { range, timeInfo } = data.request; - if (range) { - this.range = range; - } + const { timeInfo } = data.request; if (timeInfo) { this.timeInfo = timeInfo; } } + if (data.timeRange) { + this.range = data.timeRange; + } + if (this.useDataFrames) { this.handleDataFrames(data.series); } else { diff --git a/public/app/plugins/datasource/dashboard/runSharedRequest.ts b/public/app/plugins/datasource/dashboard/runSharedRequest.ts index 603cd65a7af..0941d85bc67 100644 --- a/public/app/plugins/datasource/dashboard/runSharedRequest.ts +++ b/public/app/plugins/datasource/dashboard/runSharedRequest.ts @@ -1,10 +1,9 @@ import { Observable } from 'rxjs'; import { DataQuery, PanelData, DataSourceApi } from '@grafana/ui'; import { QueryRunnerOptions } from 'app/features/dashboard/state/PanelQueryRunner'; -import { DashboardQuery } from './types'; +import { DashboardQuery, SHARED_DASHBODARD_QUERY } from './types'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { LoadingState } from '@grafana/data'; -import { SHARED_DASHBODARD_QUERY } from './types'; +import { LoadingState, DefaultTimeRange } from '@grafana/data'; export function isSharedDashboardQuery(datasource: string | DataSourceApi) { if (!datasource) { @@ -76,5 +75,6 @@ function getQueryError(msg: string): PanelData { state: LoadingState.Error, series: [], error: { message: msg }, + timeRange: DefaultTimeRange, }; } diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts index ffcbbf20ee3..5a0cf7f7b69 100644 --- a/public/test/core/redux/reducerTester.ts +++ b/public/test/core/redux/reducerTester.ts @@ -18,7 +18,7 @@ interface ObjectType extends Object { [key: string]: any; } -const deepFreeze = (obj: T): T => { +export const deepFreeze = (obj: T): T => { Object.freeze(obj); const isNotException = (object: any, propertyName: any) =>