Skip to content

Commit

Permalink
DashboardScene: Discard panel changes disabled/enabled depending of c…
Browse files Browse the repository at this point in the history
…hanges (#87137)

---------

Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
  • Loading branch information
3 people committed May 9, 2024
1 parent 588abbb commit c3936bb
Show file tree
Hide file tree
Showing 7 changed files with 561 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { map, of } from 'rxjs';

import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingState, PanelData } from '@grafana/data';
import { calculateFieldTransformer } from '@grafana/data/src/transformations/transformers/calculateField';
import { mockTransformationsRegistry } from '@grafana/data/src/utils/tests/mockTransformationsRegistry';
import { config, locationService } from '@grafana/runtime';
import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema';
Expand Down Expand Up @@ -178,12 +180,17 @@ jest.mock('@grafana/runtime', () => ({
},
}));

mockTransformationsRegistry([calculateFieldTransformer]);

jest.useFakeTimers();

describe('VizPanelManager', () => {
describe('When changing plugin', () => {
it('Should successfully change from one viz type to another', () => {
const { vizPanelManager } = setupTest('panel-1');
expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries');
vizPanelManager.changePluginType('table');

expect(vizPanelManager.state.panel.state.pluginId).toBe('table');
});

Expand Down Expand Up @@ -405,6 +412,8 @@ describe('VizPanelManager', () => {
datasourceUid: 'gdev-prometheus',
});

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.state.datasource).toEqual(ds2Mock);
expect(vizPanelManager.state.dsSettings).toEqual(instance2SettingsMock);
});
Expand Down Expand Up @@ -472,6 +481,8 @@ describe('VizPanelManager', () => {
},
});

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('2h');
});

Expand Down Expand Up @@ -515,7 +526,7 @@ describe('VizPanelManager', () => {
});

