Commit 889f8e31 by Hugo Häggmark Committed by Torkel Ödegaard

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
parent e35de167
......@@ -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' },
};
......@@ -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<T = any> {
......
......@@ -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<Props, State> {
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: DefaultTimeRange,
},
};
}
......@@ -66,6 +67,7 @@ export class PanelChrome extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
<PanelComponent
id={panel.id}
data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
timeRange={timeRange}
timeZone={this.props.dashboard.getTimezone()}
options={panel.getOptions()}
transparent={panel.transparent}
......
......@@ -2,25 +2,29 @@
import React, { PureComponent } from 'react';
import _ from 'lodash';
import { css } from 'emotion';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryInspector } from './QueryInspector';
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';
// Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getBackendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
// Types
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { DataQuery, DataSourceSelectItem, PanelData, AlphaNotice, PluginState } from '@grafana/ui';
import { LoadingState, DataTransformerConfig } from '@grafana/data';
import { LoadingState, DataTransformerConfig, DefaultTimeRange } from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { Unsubscribable } from 'rxjs';
import { isSharedDashboardQuery, DashboardQueryEditor } from 'app/plugins/datasource/dashboard';
......@@ -55,6 +59,7 @@ export class QueriesTab extends PureComponent<Props, State> {
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: DefaultTimeRange,
},
};
......
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', () => {
......
......@@ -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';
......@@ -320,10 +318,13 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat
state = LoadingState.Error;
}
const timeRange = data.timeRange;
return {
state,
series,
request,
error,
timeRange,
};
}
// 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<Props, State> {
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: DefaultTimeRange,
},
};
}
......
// 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));
......
......@@ -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 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 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).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);
......
......@@ -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
......
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('<QueryStatus />', () => {
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} />);
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(<QueryStatus latency={0} queryResponse={res} />);
expect(wrapper.getElement()).toBe(null);
});
......
......@@ -42,7 +42,7 @@ describe('Explore item reducer', () => {
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({
...makeExploreItemState(),
...initalState,
scanning: true,
});
});
......@@ -57,7 +57,7 @@ describe('Explore item reducer', () => {
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({
...makeExploreItemState(),
...initalState,
scanning: false,
scanRange: undefined,
});
......
......@@ -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<DataQuery>,
series: [],
error: null,
timeRange: DefaultTimeRange,
});
/**
......
......@@ -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 {
......
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,
};
}
......@@ -18,7 +18,7 @@ interface ObjectType extends Object {
[key: string]: any;
}
const deepFreeze = <T>(obj: T): T => {
export const deepFreeze = <T>(obj: T): T => {
Object.freeze(obj);
const isNotException = (object: any, propertyName: any) =>
......
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