Dashboards: Add feature toggle to use dashboard level time macros in scenes (#108670)

* Add feature toggle to use dashboard level time macros in scenes

* Add test

* remove unregister time macro as it was removed from scenes
This commit is contained in:
Oscar Kilhed
2025-07-31 11:49:07 +02:00
committed by GitHub
parent d71924ea7b
commit e7cfe0c023
9 changed files with 143 additions and 3 deletions
@@ -1074,6 +1074,10 @@ export interface FeatureToggles {
*/
dashboardDsAdHocFiltering?: boolean;
/**
* Supports __from and __to macros that always use the dashboard level time range
*/
dashboardLevelTimeMacros?: boolean;
/**
* Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.
* @default false
*/
-2
View File
@@ -90,7 +90,6 @@ func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFol
return r0, r1
}
// DeleteDashboard provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error {
ret := _m.Called(ctx, cmd)
@@ -127,7 +126,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, req
return r0
}
// FindDashboards provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
ret := _m.Called(ctx, query)
+7
View File
@@ -1857,6 +1857,13 @@ var (
Owner: grafanaDataProSquad,
FrontendOnly: true,
},
{
Name: "dashboardLevelTimeMacros",
Description: "Supports __from and __to macros that always use the dashboard level time range",
Stage: FeatureStageExperimental,
Owner: grafanaDashboardsSquad,
FrontendOnly: true,
},
{
Name: "alertmanagerRemoteSecondaryWithRemoteState",
Description: "Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.",
+1
View File
@@ -240,5 +240,6 @@ alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,fal
pluginAssetProvider,experimental,@grafana/plugins-platform-backend,false,true,false
unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false
dashboardDsAdHocFiltering,experimental,@grafana/datapro,false,false,true
dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true
alertmanagerRemoteSecondaryWithRemoteState,experimental,@grafana/alerting-squad,false,false,false
adhocFiltersInTooltips,experimental,@grafana/datapro,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
240 pluginAssetProvider experimental @grafana/plugins-platform-backend false true false
241 unifiedStorageSearchDualReaderEnabled experimental @grafana/search-and-storage false false false
242 dashboardDsAdHocFiltering experimental @grafana/datapro false false true
243 dashboardLevelTimeMacros experimental @grafana/dashboards-squad false false true
244 alertmanagerRemoteSecondaryWithRemoteState experimental @grafana/alerting-squad false false false
245 adhocFiltersInTooltips experimental @grafana/datapro false false true
+4
View File
@@ -971,6 +971,10 @@ const (
// Enables adhoc filtering support for the dashboard datasource
FlagDashboardDsAdHocFiltering = "dashboardDsAdHocFiltering"
// FlagDashboardLevelTimeMacros
// Supports __from and __to macros that always use the dashboard level time range
FlagDashboardLevelTimeMacros = "dashboardLevelTimeMacros"
// FlagAlertmanagerRemoteSecondaryWithRemoteState
// Starts Grafana in remote secondary mode pulling the latest state from the remote Alertmanager to avoid duplicate notifications.
FlagAlertmanagerRemoteSecondaryWithRemoteState = "alertmanagerRemoteSecondaryWithRemoteState"
+13
View File
@@ -836,6 +836,19 @@
"frontend": true
}
},
{
"metadata": {
"name": "dashboardLevelTimeMacros",
"resourceVersion": "1753435849295",
"creationTimestamp": "2025-07-25T09:30:49Z"
},
"spec": {
"description": "Supports __from and __to macros that always use the dashboard level time range",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "dashboardNewLayouts",
+7 -1
View File
@@ -50,7 +50,7 @@ import {
setPanelRenderer,
setPluginPage,
} from '@grafana/runtime/internal';
import { loadResources as loadScenesResources } from '@grafana/scenes';
import { loadResources as loadScenesResources, sceneUtils } from '@grafana/scenes';
import config, { updateConfig } from 'app/core/config';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
@@ -82,6 +82,7 @@ import { initAlerting } from './features/alerting/unified/initAlerting';
import { initAuthConfig } from './features/auth-config';
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { EmbeddedDashboardLazy } from './features/dashboard-scene/embedding/EmbeddedDashboardLazy';
import { DashboardLevelTimeMacro } from './features/dashboard-scene/scene/DashboardLevelTimeMacro';
import { initGrafanaLive } from './features/live';
import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView';
import { PanelRenderer } from './features/panel/components/PanelRenderer';
@@ -284,6 +285,11 @@ export class GrafanaApp {
initializeCrashDetection();
}
if (config.featureToggles.dashboardLevelTimeMacros) {
sceneUtils.registerVariableMacro('__from', DashboardLevelTimeMacro, true);
sceneUtils.registerVariableMacro('__to', DashboardLevelTimeMacro, true);
}
const root = createRoot(document.getElementById('reactRoot')!);
root.render(
createElement(AppWrapper, {
@@ -0,0 +1,74 @@
import { getPanelPlugin } from '@grafana/data/test';
import { setPluginImportUtils } from '@grafana/runtime';
import { SceneTimeRange, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardLevelTimeMacro } from './DashboardLevelTimeMacro';
import { DashboardScene } from './DashboardScene';
import { PanelTimeRange } from './PanelTimeRange';
import { AutoGridItem } from './layout-auto-grid/AutoGridItem';
import { AutoGridLayout } from './layout-auto-grid/AutoGridLayout';
import {
AutoGridLayoutManager,
getAutoRowsTemplate,
getTemplateColumnsTemplate,
} from './layout-auto-grid/AutoGridLayoutManager';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
}));
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
describe('dashboardLevelTimeMacros', () => {
it('Can use use $__from and $__to', async () => {
const panel = new VizPanel({
$timeRange: new PanelTimeRange({ timeShift: '1h' }),
title: 'Test Panel',
key: 'panel-1',
pluginId: 'timeseries',
});
const scene = new DashboardScene({
$timeRange: new SceneTimeRange({ from: '2023-05-23T06:09:57.073Z', to: '2023-05-23T07:09:57.073Z' }),
body: new AutoGridLayoutManager({
maxColumnCount: 12,
columnWidth: 100,
rowHeight: 100,
fillScreen: true,
layout: new AutoGridLayout({
isDraggable: true,
templateColumns: getTemplateColumnsTemplate(12, 100),
autoRows: getAutoRowsTemplate(100, true),
children: [
new AutoGridItem({
body: panel,
}),
],
}),
}),
});
activateFullSceneTree(scene);
// Wait for the scene to be activated
await new Promise((resolve) => setTimeout(resolve, 100));
expect(sceneGraph.interpolate(scene, '$__from')).toBe('1684822197073'); // Dashboard level time range
expect(sceneGraph.interpolate(scene, '$__to')).toBe('1684825797073'); // Dashboard level time range
expect(sceneGraph.interpolate(panel, '$__from')).toBe('1684818597073'); // Time shifted by 1h
expect(sceneGraph.interpolate(panel, '$__to')).toBe('1684822197073'); // Time shifted by 1h
sceneUtils.registerVariableMacro('__from', DashboardLevelTimeMacro, true);
sceneUtils.registerVariableMacro('__to', DashboardLevelTimeMacro, true);
expect(sceneGraph.interpolate(panel, '$__from')).toBe('1684822197073'); // Dashboard level time range even when panel is time shifted
expect(sceneGraph.interpolate(panel, '$__to')).toBe('1684825797073'); // Dashboard level time range even when panel is time shifted
});
});
@@ -0,0 +1,33 @@
import { dateTimeFormat } from '@grafana/data';
import { FormatVariable, sceneGraph, SceneObject } from '@grafana/scenes';
/**
* This macro is used to support the old __to and __from macros that always used the dashboard level time range.
**/
export class DashboardLevelTimeMacro implements FormatVariable {
public state: { name: string; type: string };
private _sceneObject: SceneObject;
public constructor(name: string, sceneObject: SceneObject) {
this.state = { name: name, type: 'time_macro' };
this._sceneObject = sceneObject.getRoot();
}
public getValue() {
const timeRange = sceneGraph.getTimeRange(this._sceneObject);
if (this.state.name === '__from') {
return timeRange.state.value.from.valueOf();
} else {
return timeRange.state.value.to.valueOf();
}
}
public getValueText?(): string {
const timeRange = sceneGraph.getTimeRange(this._sceneObject);
if (this.state.name === '__from') {
return dateTimeFormat(timeRange.state.value.from, { timeZone: timeRange.getTimeZone() });
} else {
return dateTimeFormat(timeRange.state.value.to, { timeZone: timeRange.getTimeZone() });
}
}
}