TimeSeries: Add Time Comparison (#104672)
* TimeComparison: Add time comparison option * Display control conditionally based on option * Provide timeCompare option to panels that opt in * Only render time compare if panel supports it * Create a custom time range compare component * Move custom component * Remove button checked * Move time compare options out of field config * Fix types and remove from fieldConfigProperty enum * Remove from field config registry * Add headerActions to V2 schema * Remove dependency on timeCompare support registry * Add feature flag * Subscribe to options change in wrapper * Remove custom rendering to simplify wrapper * Bump scenes version * Hide checkbox * Remove unneeded query refresh during disable * Fix import order in timeseries module * Add util tests * Support time comparison toggle for new viz panels * Revert grafana-app-sdk and manifest changes * Revert yarn lock
This commit is contained in:
@@ -401,6 +401,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
canvasPanelPanZoom?: boolean;
|
||||
/**
|
||||
* Enables time comparison option in supported panels
|
||||
*/
|
||||
timeComparison?: boolean;
|
||||
/**
|
||||
* Enables infinite scrolling for the Logs panel in Explore and Dashboards
|
||||
* @default true
|
||||
*/
|
||||
|
||||
@@ -882,6 +882,16 @@ export type TimeZoneUtc = 'utc';
|
||||
*/
|
||||
export type TimeZoneBrowser = 'browser';
|
||||
|
||||
/**
|
||||
* Options for time comparison
|
||||
*/
|
||||
export interface TimeCompareOptions {
|
||||
/**
|
||||
* Enable time comparison control
|
||||
*/
|
||||
timeCompare?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional formats for the template variable replace functions
|
||||
* See also https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package common
|
||||
|
||||
// Options for time comparison
|
||||
TimeCompareOptions: {
|
||||
// Enable time comparison control
|
||||
timeCompare?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
+1
@@ -15,6 +15,7 @@ export const pluginVersion = "12.1.0-pre";
|
||||
export interface Options extends common.OptionsWithTimezones {
|
||||
legend: common.VizLegendOptions;
|
||||
orientation?: common.VizOrientation;
|
||||
timeCompare?: common.TimeCompareOptions;
|
||||
tooltip: common.VizTooltipOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './legend';
|
||||
export { addTooltipOptions } from './tooltip';
|
||||
export * from './text';
|
||||
export * from './stacking';
|
||||
export { addTimeCompareOption } from './timeCompare';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { TimeCompareOptions } from '@grafana/schema';
|
||||
|
||||
/**
|
||||
* Adds a generic time comparison option to the panel options editor.
|
||||
* Can be used by any panel that supports time comparison.
|
||||
*/
|
||||
export function addTimeCompareOption<T extends { timeCompare?: TimeCompareOptions }>(
|
||||
builder: PanelOptionsEditorBuilder<T>,
|
||||
defaultValue = false
|
||||
) {
|
||||
builder.addBooleanSwitch({
|
||||
path: 'timeCompare',
|
||||
name: 'Enable',
|
||||
category: ['Time Comparison'],
|
||||
description: '',
|
||||
defaultValue,
|
||||
});
|
||||
}
|
||||
@@ -667,6 +667,13 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "timeComparison",
|
||||
Description: "Enables time comparison option in supported panels",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "logsInfiniteScrolling",
|
||||
Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards",
|
||||
|
||||
@@ -88,6 +88,7 @@ dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true
|
||||
panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
|
||||
pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
timeComparison,experimental,@grafana/dataviz-squad,false,false,true
|
||||
logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
|
||||
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
|
||||
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
|
||||
|
@@ -363,6 +363,10 @@ const (
|
||||
// Allow pan and zoom in canvas panel
|
||||
FlagCanvasPanelPanZoom = "canvasPanelPanZoom"
|
||||
|
||||
// FlagTimeComparison
|
||||
// Enables time comparison option in supported panels
|
||||
FlagTimeComparison = "timeComparison"
|
||||
|
||||
// FlagLogsInfiniteScrolling
|
||||
// Enables infinite scrolling for the Logs panel in Explore and Dashboards
|
||||
FlagLogsInfiniteScrolling = "logsInfiniteScrolling"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
import { SceneTimeRangeCompare, SceneComponentProps, VizPanel, sceneGraph } from '@grafana/scenes';
|
||||
import { TimeCompareOptions } from '@grafana/schema';
|
||||
|
||||
function hasTimeCompare(options: unknown): options is TimeCompareOptions {
|
||||
return options != null && typeof options === 'object' && 'timeCompare' in options;
|
||||
}
|
||||
|
||||
export class CustomTimeRangeCompare extends SceneTimeRangeCompare {
|
||||
constructor(state: Partial<SceneTimeRangeCompare['state']> = {}) {
|
||||
super({
|
||||
...state,
|
||||
compareWith: undefined,
|
||||
compareOptions: [],
|
||||
hideCheckbox: true,
|
||||
});
|
||||
|
||||
this.addActivationHandler(() => this._activationHandler());
|
||||
}
|
||||
|
||||
private _activationHandler() {
|
||||
// Subscribe to parent panel's options changes
|
||||
const vizPanel = sceneGraph.getAncestor(this, VizPanel);
|
||||
|
||||
this._subs.add(
|
||||
vizPanel.subscribeToState((newState, prevState) => {
|
||||
const newTimeCompareEnabled = hasTimeCompare(newState.options) && newState.options.timeCompare;
|
||||
const prevTimeCompareEnabled = hasTimeCompare(prevState.options) && prevState.options.timeCompare;
|
||||
|
||||
// Only act when transitioning from enabled to disabled
|
||||
if (prevTimeCompareEnabled && !newTimeCompareEnabled) {
|
||||
this._handleDisable();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDisable() {
|
||||
// Only clear state if there's actually a comparison active
|
||||
if (this.state.compareWith) {
|
||||
this.setState({
|
||||
compareWith: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static Component = function CustomTimeRangeCompareRenderer({ model }: SceneComponentProps<SceneTimeRangeCompare>) {
|
||||
// Get the parent VizPanel to check timeCompare option
|
||||
const vizPanel = sceneGraph.getAncestor(model, VizPanel);
|
||||
const { options } = vizPanel.useState();
|
||||
|
||||
// Check if timeCompare is enabled
|
||||
const isTimeCompareEnabled = hasTimeCompare(options) && options.timeCompare;
|
||||
|
||||
if (!isTimeCompareEnabled) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <SceneTimeRangeCompare.Component model={model} />;
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
import { PanelQueryKind, PanelKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
|
||||
|
||||
import { getRuntimePanelDataSource } from './utils';
|
||||
import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare';
|
||||
|
||||
import { buildVizPanel, getRuntimePanelDataSource } from './utils';
|
||||
|
||||
// Mock the config needed for the function
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
@@ -32,9 +34,81 @@ jest.mock('@grafana/runtime', () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
featureToggles: {
|
||||
timeComparison: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock only what's essential for header actions tests
|
||||
jest.mock('../../scene/CustomTimeRangeCompare', () => ({
|
||||
CustomTimeRangeCompare: jest.fn(),
|
||||
}));
|
||||
|
||||
// Helper function to create a minimal panel for testing
|
||||
const createTestPanel = (): PanelKind => ({
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: 1,
|
||||
title: 'Test Panel',
|
||||
description: '',
|
||||
vizConfig: {
|
||||
kind: 'timeseries',
|
||||
spec: {
|
||||
options: {},
|
||||
fieldConfig: { defaults: {}, overrides: [] },
|
||||
pluginVersion: '1.0.0',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [],
|
||||
queryOptions: {},
|
||||
transformations: [],
|
||||
},
|
||||
},
|
||||
links: [],
|
||||
},
|
||||
});
|
||||
|
||||
describe('buildVizPanel', () => {
|
||||
describe('header actions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should include CustomTimeRangeCompare in headerActions when timeComparison feature toggle is enabled', () => {
|
||||
// Mock config with timeComparison enabled
|
||||
const mockConfig = require('@grafana/runtime').config;
|
||||
mockConfig.featureToggles.timeComparison = true;
|
||||
|
||||
const panel = createTestPanel();
|
||||
const vizPanel = buildVizPanel(panel);
|
||||
|
||||
expect(vizPanel.state.headerActions).toBeDefined();
|
||||
expect(vizPanel.state.headerActions).toHaveLength(1);
|
||||
expect(CustomTimeRangeCompare).toHaveBeenCalledWith({
|
||||
key: 'time-compare',
|
||||
compareWith: undefined,
|
||||
compareOptions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include headerActions when timeComparison feature toggle is disabled', () => {
|
||||
// Mock config with timeComparison disabled
|
||||
const mockConfig = require('@grafana/runtime').config;
|
||||
mockConfig.featureToggles.timeComparison = false;
|
||||
|
||||
const panel = createTestPanel();
|
||||
const vizPanel = buildVizPanel(panel);
|
||||
|
||||
expect(vizPanel.state.headerActions).toBeUndefined();
|
||||
expect(CustomTimeRangeCompare).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuntimePanelDataSource', () => {
|
||||
it('should return the datasource when it is specified in the query', () => {
|
||||
const query: PanelQueryKind = {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
||||
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
|
||||
import { ConditionalRenderingGroup } from '../../conditional-rendering/ConditionalRenderingGroup';
|
||||
import { conditionalRenderingSerializerRegistry } from '../../conditional-rendering/serializers';
|
||||
import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare';
|
||||
import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
|
||||
@@ -73,6 +74,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel {
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
// _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration
|
||||
headerActions: config.featureToggles.timeComparison
|
||||
? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })]
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!config.publicDashboardAccessToken) {
|
||||
|
||||
@@ -810,6 +810,41 @@ describe('transformSaveModelToScene', () => {
|
||||
expect((libPanelBehavior as LibraryPanelBehavior).state.name).toEqual(panel.libraryPanel.name);
|
||||
expect(gridItem.state.body.state.title).toEqual(panel.title);
|
||||
});
|
||||
|
||||
describe('header actions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should include headerActions when timeComparison feature toggle is enabled', () => {
|
||||
config.featureToggles.timeComparison = true;
|
||||
|
||||
const panel = {
|
||||
title: 'Test Panel',
|
||||
type: 'timeseries',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 8 },
|
||||
};
|
||||
|
||||
const { vizPanel } = buildGridItemForTest(panel);
|
||||
|
||||
expect(vizPanel.state.headerActions).toBeDefined();
|
||||
expect(vizPanel.state.headerActions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not include headerActions when timeComparison feature toggle is disabled', () => {
|
||||
config.featureToggles.timeComparison = false;
|
||||
|
||||
const panel = {
|
||||
title: 'Test Panel',
|
||||
type: 'timeseries',
|
||||
gridPos: { x: 0, y: 0, w: 12, h: 8 },
|
||||
};
|
||||
|
||||
const { vizPanel } = buildGridItemForTest(panel);
|
||||
|
||||
expect(vizPanel.state.headerActions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Convert to new rows', () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard';
|
||||
|
||||
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
|
||||
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||
import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
@@ -407,6 +408,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
|
||||
$behaviors: [],
|
||||
extendPanelContext: setDashboardPanelContext,
|
||||
_UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
|
||||
headerActions: config.featureToggles.timeComparison
|
||||
? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })]
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (panel.libraryPanel) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Dashboard, Panel, RowPanel } from '@grafana/schema';
|
||||
import { createLogger } from '@grafana/ui';
|
||||
import { initialIntervalVariableModelState } from 'app/features/variables/interval/reducer';
|
||||
|
||||
import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare';
|
||||
import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour';
|
||||
import { DashboardLayoutOrchestrator } from '../scene/DashboardLayoutOrchestrator';
|
||||
import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene';
|
||||
@@ -343,6 +344,9 @@ export function getDefaultVizPanel(): VizPanel {
|
||||
menu: new VizPanelMenu({
|
||||
$behaviors: [panelMenuBehavior],
|
||||
}),
|
||||
headerActions: config.featureToggles.timeComparison
|
||||
? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })]
|
||||
: undefined,
|
||||
$data: new SceneDataTransformer({
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [{ refId: 'A' }],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { commonOptionsBuilder } from '@grafana/ui';
|
||||
import { optsWithHideZeros } from '@grafana/ui/internal';
|
||||
|
||||
@@ -17,6 +18,10 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TimeSeriesPanel)
|
||||
commonOptionsBuilder.addTooltipOptions(builder, false, true, optsWithHideZeros);
|
||||
commonOptionsBuilder.addLegendOptions(builder);
|
||||
|
||||
if (config.featureToggles.timeComparison) {
|
||||
commonOptionsBuilder.addTimeCompareOption(builder);
|
||||
}
|
||||
|
||||
builder.addCustomEditor({
|
||||
id: 'timezone',
|
||||
name: t('timeseries.name-time-zone', 'Time zone'),
|
||||
|
||||
@@ -25,6 +25,7 @@ composableKinds: PanelCfg: lineage: {
|
||||
Options: common.OptionsWithTimezones & {
|
||||
legend: common.VizLegendOptions
|
||||
tooltip: common.VizTooltipOptions
|
||||
timeCompare?: common.TimeCompareOptions
|
||||
orientation?: common.VizOrientation
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as common from '@grafana/schema';
|
||||
export interface Options extends common.OptionsWithTimezones {
|
||||
legend: common.VizLegendOptions;
|
||||
orientation?: common.VizOrientation;
|
||||
timeCompare?: common.TimeCompareOptions;
|
||||
tooltip: common.VizTooltipOptions;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user