mirror of https://github.com/grafana/grafana
DashboardScene: Support panel relative time overrides and timeshift (#62844)
* Things are working * update * Added unit tests * More tests * minor fix * Update * Support hideTimeOverridepull/74266/head
parent
8113707dc8
commit
f4c127a1d8
@ -0,0 +1,59 @@ |
||||
import { advanceTo, clear } from 'jest-date-mock'; |
||||
|
||||
import { dateTime } from '@grafana/data'; |
||||
import { SceneCanvasText, SceneFlexItem, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes'; |
||||
|
||||
import { activateFullSceneTree } from '../utils/utils'; |
||||
|
||||
import { PanelTimeRange } from './PanelTimeRange'; |
||||
|
||||
describe('PanelTimeRange', () => { |
||||
const fakeCurrentDate = dateTime('2019-02-11T19:00:00.000Z').toDate(); |
||||
|
||||
beforeAll(() => { |
||||
advanceTo(fakeCurrentDate); |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
clear(); |
||||
}); |
||||
|
||||
it('should apply relative time override', () => { |
||||
const panelTime = new PanelTimeRange({ timeFrom: '2h' }); |
||||
|
||||
buildAndActivateSceneFor(panelTime); |
||||
|
||||
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T17:00:00.000Z'); |
||||
expect(panelTime.state.value.to.toISOString()).toBe(fakeCurrentDate.toISOString()); |
||||
expect(panelTime.state.value.raw.from).toBe('now-2h'); |
||||
expect(panelTime.state.timeInfo).toBe('Last 2 hours'); |
||||
}); |
||||
|
||||
it('should apply time shift', () => { |
||||
const panelTime = new PanelTimeRange({ timeShift: '2h' }); |
||||
|
||||
buildAndActivateSceneFor(panelTime); |
||||
|
||||
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T11:00:00.000Z'); |
||||
expect(panelTime.state.value.to.toISOString()).toBe('2019-02-11T17:00:00.000Z'); |
||||
expect(panelTime.state.timeInfo).toBe(' timeshift -2h'); |
||||
}); |
||||
|
||||
it('should apply both relative time and time shift', () => { |
||||
const panelTime = new PanelTimeRange({ timeFrom: '2h', timeShift: '2h' }); |
||||
|
||||
buildAndActivateSceneFor(panelTime); |
||||
|
||||
expect(panelTime.state.value.from.toISOString()).toBe('2019-02-11T15:00:00.000Z'); |
||||
expect(panelTime.state.timeInfo).toBe('Last 2 hours timeshift -2h'); |
||||
}); |
||||
}); |
||||
|
||||
function buildAndActivateSceneFor(panelTime: PanelTimeRange) { |
||||
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime }); |
||||
const scene = new SceneFlexLayout({ |
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), |
||||
children: [new SceneFlexItem({ body: panel })], |
||||
}); |
||||
activateFullSceneTree(scene); |
||||
} |
@ -0,0 +1,145 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React from 'react'; |
||||
|
||||
import { dateMath, getDefaultTimeRange, GrafanaTheme2, rangeUtil, TimeRange } from '@grafana/data'; |
||||
import { |
||||
SceneComponentProps, |
||||
sceneGraph, |
||||
SceneObjectBase, |
||||
SceneTimeRangeLike, |
||||
SceneTimeRangeState, |
||||
} from '@grafana/scenes'; |
||||
import { Icon, PanelChrome, TimePickerTooltip, Tooltip, useStyles2 } from '@grafana/ui'; |
||||
import { TimeOverrideResult } from 'app/features/dashboard/utils/panel'; |
||||
|
||||
interface PanelTimeRangeState extends SceneTimeRangeState { |
||||
timeFrom?: string; |
||||
timeShift?: string; |
||||
hideTimeOverride?: boolean; |
||||
timeInfo?: string; |
||||
} |
||||
|
||||
export class PanelTimeRange extends SceneObjectBase<PanelTimeRangeState> implements SceneTimeRangeLike { |
||||
public static Component = PanelTimeRangeRenderer; |
||||
|
||||
public constructor(state: Partial<PanelTimeRangeState> = {}) { |
||||
super({ |
||||
...state, |
||||
// This time range is not valid until activation
|
||||
from: 'now-6h', |
||||
to: 'now', |
||||
value: getDefaultTimeRange(), |
||||
}); |
||||
|
||||
this.addActivationHandler(() => this._activationHandler()); |
||||
} |
||||
|
||||
private getTimeOverride(parentTimeRange: TimeRange): TimeOverrideResult { |
||||
const { timeFrom, timeShift } = this.state; |
||||
const newTimeData = { timeInfo: '', timeRange: parentTimeRange }; |
||||
|
||||
if (timeFrom) { |
||||
const timeFromInterpolated = sceneGraph.interpolate(this, this.state.timeFrom); |
||||
const timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated); |
||||
|
||||
if (timeFromInfo.invalid) { |
||||
newTimeData.timeInfo = 'invalid time override'; |
||||
return newTimeData; |
||||
} |
||||
|
||||
// Only evaluate if the timeFrom if parent time is relative
|
||||
if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) { |
||||
newTimeData.timeInfo = timeFromInfo.display; |
||||
newTimeData.timeRange = { |
||||
from: dateMath.parse(timeFromInfo.from)!, |
||||
to: dateMath.parse(timeFromInfo.to)!, |
||||
raw: { from: timeFromInfo.from, to: timeFromInfo.to }, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
if (timeShift) { |
||||
const timeShiftInterpolated = sceneGraph.interpolate(this, this.state.timeShift); |
||||
const timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated); |
||||
|
||||
if (timeShiftInfo.invalid) { |
||||
newTimeData.timeInfo = 'invalid timeshift'; |
||||
return newTimeData; |
||||
} |
||||
|
||||
const timeShift = '-' + timeShiftInterpolated; |
||||
newTimeData.timeInfo += ' timeshift ' + timeShift; |
||||
const from = dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false)!; |
||||
const to = dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true)!; |
||||
|
||||
newTimeData.timeRange = { from, to, raw: { from, to } }; |
||||
} |
||||
|
||||
return newTimeData; |
||||
} |
||||
|
||||
private _activationHandler(): void { |
||||
const parentTimeRange = this.getParentTimeRange(); |
||||
|
||||
this._subs.add(parentTimeRange.subscribeToState((state) => this.handleParentTimeRangeChanged(state.value))); |
||||
|
||||
this.handleParentTimeRangeChanged(parentTimeRange.state.value); |
||||
} |
||||
|
||||
private handleParentTimeRangeChanged(parentTimeRange: TimeRange) { |
||||
const overrideResult = this.getTimeOverride(parentTimeRange); |
||||
this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo }); |
||||
} |
||||
|
||||
private getParentTimeRange(): SceneTimeRangeLike { |
||||
if (!this.parent || !this.parent.parent) { |
||||
throw new Error('Missing parent'); |
||||
} |
||||
|
||||
// Need to go up two levels otherwise we will get ourselves
|
||||
return sceneGraph.getTimeRange(this.parent.parent); |
||||
} |
||||
|
||||
public onTimeRangeChange = (timeRange: TimeRange) => { |
||||
const parentTimeRange = this.getParentTimeRange(); |
||||
parentTimeRange.onTimeRangeChange(timeRange); |
||||
}; |
||||
|
||||
public onRefresh(): void { |
||||
this.getParentTimeRange().onRefresh(); |
||||
} |
||||
|
||||
public onTimeZoneChange(timeZone: string): void { |
||||
this.getParentTimeRange().onTimeZoneChange(timeZone); |
||||
} |
||||
|
||||
public getTimeZone(): string { |
||||
return this.getParentTimeRange().getTimeZone(); |
||||
} |
||||
} |
||||
|
||||
function PanelTimeRangeRenderer({ model }: SceneComponentProps<PanelTimeRange>) { |
||||
const { timeInfo, hideTimeOverride } = model.useState(); |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
if (!timeInfo || hideTimeOverride) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<Tooltip content={<TimePickerTooltip timeRange={model.state.value} timeZone={model.getTimeZone()} />}> |
||||
<PanelChrome.TitleItem className={styles.timeshift}> |
||||
<Icon name="clock-nine" size="sm" /> {timeInfo} |
||||
</PanelChrome.TitleItem> |
||||
</Tooltip> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
timeshift: css({ |
||||
color: theme.colors.text.link, |
||||
gap: theme.spacing(0.5), |
||||
}), |
||||
}; |
||||
}; |
@ -1,14 +1,44 @@ |
||||
import { SceneGridItem } from '@grafana/scenes'; |
||||
import { Panel } from '@grafana/schema'; |
||||
import { PanelModel } from 'app/features/dashboard/state'; |
||||
|
||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json'; |
||||
import { transformSaveModelToScene } from './transformSaveModelToScene'; |
||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel'; |
||||
import { buildSceneFromPanelModel, transformSaveModelToScene } from './transformSaveModelToScene'; |
||||
import { gridItemToPanel, transformSceneToSaveModel } from './transformSceneToSaveModel'; |
||||
|
||||
describe('transformSceneToSaveModel', () => { |
||||
describe('Given a scene', () => { |
||||
it('Should transfrom back to peristed model', () => { |
||||
it('Should transform back to peristed model', () => { |
||||
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} }); |
||||
const saveModel = transformSceneToSaveModel(scene); |
||||
|
||||
expect(saveModel).toMatchSnapshot(); |
||||
}); |
||||
}); |
||||
|
||||
describe('Panel options', () => { |
||||
it('Given panel with time override', () => { |
||||
const gridItem = createVizPanelFromPanelSchema({ |
||||
timeFrom: '2h', |
||||
timeShift: '1d', |
||||
hideTimeOverride: true, |
||||
}); |
||||
|
||||
const saveModel = gridItemToPanel(gridItem); |
||||
expect(saveModel.timeFrom).toBe('2h'); |
||||
expect(saveModel.timeShift).toBe('1d'); |
||||
expect(saveModel.hideTimeOverride).toBe(true); |
||||
}); |
||||
|
||||
it('transparent panel', () => { |
||||
const gridItem = createVizPanelFromPanelSchema({ transparent: true }); |
||||
const saveModel = gridItemToPanel(gridItem); |
||||
|
||||
expect(saveModel.transparent).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
export function createVizPanelFromPanelSchema(panel: Partial<Panel>): SceneGridItem { |
||||
return buildSceneFromPanelModel(new PanelModel(panel)); |
||||
} |
||||
|
Loading…
Reference in new issue