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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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
@@ -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() });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user