PanelData: Adds timeRange prop to PanelData (#19361)

* Refactor: Adds newTimeRange property to PanelData

* Refactor: Handles timeRange prop after requests

* Refactor: Makes timeRange mandatory

* Refactor: Adds DefaultTimeRange
This commit is contained in:
Hugo Häggmark 2019-09-25 02:19:17 -07:00 committed by Torkel Ödegaard
parent e35de167f9
commit 889f8e3131
16 changed files with 111 additions and 51 deletions

View File

@ -41,3 +41,9 @@ export interface TimeOptions {
export type TimeFragment = string | DateTime; export type TimeFragment = string | DateTime;
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; 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' },
};

View File

@ -16,6 +16,8 @@ export interface PanelData {
series: DataFrame[]; series: DataFrame[];
request?: DataQueryRequest; request?: DataQueryRequest;
error?: DataQueryError; error?: DataQueryError;
// Contains the range from the request or a shifted time range if a request uses relative time
timeRange: TimeRange;
} }
export interface PanelProps<T = any> { export interface PanelProps<T = any> {

View File

@ -14,7 +14,7 @@ import templateSrv from 'app/features/templating/template_srv';
import config from 'app/core/config'; import config from 'app/core/config';
// Types // Types
import { DashboardModel, PanelModel } from '../state'; 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'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin';
@ -52,6 +52,7 @@ export class PanelChrome extends PureComponent<Props, State> {
data: { data: {
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
series: [], series: [],
timeRange: DefaultTimeRange,
}, },
}; };
} }
@ -66,6 +67,7 @@ export class PanelChrome extends PureComponent<Props, State> {
if (this.hasPanelSnapshot) { if (this.hasPanelSnapshot) {
this.setState({ this.setState({
data: { data: {
...this.state.data,
state: LoadingState.Done, state: LoadingState.Done,
series: getProcessedDataFrames(panel.snapshotData), series: getProcessedDataFrames(panel.snapshotData),
}, },
@ -241,6 +243,7 @@ export class PanelChrome extends PureComponent<Props, State> {
const PanelComponent = plugin.panel; const PanelComponent = plugin.panel;
const innerPanelHeight = calculateInnerPanelHeight(panel, height); const innerPanelHeight = calculateInnerPanelHeight(panel, height);
const timeRange = data.timeRange || this.timeSrv.timeRange();
return ( return (
<> <>
@ -249,7 +252,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<PanelComponent <PanelComponent
id={panel.id} id={panel.id}
data={data} data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()} timeRange={timeRange}
timeZone={this.props.dashboard.getTimezone()} timeZone={this.props.dashboard.getTimezone()}
options={panel.getOptions()} options={panel.getOptions()}
transparent={panel.transparent} transparent={panel.transparent}

View File

@ -2,25 +2,29 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { css } from 'emotion'; import { css } from 'emotion';
// Components // Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector'; import { QueryInspector } from './QueryInspector';
import { QueryOptions } from './QueryOptions'; import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup, TransformationsEditor } from '@grafana/ui'; import {
PanelOptionsGroup,
TransformationsEditor,
DataQuery,
DataSourceSelectItem,
PanelData,
AlphaNotice,
PluginState,
} from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow'; import { QueryEditorRow } from './QueryEditorRow';
// Services // Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config'; import config from 'app/core/config';
// Types // Types
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { DataQuery, DataSourceSelectItem, PanelData, AlphaNotice, PluginState } from '@grafana/ui'; import { LoadingState, DataTransformerConfig, DefaultTimeRange } from '@grafana/data';
import { LoadingState, DataTransformerConfig } from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { isSharedDashboardQuery, DashboardQueryEditor } from 'app/plugins/datasource/dashboard'; import { isSharedDashboardQuery, DashboardQueryEditor } from 'app/plugins/datasource/dashboard';
@ -55,6 +59,7 @@ export class QueriesTab extends PureComponent<Props, State> {
data: { data: {
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
series: [], series: [],
timeRange: DefaultTimeRange,
}, },
}; };

View File

@ -1,4 +1,4 @@
import { LoadingState, toDataFrame } from '@grafana/data'; import { LoadingState, toDataFrame, dateTime } from '@grafana/data';
import { PanelData, DataQueryRequest } from '@grafana/ui'; import { PanelData, DataQueryRequest } from '@grafana/ui';
import { filterPanelDataToQuery } from './QueryEditorRow'; import { filterPanelDataToQuery } from './QueryEditorRow';
@ -28,6 +28,7 @@ describe('filterPanelDataToQuery', () => {
makePretendRequest('sub2'), makePretendRequest('sub2'),
makePretendRequest('sub3'), makePretendRequest('sub3'),
]), ]),
timeRange: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } },
}; };
it('should not have an error unless the refId matches', () => { it('should not have an error unless the refId matches', () => {

View File

@ -2,13 +2,11 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
// Utils & Services // Utils & Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
// Types // Types
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, ErrorBoundaryAlert } from '@grafana/ui'; import { DataQuery, DataSourceApi, PanelData, DataQueryRequest, ErrorBoundaryAlert } from '@grafana/ui';
@ -320,10 +318,13 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat
state = LoadingState.Error; state = LoadingState.Error;
} }
const timeRange = data.timeRange;
return { return {
state, state,
series, series,
request, request,
error, error,
timeRange,
}; };
} }

