From b1884808db414ec9c12f01e5314063f5e5bf1cb6 Mon Sep 17 00:00:00 2001 From: Darren Janeczek Date: Thu, 23 May 2024 11:38:56 -0400 Subject: [PATCH 1/3] demo: alternative time range selection on timeseries panel --- packages/scenes-app/src/demos/index.ts | 2 + .../src/demos/panelTimeRangeHandler.tsx | 245 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 packages/scenes-app/src/demos/panelTimeRangeHandler.tsx diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index e1adfb5b4..8811b1ab6 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -39,6 +39,7 @@ import { getInteropDemo } from './interopDemo'; import { getUrlSyncTest } from './urlSyncTest'; import { getMlDemo } from './ml'; import { getSceneGraphEventsDemo } from './sceneGraphEvents'; +import { getPanelTimeRangeHandlerDemoScene } from './panelTimeRangeHandler'; export interface DemoDescriptor { title: string; @@ -51,6 +52,7 @@ export function getDemos(): DemoDescriptor[] { { title: 'Responsive layout', getPage: getResponsiveLayoutDemo }, { title: 'Panel menu', getPage: getPanelMenuTest }, { title: 'Panel context', getPage: getPanelContextDemoScene }, + { title: 'Panel alternative time range selection', getPage: getPanelTimeRangeHandlerDemoScene }, { title: 'Repeat layout by series', getPage: getPanelRepeaterTest }, { title: 'Repeat layout by variable', getPage: getVariableRepeaterDemo }, { title: 'Grid layout', getPage: getGridLayoutTest }, diff --git a/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx new file mode 100644 index 000000000..63195b113 --- /dev/null +++ b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx @@ -0,0 +1,245 @@ +import { + SceneFlexLayout, + SceneFlexItem, + SceneQueryRunner, + SceneAppPage, + EmbeddedScene, + SceneAppPageState, + PanelBuilders, + VizPanel, + SceneObjectBase, + SceneTimeRangeLike, + sceneGraph, + SceneTimeRangeState, + SceneDataState, + SceneTimeRange, +} from '@grafana/scenes'; +import { DATASOURCE_REF } from '../constants'; +import { getEmbeddedSceneDefaults } from './utils'; +import { FieldType, MutableDataFrame, TimeRange, getDefaultTimeRange } from '@grafana/data'; + +export function getPanelTimeRangeHandlerDemoScene(defaults: SceneAppPageState): SceneAppPage { + return new SceneAppPage({ + ...defaults, + subTitle: 'This timeseries panel has the ability to select an alternative time range. ', + getScene: () => { + return new EmbeddedScene({ + ...getEmbeddedSceneDefaults(), + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexItem({ + height: 400, + body: PanelBuilders.timeseries() + .setData(getQueryRunnerFor3SeriesWithLabels()) + .setTitle('Change selection by mouse-dragging a time range') + .setDisplayName('${__field.labels.cluster}') + .setBehaviors( + [ + (vizPanel: VizPanel) => { + patchPanelContext(vizPanel); + const altTimeRangeScene = sceneGraph.findObject(vizPanel, (scene) => { + return scene.state.key === 'altTimeRangeScene'; + }); + + function representTimeRangeSelection(selection: TimeRange) { + const data = vizPanel.state.$data?.state.data; + if (!data) { + return; + } + + + const annotation = new RangeAnnotation(); + annotation.addRange( + { + time: selection.from.unix() * 1000, + timeEnd: selection.to.unix() * 1000, + color: 'magenta', + text: "Alternate time range selection" + } + ); + + const newState: Partial = { + data: { + ...data, + annotations: [annotation] + } + } + + vizPanel.state.$data?.setState(newState) + altTimeRangeScene?.state.$timeRange?.onTimeRangeChange(selection); + } + + // Override time range update behavior + vizPanel.setState({ + $timeRange: new TimeRangeChangeOverride({ + onTimeRangeChange(timeRange) { + representTimeRangeSelection(timeRange); + }, + }) + }); + + + // Restore time range selection if random walk is reset + vizPanel.state.$data?.subscribeToState((newState, oldState) => { + if (!newState.data) { + return; + } + if (!newState.data?.annotations?.length && !oldState.data?.annotations?.length) { + // Make new annotations, for the first time + if (altTimeRangeScene) { + const timeRange = altTimeRangeScene?.state.$timeRange?.state.value; + timeRange && representTimeRangeSelection(timeRange); + } + } + else if (!newState.data?.annotations?.length && oldState.data?.annotations?.length) { + // We can just ensure we retain the old annotations if they exist + newState.data.annotations = oldState.data.annotations; + } + }); + + } + ]) + .build(), + }), + new SceneFlexItem({ + height: 30, + body: PanelBuilders.text().setOption('content', '').setTitle('Root time range from: ${__from:date:iso} to: ${__to:date:iso}').build(), + }), + new SceneFlexItem({ + key: "altTimeRangeScene", + $timeRange: new SceneTimeRange(), + height: 30, + body: PanelBuilders.text().setOption('content', '').setTitle('Alternative (magenta) time range from: ${__from:date:iso} to: ${__to:date:iso}').build(), + }), + + ], + }), + }); + }, + }); +} + +interface TimeRangeChangeOverrideState extends SceneTimeRangeState { + alternateTimeRange: TimeRange + onTimeRangeChange?: (timeRange: TimeRange) => void +} + +class TimeRangeChangeOverride extends SceneObjectBase implements SceneTimeRangeLike { + constructor(state: Omit = {}) { + super({ + ...state, + // We set a default time range here. It will be overwritten on activation based on ancestor time range. + from: 'now-6h', + to: 'now', + value: getDefaultTimeRange(), + alternateTimeRange: getDefaultTimeRange(), + } + ); + + this.addActivationHandler(() => { + const timeRange = this.realTimeRange; + + this.setState({ ...timeRange.state, alternateTimeRange: timeRange.state.value }); + + this._subs.add( + timeRange.subscribeToState( + (newState) => this.setState(newState) + ) + ) + }); + } + + private get realTimeRange() { + const scene = this.parent; + if (!scene?.parent) { + throw Error("A time range change override will not function if it is on a scene with no parent."); + } + const timeRange = sceneGraph.getTimeRange(scene?.parent); + return timeRange; + } + + onTimeRangeChange(timeRange: TimeRange): void { + this.setState({ alternateTimeRange: timeRange }); + this.state.onTimeRangeChange?.(timeRange); + } + + onTimeZoneChange(timeZone: string): void { + this.realTimeRange.onTimeZoneChange(timeZone); + } + + getTimeZone(): string { + return this.realTimeRange.getTimeZone(); + } + + onRefresh(): void { + this.realTimeRange.onRefresh(); + } +} + +export function getQueryRunnerFor3SeriesWithLabels() { + return new SceneQueryRunner({ + datasource: DATASOURCE_REF, + queries: [ + { + labels: 'cluster=eu', + refId: 'A', + scenarioId: 'random_walk', + seriesCount: 1, + }, + { + hide: false, + labels: 'cluster=us', + refId: 'B', + scenarioId: 'random_walk', + seriesCount: 1, + }, + { + hide: false, + labels: 'cluster=asia', + refId: 'C', + scenarioId: 'random_walk', + seriesCount: 1, + }, + ], + }); +} + +class RangeAnnotation extends MutableDataFrame { + constructor() { + super(); + this.addField({ + name: 'time', + type: FieldType.time, + }); + this.addField({ + name: 'timeEnd', + type: FieldType.time, + }); + this.addField({ + name: 'isRegion', + type: FieldType.boolean, + }); + this.addField({ + name: 'color', + type: FieldType.other, + }); + this.addField({ + name: 'text', + type: FieldType.string, + }); + } + addRange(entry: { time: number; timeEnd: number; color?: string; text: string }) { + this.add({ ...entry, isRegion: true }); + } +} + +function patchPanelContext(vizPanel: VizPanel) { + // Avoid undefined errors by providing placeholder functions. + // This is required for version of Grafana prior to viz tooltip enhancements (~10.3) + // Until 10.3 & 10.4 still require `newVizTooltips` feature flag. + const panelContext = vizPanel.getPanelContext(); + const nope = () => false; + panelContext.canEditAnnotations = nope; + panelContext.canDeleteAnnotations = nope; +} \ No newline at end of file From 745e090b7c905b2bdbd492b5375cbdea9587f024 Mon Sep 17 00:00:00 2001 From: Darren Janeczek Date: Thu, 23 May 2024 13:34:31 -0400 Subject: [PATCH 2/3] fix: new line --- packages/scenes-app/src/demos/panelTimeRangeHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx index 63195b113..9470ade53 100644 --- a/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx +++ b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx @@ -242,4 +242,4 @@ function patchPanelContext(vizPanel: VizPanel) { const nope = () => false; panelContext.canEditAnnotations = nope; panelContext.canDeleteAnnotations = nope; -} \ No newline at end of file +} From cfb93079c87a451c0407ccc80bf4d48e3dddeb15 Mon Sep 17 00:00:00 2001 From: Darren Janeczek Date: Mon, 3 Jun 2024 09:27:40 -0400 Subject: [PATCH 3/3] fix: use new util method --- packages/scenes-app/src/demos/panelTimeRangeHandler.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx index 9470ade53..1a32fd89f 100644 --- a/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx +++ b/packages/scenes-app/src/demos/panelTimeRangeHandler.tsx @@ -38,9 +38,7 @@ export function getPanelTimeRangeHandlerDemoScene(defaults: SceneAppPageState): [ (vizPanel: VizPanel) => { patchPanelContext(vizPanel); - const altTimeRangeScene = sceneGraph.findObject(vizPanel, (scene) => { - return scene.state.key === 'altTimeRangeScene'; - }); + const altTimeRangeScene = sceneGraph.findByKey(vizPanel, 'altTimeRangeScene'); function representTimeRangeSelection(selection: TimeRange) { const data = vizPanel.state.$data?.state.data; @@ -67,7 +65,7 @@ export function getPanelTimeRangeHandlerDemoScene(defaults: SceneAppPageState): } vizPanel.state.$data?.setState(newState) - altTimeRangeScene?.state.$timeRange?.onTimeRangeChange(selection); + altTimeRangeScene.state.$timeRange?.onTimeRangeChange(selection); } // Override time range update behavior