Skip to content

Commit

Permalink
demo: alternative time range selection on timeseries panel
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenjaneczek committed May 23, 2024
1 parent 07c5dc6 commit d9d3c3c
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/scenes-app/src/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { getInteractiveTableDemo } from './interactiveTableDemo';
import { getVariableRepeaterDemo } from './variableRepeater';
import { getQueryControllerDemo } from './queryController';
import { getDynamicDataLayersDemo } from './dynamicDataLayers';
import { getPanelTimeRangeHandlerDemoScene } from './panelTimeRangeHandler';

export interface DemoDescriptor {
title: string;
Expand All @@ -47,6 +48,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 },
Expand Down
245 changes: 245 additions & 0 deletions packages/scenes-app/src/demos/panelTimeRangeHandler.tsx
Original file line number Diff line number Diff line change
@@ -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<SceneDataState> = {
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<TimeRangeChangeOverrideState> implements SceneTimeRangeLike {
constructor(state: Omit<TimeRangeChangeOverrideState, 'from' | 'to' | 'value' | 'timeZone' | 'alternateTimeRange'> = {}) {
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;
}
2 changes: 1 addition & 1 deletion packages/scenes/src/components/SceneRefreshPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export function SceneRefreshPickerRenderer({ model }: SceneComponentProps<SceneR
const { refresh, intervals, autoEnabled, autoValue, isOnCanvas, primary, withText } = model.useState();
const isRunning = useQueryControllerState(model);

let text = refresh === RefreshPicker.autoOption.value ? autoValue : withText ? 'Refresh' : undefined;
let text = refresh === RefreshPicker.autoOption?.value ? autoValue : withText ? 'Refresh' : undefined;
let tooltip: string | undefined;
let width: string | undefined;

Expand Down

0 comments on commit d9d3c3c

Please sign in to comment.