View File

@ -1,27 +1,23 @@
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Utils & Services // Utils & Services
import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
// Components // Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { VizTypePicker } from './VizTypePicker'; import { VizTypePicker } from './VizTypePicker';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { FadeIn } from 'app/core/components/Animations/FadeIn';
// Types // Types
import { PanelModel } from '../state'; import { PanelModel, DashboardModel } from '../state';
import { DashboardModel } from '../state';
import { VizPickerSearch } from './VizPickerSearch'; import { VizPickerSearch } from './VizPickerSearch';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { PanelPlugin, PanelPluginMeta, PanelData } from '@grafana/ui'; import { PanelPlugin, PanelPluginMeta, PanelData } from '@grafana/ui';
import { PanelCtrl } from 'app/plugins/sdk'; import { PanelCtrl } from 'app/plugins/sdk';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { LoadingState } from '@grafana/data'; import { LoadingState, DefaultTimeRange } from '@grafana/data';
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
@ -57,6 +53,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
data: { data: {
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
series: [], series: [],
timeRange: DefaultTimeRange,
}, },
}; };
} }

View File

@ -1,17 +1,22 @@
// Libraries // Libraries
import _ from 'lodash'; import _ from 'lodash';
// Utils // Utils
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { dateMath } from '@grafana/data';
// Types // 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 { ITimeoutService, ILocationService } from 'angular';
import { ContextSrv } from 'app/core/services/context_srv'; import { ContextSrv } from 'app/core/services/context_srv';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { toUtc, dateTime, isDateTime } from '@grafana/data';
import { getZoomedTimeRange, getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getZoomedTimeRange, getShiftedTimeRange } from 'app/core/utils/timePicker';
export class TimeSrv { export class TimeSrv {
@ -32,7 +37,7 @@ export class TimeSrv {
private contextSrv: ContextSrv private contextSrv: ContextSrv
) { ) {
// default time // default time
this.time = { from: '6h', to: 'now' }; this.time = DefaultTimeRange.raw;
$rootScope.$on('zoom-out', this.zoomOut.bind(this)); $rootScope.$on('zoom-out', this.zoomOut.bind(this));
$rootScope.$on('shift-time', this.shiftTime.bind(this)); $rootScope.$on('shift-time', this.shiftTime.bind(this));

View File

@ -2,6 +2,7 @@ import { DataFrame, LoadingState, dateTime } from '@grafana/data';
import { PanelData, DataSourceApi, DataQueryRequest, DataQueryResponse } from '@grafana/ui'; import { PanelData, DataSourceApi, DataQueryRequest, DataQueryResponse } from '@grafana/ui';
import { Subscriber, Observable, Subscription } from 'rxjs'; import { Subscriber, Observable, Subscription } from 'rxjs';
import { runRequest } from './runRequest'; import { runRequest } from './runRequest';
import { deepFreeze } from '../../../../test/core/redux/reducerTester';
jest.mock('app/core/services/backend_srv'); jest.mock('app/core/services/backend_srv');
@ -186,19 +187,56 @@ describe('runRequest', () => {
runRequestScenario('If time range is relative', ctx => { runRequestScenario('If time range is relative', ctx => {
ctx.setup(async () => { ctx.setup(async () => {
// any changes to ctx.request.range will throw and state would become LoadingState.Error
deepFreeze(ctx.request.range);
ctx.start(); ctx.start();
// wait a bit // wait a bit
await sleep(20); await sleep(20);
ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] }); ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] });
}); });
it('should update returned request range', () => { it('should add the correct timeRange property and the request range should not be mutated', () => {
expect(ctx.results[0].request.range.to.valueOf()).not.toBe(ctx.fromStartTime); 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) { async function sleep(ms: number) {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(resolve, ms); setTimeout(resolve, ms);

View File

@ -34,14 +34,14 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
packets[packet.key || 'A'] = packet; packets[packet.key || 'A'] = packet;
// Update the time range // Update the time range
let timeRange = request.range; const range = { ...request.range };
if (isString(timeRange.raw.from)) { const timeRange = isString(range.raw.from)
timeRange = { ? {
from: dateMath.parse(timeRange.raw.from, false), from: dateMath.parse(range.raw.from, false),
to: dateMath.parse(timeRange.raw.to, true), to: dateMath.parse(range.raw.to, true),
raw: timeRange.raw, raw: range.raw,
};
} }
: range;
const combinedData = flatten( const combinedData = flatten(
lodashMap(packets, (packet: DataQueryResponse) => { lodashMap(packets, (packet: DataQueryResponse) => {
@ -52,10 +52,8 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
const panelData = { const panelData = {
state: packet.state || LoadingState.Done, state: packet.state || LoadingState.Done,
series: combinedData, series: combinedData,
request: { request,
...request, timeRange,
range: timeRange,
},
}; };
return { packets, panelData }; return { packets, panelData };
@ -75,6 +73,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
state: LoadingState.Loading, state: LoadingState.Loading,
series: [], series: [],
request: request, request: request,
timeRange: request.range,
}, },
packets: {}, packets: {},
}; };
@ -96,6 +95,7 @@ export function runRequest(datasource: DataSourceApi, request: DataQueryRequest)
request.endTime = Date.now(); request.endTime = Date.now();
state = processResponsePacket(packet, state); state = processResponsePacket(packet, state);
return state.panelData; return state.panelData;
}), }),
// handle errors // handle errors

View File

@ -1,19 +1,19 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { LoadingState } from '@grafana/data'; import { LoadingState, TimeRange } from '@grafana/data';
import { PanelData } from '@grafana/ui'; import { PanelData } from '@grafana/ui';
import QueryStatus from './QueryStatus'; import QueryStatus from './QueryStatus';
describe('<QueryStatus />', () => { describe('<QueryStatus />', () => {
it('should render with a latency', () => { 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(<QueryStatus latency={0} queryResponse={res} />); const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />);
expect(wrapper.find('div').exists()).toBeTruthy(); expect(wrapper.find('div').exists()).toBeTruthy();
}); });
it('should not render when query has not started', () => { 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(<QueryStatus latency={0} queryResponse={res} />); const wrapper = shallow(<QueryStatus latency={0} queryResponse={res} />);
expect(wrapper.getElement()).toBe(null); expect(wrapper.getElement()).toBe(null);
}); });

View File

@ -42,7 +42,7 @@ describe('Explore item reducer', () => {
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState) .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ .thenStateShouldEqual({
...makeExploreItemState(), ...initalState,
scanning: true, scanning: true,
}); });
}); });
@ -57,7 +57,7 @@ describe('Explore item reducer', () => {
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState) .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left })) .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({ .thenStateShouldEqual({
...makeExploreItemState(), ...initalState,
scanning: false, scanning: false,
scanRange: undefined, scanRange: undefined,
}); });