describe('max data points and interval', () => {
it('max data points', async () => {
it('should update max data points', async () => {
const { vizPanelManager } = setupTest('panel-1');
vizPanelManager.activate();
await Promise.resolve();
Expand All @@ -534,10 +545,12 @@ describe('VizPanelManager', () => {
maxDataPoints: 100,
});

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(dataObj.state.maxDataPoints).toBe(100);
});

it('max data points', async () => {
it('should update min interval', async () => {
const { vizPanelManager } = setupTest('panel-1');
vizPanelManager.activate();
await Promise.resolve();
Expand All @@ -556,6 +569,8 @@ describe('VizPanelManager', () => {
minInterval: '1s',
});

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(dataObj.state.minInterval).toBe('1s');
});
});
Expand All @@ -579,6 +594,8 @@ describe('VizPanelManager', () => {
queries: [],
});

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(dataObj.state.cacheTimeout).toBe('60');
expect(dataObj.state.queryCachingTTL).toBe(200000);
});
Expand Down Expand Up @@ -616,6 +633,8 @@ describe('VizPanelManager', () => {
},
} as DataSourceInstanceSettings);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
uid: 'gdev-prometheus',
type: 'grafana-prometheus-datasource',
Expand Down Expand Up @@ -643,6 +662,8 @@ describe('VizPanelManager', () => {
},
} as DataSourceInstanceSettings);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
uid: SHARED_DASHBOARD_QUERY,
type: 'datasource',
Expand Down Expand Up @@ -670,6 +691,8 @@ describe('VizPanelManager', () => {
},
} as DataSourceInstanceSettings);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.queryRunner.state.datasource).toEqual({
uid: 'gdev-prometheus',
type: 'grafana-prometheus-datasource',
Expand All @@ -691,6 +714,8 @@ describe('VizPanelManager', () => {
vizPanelManager.dataTransformer.reprocessTransformations = reprocessMock;
vizPanelManager.changeTransformations([{ id: 'calculateField', options: {} }]);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(reprocessMock).toHaveBeenCalledTimes(1);
expect(vizPanelManager.dataTransformer.state.transformations).toEqual([{ id: 'calculateField', options: {} }]);
});
Expand All @@ -716,6 +741,8 @@ describe('VizPanelManager', () => {
},
]);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.queryRunner.state.queries).toEqual([
{
datasource: {
Expand Down Expand Up @@ -763,6 +790,8 @@ describe('VizPanelManager', () => {
},
]);

jest.runAllTimers(); // The detect panel changes is debounced
expect(vizPanelManager.state.isDirty).toBe(true);
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useEffect } from 'react';

import {
Expand All @@ -20,6 +21,7 @@ import {
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SceneObjectStateChangedEvent,
SceneQueryRunner,
VizPanel,
sceneUtils,
Expand All @@ -34,10 +36,12 @@ import { updateQueries } from 'app/features/query/state/updateQueries';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { QueryGroupOptions } from 'app/types';

import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { getPanelChanges } from '../saving/getDashboardChanges';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange';
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';

export interface VizPanelManagerState extends SceneObjectState {
Expand All @@ -49,6 +53,7 @@ export interface VizPanelManagerState extends SceneObjectState {
repeat?: string;
repeatDirection?: RepeatDirection;
maxPerRow?: number;
isDirty?: boolean;
}

export enum DisplayMode {
Expand Down Expand Up @@ -95,8 +100,27 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {

private _onActivate() {
this.loadDataSource();
const changesSub = this.subscribeToEvent(SceneObjectStateChangedEvent, this._handleStateChange);

return () => {
changesSub.unsubscribe();
};
}

private _detectPanelModelChanges = debounce(() => {
const { hasChanges } = getPanelChanges(
vizPanelToPanel(this.state.sourcePanel.resolve()),
vizPanelToPanel(this.state.panel)
);
this.setState({ isDirty: hasChanges });
}, 250);

private _handleStateChange = (event: SceneObjectStateChangedEvent) => {
if (DashboardSceneChangeTracker.isUpdatingPersistedState(event)) {
this._detectPanelModelChanges();
}
};

private async loadDataSource() {
const dataObj = this.state.panel.state.$data;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ export class DashboardSceneChangeTracker {
this._dashboard = dashboard;
}

private onStateChanged({ payload }: SceneObjectStateChangedEvent) {
static isUpdatingPersistedState({ payload }: SceneObjectStateChangedEvent) {
// If there are no changes in the state, the check is not needed
if (Object.keys(payload.partialUpdate).length === 0) {
return;
return false;
}

// Any change in the panel should trigger a change detection
Expand All @@ -50,7 +50,7 @@ export class DashboardSceneChangeTracker {
payload.changedObject instanceof DashboardGridItem ||
payload.changedObject instanceof PanelTimeRange
) {
return this.detectSaveModelChanges();
return true;
}
// VizPanelManager includes the repeat configuration
if (payload.changedObject instanceof VizPanelManager) {
Expand All @@ -59,75 +59,82 @@ export class DashboardSceneChangeTracker {
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'repeatDirection') ||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'maxPerRow')
) {
return this.detectSaveModelChanges();
return true;
}
}
// SceneQueryRunner includes the DS configuration
if (payload.changedObject instanceof SceneQueryRunner) {
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
return this.detectSaveModelChanges();
return true;
}
}
// SceneDataTransformer includes the transformation configuration
if (payload.changedObject instanceof SceneDataTransformer) {
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof VizPanelLinks) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof LibraryVizPanel) {
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'name')) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof SceneRefreshPicker) {
if (
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'intervals') ||
Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'refresh')
) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof behaviors.CursorSync) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof SceneDataLayerSet) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof DashboardGridItem) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof SceneGridLayout) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof DashboardScene) {
if (Object.keys(payload.partialUpdate).some((key) => PERSISTED_PROPS.includes(key))) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof SceneTimeRange) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof DashboardControls) {
if (Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'hideTimeControls')) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof SceneVariableSet) {
return this.detectSaveModelChanges();
return true;
}
if (payload.changedObject instanceof DashboardAnnotationsDataLayer) {
if (!Object.prototype.hasOwnProperty.call(payload.partialUpdate, 'data')) {
return this.detectSaveModelChanges();
return true;
}
}
if (payload.changedObject instanceof behaviors.LiveNowTimer) {
return this.detectSaveModelChanges();
return true;
}
if (isSceneVariableInstance(payload.changedObject)) {
return this.detectSaveModelChanges();
return true;
}
return false;
}

private onStateChanged(event: SceneObjectStateChangedEvent) {
if (DashboardSceneChangeTracker.isUpdatingPersistedState(event)) {
this.detectSaveModelChanges();
}
}

Expand Down

0 comments on commit c3936bb

Please sign in to comment.