View File

@ -10,7 +10,7 @@ import {
refreshIntervalToSortOrder, refreshIntervalToSortOrder,
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, ExploreId, ExploreUpdateState, ExploreMode } from 'app/types/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 { DataQuery, DataSourceApi, PanelData, DataQueryRequest } from '@grafana/ui';
import { import {
HigherOrderAction, HigherOrderAction,
@ -121,6 +121,7 @@ export const createEmptyQueryResponse = (): PanelData => ({
request: {} as DataQueryRequest<DataQuery>, request: {} as DataQueryRequest<DataQuery>,
series: [], series: [],
error: null, error: null,
timeRange: DefaultTimeRange,
}); });
/** /**

View File

@ -131,15 +131,16 @@ class MetricsPanelCtrl extends PanelCtrl {
} }
if (data.request) { if (data.request) {
const { range, timeInfo } = data.request; const { timeInfo } = data.request;
if (range) {
this.range = range;
}
if (timeInfo) { if (timeInfo) {
this.timeInfo = timeInfo; this.timeInfo = timeInfo;
} }
} }
if (data.timeRange) {
this.range = data.timeRange;
}
if (this.useDataFrames) { if (this.useDataFrames) {
this.handleDataFrames(data.series); this.handleDataFrames(data.series);
} else { } else {

View File

@ -1,10 +1,9 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DataQuery, PanelData, DataSourceApi } from '@grafana/ui'; import { DataQuery, PanelData, DataSourceApi } from '@grafana/ui';
import { QueryRunnerOptions } from 'app/features/dashboard/state/PanelQueryRunner'; 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 { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { LoadingState } from '@grafana/data'; import { LoadingState, DefaultTimeRange } from '@grafana/data';
import { SHARED_DASHBODARD_QUERY } from './types';
export function isSharedDashboardQuery(datasource: string | DataSourceApi) { export function isSharedDashboardQuery(datasource: string | DataSourceApi) {
if (!datasource) { if (!datasource) {
@ -76,5 +75,6 @@ function getQueryError(msg: string): PanelData {
state: LoadingState.Error, state: LoadingState.Error,
series: [], series: [],
error: { message: msg }, error: { message: msg },
timeRange: DefaultTimeRange,
}; };
} }

View File

@ -18,7 +18,7 @@ interface ObjectType extends Object {
[key: string]: any; [key: string]: any;
} }
const deepFreeze = <T>(obj: T): T => { export const deepFreeze = <T>(obj: T): T => {
Object.freeze(obj); Object.freeze(obj);
const isNotException = (object: any, propertyName: any) => const isNotException = (object: any, propertyName: any) =>