mirror of https://github.com/grafana/grafana
PanelEdit: Edit the source panel, refactor out VizPanelManager, simplify (#93045)
* Options pane, data pane queries tab and transformations tab working * Update * Discard works * Panel inspect working * Table viw works * Repeat options * Began fixing tests * More tests fixed * Progress on tests * no errors * init full width when enabling repeat * Began moving VizPanelManager tests to where the code was moved * Unlink libray panel code and unit test * Fixes and unit tests for change tracking and resetting original state and dirty flag when saving * migrating and improving unit tests * Done with VizPanelManager tests refactoring * Update * Update * remove console.log * Removed unnesssary behavior and fixed test * Update * Fix unrelated test * conditional options fix * remove * Fixing issue with editing repeated panels and scoping variable to first value * Minor fix * Fix discard query runner changes * Review comment changes * fix discard issue with new panels * Add loading state to panel edit * Fix test * Update * fix test * fix lint * lint * Fix * Fix overrides editing mutating fieldConfig --------- Co-authored-by: alexandra vargas <alexa1866@gmail.com>pull/93691/head
parent
9adb7b03a7
commit
038d9cabde
@ -0,0 +1,48 @@ |
||||
import { Observable } from 'rxjs'; |
||||
|
||||
import { |
||||
SceneDataProvider, |
||||
SceneDataProviderResult, |
||||
SceneDataState, |
||||
SceneObjectBase, |
||||
SceneObjectRef, |
||||
} from '@grafana/scenes'; |
||||
|
||||
export interface DataProviderSharerState extends SceneDataState { |
||||
source: SceneObjectRef<SceneDataProvider>; |
||||
} |
||||
|
||||
export class DataProviderSharer extends SceneObjectBase<DataProviderSharerState> implements SceneDataProvider { |
||||
public constructor(state: DataProviderSharerState) { |
||||
super({ |
||||
source: state.source, |
||||
data: state.source.resolve().state.data, |
||||
}); |
||||
|
||||
this.addActivationHandler(() => { |
||||
this._subs.add( |
||||
this.state.source.resolve().subscribeToState((newState, oldState) => { |
||||
if (newState.data !== oldState.data) { |
||||
this.setState({ data: newState.data }); |
||||
} |
||||
}) |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public setContainerWidth(width: number) { |
||||
this.state.source.resolve().setContainerWidth?.(width); |
||||
} |
||||
|
||||
public isDataReadyToDisplay() { |
||||
return this.state.source.resolve().isDataReadyToDisplay?.() ?? true; |
||||
} |
||||
|
||||
public cancelQuery() { |
||||
this.state.source.resolve().cancelQuery?.(); |
||||
} |
||||
|
||||
public getResultsStream(): Observable<SceneDataProviderResult> { |
||||
return this.state.source.resolve().getResultsStream(); |
||||
} |
||||
} |
@ -0,0 +1,101 @@ |
||||
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; |
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; |
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; |
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; |
||||
import { findVizPanelByKey } from '../utils/utils'; |
||||
|
||||
import { PanelOptionsPane } from './PanelOptionsPane'; |
||||
import { testDashboard } from './testfiles/testDashboard'; |
||||
|
||||
describe('PanelOptionsPane', () => { |
||||
describe('When changing plugin', () => { |
||||
it('Should set the cache', () => { |
||||
const { optionsPane, panel } = setupTest('panel-1'); |
||||
panel.changePluginType = jest.fn(); |
||||
|
||||
expect(panel.state.pluginId).toBe('timeseries'); |
||||
|
||||
optionsPane.onChangePanelPlugin({ pluginId: 'table' }); |
||||
|
||||
expect(optionsPane['_cachedPluginOptions']['timeseries']?.options).toBe(panel.state.options); |
||||
expect(optionsPane['_cachedPluginOptions']['timeseries']?.fieldConfig).toBe(panel.state.fieldConfig); |
||||
}); |
||||
|
||||
it('Should preserve correct field config', () => { |
||||
const { optionsPane, panel } = setupTest('panel-1'); |
||||
|
||||
const mockFn = jest.fn(); |
||||
panel.changePluginType = mockFn; |
||||
|
||||
const fieldConfig = panel.state.fieldConfig; |
||||
|
||||
fieldConfig.defaults = { |
||||
...fieldConfig.defaults, |
||||
unit: 'flop', |
||||
decimals: 2, |
||||
}; |
||||
|
||||
fieldConfig.overrides = [ |
||||
{ |
||||
matcher: { |
||||
id: 'byName', |
||||
options: 'A-series', |
||||
}, |
||||
properties: [ |
||||
{ |
||||
id: 'displayName', |
||||
value: 'test', |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
matcher: { id: 'byName', options: 'D-series' }, |
||||
//should be removed because it's custom
|
||||
properties: [ |
||||
{ |
||||
id: 'custom.customPropNoExist', |
||||
value: 'google', |
||||
}, |
||||
], |
||||
}, |
||||
]; |
||||
|
||||
panel.setState({ fieldConfig: fieldConfig }); |
||||
|
||||
expect(panel.state.fieldConfig.defaults.color?.mode).toBe('palette-classic'); |
||||
expect(panel.state.fieldConfig.defaults.thresholds?.mode).toBe('absolute'); |
||||
expect(panel.state.fieldConfig.defaults.unit).toBe('flop'); |
||||
expect(panel.state.fieldConfig.defaults.decimals).toBe(2); |
||||
expect(panel.state.fieldConfig.overrides).toHaveLength(2); |
||||
expect(panel.state.fieldConfig.overrides[1].properties).toHaveLength(1); |
||||
expect(panel.state.fieldConfig.defaults.custom).toHaveProperty('axisBorderShow'); |
||||
|
||||
optionsPane.onChangePanelPlugin({ pluginId: 'table' }); |
||||
|
||||
expect(mockFn).toHaveBeenCalled(); |
||||
expect(mockFn.mock.calls[0][2].defaults.color?.mode).toBe('palette-classic'); |
||||
expect(mockFn.mock.calls[0][2].defaults.thresholds?.mode).toBe('absolute'); |
||||
expect(mockFn.mock.calls[0][2].defaults.unit).toBe('flop'); |
||||
expect(mockFn.mock.calls[0][2].defaults.decimals).toBe(2); |
||||
expect(mockFn.mock.calls[0][2].overrides).toHaveLength(2); |
||||
//removed custom property
|
||||
expect(mockFn.mock.calls[0][2].overrides[1].properties).toHaveLength(0); |
||||
//removed fieldConfig custom values as well
|
||||
expect(mockFn.mock.calls[0][2].defaults.custom).toStrictEqual({}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
function setupTest(panelId: string) { |
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} }); |
||||
const panel = findVizPanelByKey(scene, panelId)!; |
||||
|
||||
const optionsPane = new PanelOptionsPane({ panelRef: panel.getRef(), listMode: OptionFilter.All, searchQuery: '' }); |
||||
|
||||
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||
// @ts-expect-error
|
||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene)); |
||||
|
||||
return { optionsPane, scene, panel }; |
||||
} |
@ -1,864 +0,0 @@ |
||||
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 { |
||||
CustomVariable, |
||||
LocalValueVariable, |
||||
SceneGridRow, |
||||
SceneVariableSet, |
||||
VizPanel, |
||||
sceneGraph, |
||||
} from '@grafana/scenes'; |
||||
import { DataQuery, DataSourceJsonData, DataSourceRef } from '@grafana/schema'; |
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; |
||||
import { InspectTab } from 'app/features/inspector/types'; |
||||
import * as libAPI from 'app/features/library-panels/state/api'; |
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; |
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types'; |
||||
|
||||
import { DashboardGridItem } from '../scene/DashboardGridItem'; |
||||
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; |
||||
import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; |
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; |
||||
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; |
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; |
||||
import { findVizPanelByKey } from '../utils/utils'; |
||||
|
||||
import { buildPanelEditScene } from './PanelEditor'; |
||||
import { VizPanelManager } from './VizPanelManager'; |
||||
import { panelWithQueriesOnly, panelWithTransformations, testDashboard } from './testfiles/testDashboard'; |
||||
|
||||
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { |
||||
const result: PanelData = { |
||||
state: LoadingState.Loading, |
||||
series: [], |
||||
timeRange: request.range, |
||||
}; |
||||
|
||||
return of([]).pipe( |
||||
map(() => { |
||||
result.state = LoadingState.Done; |
||||
result.series = []; |
||||
|
||||
return result; |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
const ds1Mock: DataSourceApi = { |
||||
meta: { |
||||
id: 'grafana-testdata-datasource', |
||||
}, |
||||
name: 'grafana-testdata-datasource', |
||||
type: 'grafana-testdata-datasource', |
||||
uid: 'gdev-testdata', |
||||
getRef: () => { |
||||
return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }; |
||||
}, |
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>; |
||||
|
||||
const ds2Mock: DataSourceApi = { |
||||
meta: { |
||||
id: 'grafana-prometheus-datasource', |
||||
}, |
||||
name: 'grafana-prometheus-datasource', |
||||
type: 'grafana-prometheus-datasource', |
||||
uid: 'gdev-prometheus', |
||||
getRef: () => { |
||||
return { type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' }; |
||||
}, |
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>; |
||||
|
||||
const ds3Mock: DataSourceApi = { |
||||
meta: { |
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID, |
||||
}, |
||||
name: SHARED_DASHBOARD_QUERY, |
||||
type: SHARED_DASHBOARD_QUERY, |
||||
uid: SHARED_DASHBOARD_QUERY, |
||||
getRef: () => { |
||||
return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY }; |
||||
}, |
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>; |
||||
|
||||
const defaultDsMock: DataSourceApi = { |
||||
meta: { |
||||
id: 'grafana-testdata-datasource', |
||||
}, |
||||
name: 'grafana-testdata-datasource', |
||||
type: 'grafana-testdata-datasource', |
||||
uid: 'gdev-testdata', |
||||
getRef: () => { |
||||
return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }; |
||||
}, |
||||
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>; |
||||
|
||||
const instance1SettingsMock = { |
||||
id: 1, |
||||
uid: 'gdev-testdata', |
||||
name: 'testDs1', |
||||
type: 'grafana-testdata-datasource', |
||||
meta: { |
||||
id: 'grafana-testdata-datasource', |
||||
}, |
||||
}; |
||||
|
||||
const instance2SettingsMock = { |
||||
id: 1, |
||||
uid: 'gdev-prometheus', |
||||
name: 'testDs2', |
||||
type: 'grafana-prometheus-datasource', |
||||
meta: { |
||||
id: 'grafana-prometheus-datasource', |
||||
}, |
||||
}; |
||||
|
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
const grafanaDs = { |
||||
id: 1, |
||||
uid: '-- Grafana --', |
||||
name: 'grafana', |
||||
type: 'grafana', |
||||
meta: { |
||||
id: 'grafana', |
||||
}, |
||||
}; |
||||
|
||||
// Mock the store module
|
||||
jest.mock('app/core/store', () => ({ |
||||
exists: jest.fn(), |
||||
get: jest.fn(), |
||||
getObject: jest.fn((_a, b) => b), |
||||
setObject: jest.fn(), |
||||
})); |
||||
|
||||
const store = jest.requireMock('app/core/store'); |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { |
||||
return runRequestMock(ds, request); |
||||
}, |
||||
getDataSourceSrv: () => ({ |
||||
get: async (ref: DataSourceRef) => { |
||||
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||
|
||||
if (ref.uid === '-- Grafana --') { |
||||
return grafanaDs; |
||||
} |
||||
|
||||
if (ref.uid === 'gdev-testdata') { |
||||
return ds1Mock; |
||||
} |
||||
|
||||
if (ref.uid === 'gdev-prometheus') { |
||||
return ds2Mock; |
||||
} |
||||
|
||||
if (ref.uid === SHARED_DASHBOARD_QUERY) { |
||||
return ds3Mock; |
||||
} |
||||
|
||||
// if datasource is not found, return default datasource
|
||||
return defaultDsMock; |
||||
}, |
||||
getInstanceSettings: (ref: DataSourceRef) => { |
||||
if (ref.uid === 'gdev-testdata') { |
||||
return instance1SettingsMock; |
||||
} |
||||
|
||||
if (ref.uid === 'gdev-prometheus') { |
||||
return instance2SettingsMock; |
||||
} |
||||
|
||||
// if datasource is not found, return default instance settings
|
||||
return instance1SettingsMock; |
||||
}, |
||||
}), |
||||
locationService: { |
||||
partial: jest.fn(), |
||||
}, |
||||
config: { |
||||
...jest.requireActual('@grafana/runtime').config, |
||||
defaultDatasource: 'gdev-testdata', |
||||
}, |
||||
})); |
||||
|
||||
mockTransformationsRegistry([calculateFieldTransformer]); |
||||
|
||||
jest.useFakeTimers(); |
||||
|
||||
describe('VizPanelManager', () => { |
||||
describe('When changing plugin', () => { |
||||
it('Should set the cache', () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.state.panel.changePluginType = jest.fn(); |
||||
|
||||
expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries'); |
||||
|
||||
vizPanelManager.changePluginType('table'); |
||||
|
||||
expect(vizPanelManager['_cachedPluginOptions']['timeseries']?.options).toBe( |
||||
vizPanelManager.state.panel.state.options |
||||
); |
||||
expect(vizPanelManager['_cachedPluginOptions']['timeseries']?.fieldConfig).toBe( |
||||
vizPanelManager.state.panel.state.fieldConfig |
||||
); |
||||
}); |
||||
|
||||
it('Should preserve correct field config', () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
const mockFn = jest.fn(); |
||||
vizPanelManager.state.panel.changePluginType = mockFn; |
||||
const fieldConfig = vizPanelManager.state.panel.state.fieldConfig; |
||||
fieldConfig.defaults = { |
||||
...fieldConfig.defaults, |
||||
unit: 'flop', |
||||
decimals: 2, |
||||
}; |
||||
fieldConfig.overrides = [ |
||||
{ |
||||
matcher: { |
||||
id: 'byName', |
||||
options: 'A-series', |
||||
}, |
||||
properties: [ |
||||
{ |
||||
id: 'displayName', |
||||
value: 'test', |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
matcher: { id: 'byName', options: 'D-series' }, |
||||
//should be removed because it's custom
|
||||
properties: [ |
||||
{ |
||||
id: 'custom.customPropNoExist', |
||||
value: 'google', |
||||
}, |
||||
], |
||||
}, |
||||
]; |
||||
vizPanelManager.state.panel.setState({ |
||||
fieldConfig: fieldConfig, |
||||
}); |
||||
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.color?.mode).toBe('palette-classic'); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.thresholds?.mode).toBe('absolute'); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.unit).toBe('flop'); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.decimals).toBe(2); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toHaveLength(2); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides[1].properties).toHaveLength(1); |
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toHaveProperty('axisBorderShow'); |
||||
|
||||
vizPanelManager.changePluginType('table'); |
||||
|
||||
expect(mockFn).toHaveBeenCalled(); |
||||
expect(mockFn.mock.calls[0][2].defaults.color?.mode).toBe('palette-classic'); |
||||
expect(mockFn.mock.calls[0][2].defaults.thresholds?.mode).toBe('absolute'); |
||||
expect(mockFn.mock.calls[0][2].defaults.unit).toBe('flop'); |
||||
expect(mockFn.mock.calls[0][2].defaults.decimals).toBe(2); |
||||
expect(mockFn.mock.calls[0][2].overrides).toHaveLength(2); |
||||
//removed custom property
|
||||
expect(mockFn.mock.calls[0][2].overrides[1].properties).toHaveLength(0); |
||||
//removed fieldConfig custom values as well
|
||||
expect(mockFn.mock.calls[0][2].defaults.custom).toStrictEqual({}); |
||||
}); |
||||
}); |
||||
|
||||
describe('library panels', () => { |
||||
it('saves library panels on commit', () => { |
||||
const panel = new VizPanel({ |
||||
key: 'panel-1', |
||||
pluginId: 'text', |
||||
}); |
||||
|
||||
const libraryPanelModel = { |
||||
title: 'title', |
||||
uid: 'uid', |
||||
name: 'libraryPanelName', |
||||
model: vizPanelToPanel(panel), |
||||
type: 'panel', |
||||
version: 1, |
||||
}; |
||||
|
||||
const libPanelBehavior = new LibraryPanelBehavior({ |
||||
isLoaded: true, |
||||
title: libraryPanelModel.title, |
||||
uid: libraryPanelModel.uid, |
||||
name: libraryPanelModel.name, |
||||
_loadedPanel: libraryPanelModel, |
||||
}); |
||||
|
||||
panel.setState({ |
||||
$behaviors: [libPanelBehavior], |
||||
}); |
||||
|
||||
new DashboardGridItem({ body: panel }); |
||||
|
||||
const panelManager = VizPanelManager.createFor(panel); |
||||
|
||||
const apiCall = jest.spyOn(libAPI, 'saveLibPanel'); |
||||
|
||||
panelManager.state.panel.setState({ title: 'new title' }); |
||||
panelManager.commitChanges(); |
||||
|
||||
expect(apiCall.mock.calls[0][0].state.title).toBe('new title'); |
||||
}); |
||||
|
||||
it('unlinks library panel', () => { |
||||
const panel = new VizPanel({ |
||||
key: 'panel-1', |
||||
pluginId: 'text', |
||||
}); |
||||
|
||||
const libraryPanelModel = { |
||||
title: 'title', |
||||
uid: 'uid', |
||||
name: 'libraryPanelName', |
||||
model: vizPanelToPanel(panel), |
||||
type: 'panel', |
||||
version: 1, |
||||
}; |
||||
|
||||
const libPanelBehavior = new LibraryPanelBehavior({ |
||||
isLoaded: true, |
||||
title: libraryPanelModel.title, |
||||
uid: libraryPanelModel.uid, |
||||
name: libraryPanelModel.name, |
||||
_loadedPanel: libraryPanelModel, |
||||
}); |
||||
|
||||
panel.setState({ |
||||
$behaviors: [libPanelBehavior], |
||||
}); |
||||
|
||||
new DashboardGridItem({ body: panel }); |
||||
|
||||
const panelManager = VizPanelManager.createFor(panel); |
||||
panelManager.unlinkLibraryPanel(); |
||||
|
||||
const sourcePanel = panelManager.state.sourcePanel.resolve(); |
||||
expect(sourcePanel.state.$behaviors).toBe(undefined); |
||||
}); |
||||
}); |
||||
|
||||
describe('query options', () => { |
||||
beforeEach(() => { |
||||
store.setObject.mockClear(); |
||||
}); |
||||
|
||||
describe('activation', () => { |
||||
it('should load data source', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.state.datasource).toEqual(ds1Mock); |
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock); |
||||
}); |
||||
|
||||
it('should store loaded data source in local storage', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(store.setObject).toHaveBeenCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { |
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', |
||||
datasourceUid: 'gdev-testdata', |
||||
}); |
||||
}); |
||||
|
||||
it('should load default datasource if the datasource passed is not found', async () => { |
||||
const { vizPanelManager } = setupTest('panel-6'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({ |
||||
uid: 'abc', |
||||
type: 'datasource', |
||||
}); |
||||
|
||||
expect(config.defaultDatasource).toBe('gdev-testdata'); |
||||
expect(vizPanelManager.state.datasource).toEqual(defaultDsMock); |
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock); |
||||
}); |
||||
}); |
||||
|
||||
describe('data source change', () => { |
||||
it('should load new data source', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
vizPanelManager.state.panel.state.$data?.activate(); |
||||
|
||||
await Promise.resolve(); |
||||
|
||||
await vizPanelManager.changePanelDataSource( |
||||
{ type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' } as DataSourceInstanceSettings, |
||||
[] |
||||
); |
||||
|
||||
expect(store.setObject).toHaveBeenCalledTimes(2); |
||||
expect(store.setObject).toHaveBeenLastCalledWith('grafana.dashboards.panelEdit.lastUsedDatasource', { |
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', |
||||
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); |
||||
}); |
||||
}); |
||||
|
||||
describe('query options change', () => { |
||||
describe('time overrides', () => { |
||||
it('should create PanelTimeRange object', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
vizPanelManager.state.panel.state.$data?.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const panel = vizPanelManager.state.panel; |
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined(); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
timeRange: { |
||||
from: '1h', |
||||
}, |
||||
}); |
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); |
||||
}); |
||||
it('should update PanelTimeRange object on time options update', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const panel = vizPanelManager.state.panel; |
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined(); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
timeRange: { |
||||
from: '1h', |
||||
}, |
||||
}); |
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); |
||||
expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('1h'); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
timeRange: { |
||||
from: '2h', |
||||
}, |
||||
}); |
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true); |
||||
expect((panel.state.$timeRange?.state as PanelTimeRangeState).timeFrom).toBe('2h'); |
||||
}); |
||||
|
||||
it('should remove PanelTimeRange object on time options cleared', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const panel = vizPanelManager.state.panel; |
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined(); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
timeRange: { |
||||
from: '1h', |
||||
}, |
||||
}); |
||||
|
||||
expect(panel.state.$timeRange).toBeInstanceOf(PanelTimeRange); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
timeRange: { |
||||
from: null, |
||||
}, |
||||
}); |
||||
|
||||
expect(panel.state.$timeRange).toBeUndefined(); |
||||
}); |
||||
}); |
||||
|
||||
describe('max data points and interval', () => { |
||||
it('should update max data points', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const dataObj = vizPanelManager.queryRunner; |
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined(); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
maxDataPoints: 100, |
||||
}); |
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true); |
||||
expect(dataObj.state.maxDataPoints).toBe(100); |
||||
}); |
||||
|
||||
it('should update min interval', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const dataObj = vizPanelManager.queryRunner; |
||||
|
||||
expect(dataObj.state.maxDataPoints).toBeUndefined(); |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
queries: [], |
||||
minInterval: '1s', |
||||
}); |
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true); |
||||
expect(dataObj.state.minInterval).toBe('1s'); |
||||
}); |
||||
}); |
||||
|
||||
describe('query caching', () => { |
||||
it('updates cacheTimeout and queryCachingTTL', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
const dataObj = vizPanelManager.queryRunner; |
||||
|
||||
vizPanelManager.changeQueryOptions({ |
||||
cacheTimeout: '60', |
||||
queryCachingTTL: 200000, |
||||
dataSource: { |
||||
name: 'grafana-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
default: true, |
||||
}, |
||||
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); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('query inspection', () => { |
||||
it('allows query inspection from the tab', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.inspectPanel(); |
||||
|
||||
expect(locationService.partial).toHaveBeenCalledWith({ inspect: 1, inspectTab: InspectTab.Query }); |
||||
}); |
||||
}); |
||||
|
||||
describe('data source change', () => { |
||||
it('changing from one plugin to another', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({ |
||||
uid: 'gdev-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
}); |
||||
|
||||
await vizPanelManager.changePanelDataSource({ |
||||
name: 'grafana-prometheus', |
||||
type: 'grafana-prometheus-datasource', |
||||
uid: 'gdev-prometheus', |
||||
meta: { |
||||
name: 'Prometheus', |
||||
module: 'prometheus', |
||||
id: 'grafana-prometheus-datasource', |
||||
}, |
||||
} 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', |
||||
}); |
||||
}); |
||||
|
||||
it('changing from a plugin to a dashboard data source', async () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({ |
||||
uid: 'gdev-testdata', |
||||
type: 'grafana-testdata-datasource', |
||||
}); |
||||
|
||||
await vizPanelManager.changePanelDataSource({ |
||||
name: SHARED_DASHBOARD_QUERY, |
||||
type: 'datasource', |
||||
uid: SHARED_DASHBOARD_QUERY, |
||||
meta: { |
||||
name: 'Prometheus', |
||||
module: 'prometheus', |
||||
id: DASHBOARD_DATASOURCE_PLUGIN_ID, |
||||
}, |
||||
} 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', |
||||
}); |
||||
}); |
||||
|
||||
it('changing from dashboard data source to a plugin', async () => { |
||||
const { vizPanelManager } = setupTest('panel-3'); |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.queryRunner.state.datasource).toEqual({ |
||||
uid: SHARED_DASHBOARD_QUERY, |
||||
type: 'datasource', |
||||
}); |
||||
|
||||
await vizPanelManager.changePanelDataSource({ |
||||
name: 'grafana-prometheus', |
||||
type: 'grafana-prometheus-datasource', |
||||
uid: 'gdev-prometheus', |
||||
meta: { |
||||
name: 'Prometheus', |
||||
module: 'prometheus', |
||||
id: 'grafana-prometheus-datasource', |
||||
}, |
||||
} 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', |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('change transformations', () => { |
||||
it('should update and reprocess transformations', () => { |
||||
const { scene, panel } = setupTest('panel-3'); |
||||
scene.setState({ editPanel: buildPanelEditScene(panel) }); |
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager; |
||||
vizPanelManager.activate(); |
||||
vizPanelManager.state.panel.state.$data?.activate(); |
||||
|
||||
const reprocessMock = jest.fn(); |
||||
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: {} }]); |
||||
}); |
||||
}); |
||||
|
||||
describe('change queries', () => { |
||||
describe('plugin queries', () => { |
||||
it('should update queries', () => { |
||||
const { vizPanelManager } = setupTest('panel-1'); |
||||
|
||||
vizPanelManager.activate(); |
||||
vizPanelManager.state.panel.state.$data?.activate(); |
||||
|
||||
vizPanelManager.changeQueries([ |
||||
{ |
||||
datasource: { |
||||
type: 'grafana-testdata-datasource', |
||||
uid: 'gdev-testdata', |
||||
}, |
||||
refId: 'A', |
||||
scenarioId: 'random_walk', |
||||
seriesCount: 5, |
||||
}, |
||||
]); |
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true); |
||||
expect(vizPanelManager.queryRunner.state.queries).toEqual([ |
||||
{ |
||||
datasource: { |
||||
type: 'grafana-testdata-datasource', |
||||
uid: 'gdev-testdata', |
||||
}, |
||||
refId: 'A', |
||||
scenarioId: 'random_walk', |
||||
seriesCount: 5, |
||||
}, |
||||
]); |
||||
}); |
||||
}); |
||||
|
||||
describe('dashboard queries', () => { |
||||
it('should update queries', () => { |
||||
const { scene, panel } = setupTest('panel-3'); |
||||
scene.setState({ editPanel: buildPanelEditScene(panel) }); |
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager; |
||||
vizPanelManager.activate(); |
||||
vizPanelManager.state.panel.state.$data?.activate(); |
||||
|
||||
// Changing dashboard query to a panel with transformations
|
||||
vizPanelManager.changeQueries([ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
type: DASHBOARD_DATASOURCE_PLUGIN_ID, |
||||
}, |
||||
panelId: panelWithTransformations.id, |
||||
}, |
||||
]); |
||||
|
||||
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id); |
||||
|
||||
// Changing dashboard query to a panel with queries only
|
||||
vizPanelManager.changeQueries([ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
type: DASHBOARD_DATASOURCE_PLUGIN_ID, |
||||
}, |
||||
panelId: panelWithQueriesOnly.id, |
||||
}, |
||||
]); |
||||
|
||||
jest.runAllTimers(); // The detect panel changes is debounced
|
||||
expect(vizPanelManager.state.isDirty).toBe(true); |
||||
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
it('should load last used data source if no data source specified for a panel', async () => { |
||||
store.exists.mockReturnValue(true); |
||||
store.getObject.mockReturnValue({ |
||||
dashboardUid: 'ffbe00e2-803c-4d49-adb7-41aad336234f', |
||||
datasourceUid: 'gdev-testdata', |
||||
}); |
||||
const { scene, panel } = setupTest('panel-5'); |
||||
scene.setState({ editPanel: buildPanelEditScene(panel) }); |
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager; |
||||
vizPanelManager.activate(); |
||||
await Promise.resolve(); |
||||
|
||||
expect(vizPanelManager.state.datasource).toEqual(ds1Mock); |
||||
expect(vizPanelManager.state.dsSettings).toEqual(instance1SettingsMock); |
||||
}); |
||||
|
||||
it('Should default to the first variable value if panel is repeated', async () => { |
||||
const { scene, panel } = setupTest('panel-10'); |
||||
|
||||
scene.setState({ |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new CustomVariable({ name: 'custom', query: 'A,B,C', value: ['A', 'B', 'C'], text: ['A', 'B', 'C'] }), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
scene.setState({ editPanel: buildPanelEditScene(panel) }); |
||||
|
||||
const vizPanelManager = scene.state.editPanel!.state.vizManager; |
||||
vizPanelManager.activate(); |
||||
|
||||
const variable = sceneGraph.lookupVariable('custom', vizPanelManager); |
||||
expect(variable?.getValue()).toBe('A'); |
||||
}); |
||||
|
||||
describe('Given a panel inside repeated row', () => { |
||||
it('Should include row variable scope', () => { |
||||
const { panel } = setupTest('panel-9'); |
||||
|
||||
const row = panel.parent?.parent; |
||||
if (!(row instanceof SceneGridRow)) { |
||||
throw new Error('Did not find parent row'); |
||||
} |
||||
|
||||
row.setState({ |
||||
$variables: new SceneVariableSet({ variables: [new LocalValueVariable({ name: 'hello', value: 'A' })] }), |
||||
}); |
||||
|
||||
const editor = buildPanelEditScene(panel); |
||||
const variable = sceneGraph.lookupVariable('hello', editor.state.vizManager); |
||||
expect(variable?.getValue()).toBe('A'); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
const setupTest = (panelId: string) => { |
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} }); |
||||
|
||||
const panel = findVizPanelByKey(scene, panelId)!; |
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(panel); |
||||
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||
// @ts-expect-error
|
||||
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene)); |
||||
|
||||
return { vizPanelManager, scene, panel }; |
||||
}; |
@ -1,504 +0,0 @@ |
||||
import { css } from '@emotion/css'; |
||||
import { debounce } from 'lodash'; |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { |
||||
DataSourceApi, |
||||
DataSourceInstanceSettings, |
||||
FieldConfigSource, |
||||
GrafanaTheme2, |
||||
filterFieldConfigOverrides, |
||||
getDataSourceRef, |
||||
isStandardFieldProp, |
||||
restoreCustomOverrideRules, |
||||
} from '@grafana/data'; |
||||
import { config, getDataSourceSrv, locationService } from '@grafana/runtime'; |
||||
import { |
||||
DeepPartial, |
||||
LocalValueVariable, |
||||
MultiValueVariable, |
||||
PanelBuilders, |
||||
SceneComponentProps, |
||||
SceneDataTransformer, |
||||
SceneObjectBase, |
||||
SceneObjectRef, |
||||
SceneObjectState, |
||||
SceneObjectStateChangedEvent, |
||||
SceneQueryRunner, |
||||
SceneVariableSet, |
||||
SceneVariables, |
||||
VizPanel, |
||||
sceneGraph, |
||||
} from '@grafana/scenes'; |
||||
import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema'; |
||||
import { useStyles2 } from '@grafana/ui'; |
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; |
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; |
||||
import { saveLibPanel } from 'app/features/library-panels/state/api'; |
||||
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 { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; |
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel'; |
||||
import { |
||||
getDashboardSceneFor, |
||||
getMultiVariableValues, |
||||
getPanelIdForVizPanel, |
||||
getQueryRunnerFor, |
||||
isLibraryPanel, |
||||
} from '../utils/utils'; |
||||
|
||||
export interface VizPanelManagerState extends SceneObjectState { |
||||
panel: VizPanel; |
||||
sourcePanel: SceneObjectRef<VizPanel>; |
||||
pluginId: string; |
||||
datasource?: DataSourceApi; |
||||
dsSettings?: DataSourceInstanceSettings; |
||||
tableView?: VizPanel; |
||||
repeat?: string; |
||||
repeatDirection?: RepeatDirection; |
||||
maxPerRow?: number; |
||||
isDirty?: boolean; |
||||
} |
||||
|
||||
export enum DisplayMode { |
||||
Fill = 0, |
||||
Fit = 1, |
||||
Exact = 2, |
||||
} |
||||
|
||||
// VizPanelManager serves as an API to manipulate VizPanel state from the outside. It allows panel type, options and data manipulation.
|
||||
export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> { |
||||
private _cachedPluginOptions: Record< |
||||
string, |
||||
{ options: DeepPartial<{}>; fieldConfig: FieldConfigSource<DeepPartial<{}>> } | undefined |
||||
> = {}; |
||||
|
||||
public constructor(state: VizPanelManagerState) { |
||||
super(state); |
||||
this.addActivationHandler(() => this._onActivate()); |
||||
} |
||||
|
||||
/** |
||||
* Will clone the source panel and move the data provider to |
||||
* live on the VizPanelManager level instead of the VizPanel level |
||||
*/ |
||||
public static createFor(sourcePanel: VizPanel) { |
||||
let repeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {}; |
||||
|
||||
const gridItem = sourcePanel.parent; |
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) { |
||||
console.error('VizPanel is not a child of a dashboard grid item'); |
||||
throw new Error('VizPanel is not a child of a dashboard grid item'); |
||||
} |
||||
|
||||
const { variableName: repeat, repeatDirection, maxPerRow } = gridItem.state; |
||||
repeatOptions = { repeat, repeatDirection, maxPerRow }; |
||||
|
||||
let variables: SceneVariables | undefined; |
||||
|
||||
if (gridItem.parent?.state.$variables) { |
||||
variables = gridItem.parent.state.$variables.clone(); |
||||
} |
||||
|
||||
if (repeatOptions.repeat) { |
||||
const variable = sceneGraph.lookupVariable(repeatOptions.repeat, gridItem); |
||||
|
||||
if (variable instanceof MultiValueVariable && variable.state.value.length) { |
||||
const { values, texts } = getMultiVariableValues(variable); |
||||
|
||||
const varWithDefaultValue = new LocalValueVariable({ |
||||
name: variable.state.name, |
||||
value: values[0], |
||||
text: String(texts[0]), |
||||
}); |
||||
|
||||
if (!variables) { |
||||
variables = new SceneVariableSet({ |
||||
variables: [varWithDefaultValue], |
||||
}); |
||||
} else { |
||||
variables.setState({ variables: [varWithDefaultValue] }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return new VizPanelManager({ |
||||
$variables: variables, |
||||
panel: sourcePanel.clone(), |
||||
sourcePanel: sourcePanel.getRef(), |
||||
pluginId: sourcePanel.state.pluginId, |
||||
...repeatOptions, |
||||
}); |
||||
} |
||||
|
||||
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().clone({ $behaviors: undefined })), |
||||
vizPanelToPanel(this.state.panel.clone({ $behaviors: undefined })) |
||||
); |
||||
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; |
||||
|
||||
if (!dataObj) { |
||||
return; |
||||
} |
||||
|
||||
let datasourceToLoad = this.queryRunner.state.datasource; |
||||
|
||||
try { |
||||
let datasource: DataSourceApi | undefined; |
||||
let dsSettings: DataSourceInstanceSettings | undefined; |
||||
|
||||
if (!datasourceToLoad) { |
||||
const dashboardScene = getDashboardSceneFor(this); |
||||
const dashboardUid = dashboardScene.state.uid ?? ''; |
||||
const lastUsedDatasource = getLastUsedDatasourceFromStorage(dashboardUid!); |
||||
|
||||
// do we have a last used datasource for this dashboard
|
||||
if (lastUsedDatasource?.datasourceUid !== null) { |
||||
// get datasource from dashbopard uid
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid }); |
||||
if (dsSettings) { |
||||
datasource = await getDataSourceSrv().get({ |
||||
uid: lastUsedDatasource?.datasourceUid, |
||||
type: dsSettings.type, |
||||
}); |
||||
|
||||
this.queryRunner.setState({ |
||||
datasource: { |
||||
...getDataSourceRef(dsSettings), |
||||
uid: lastUsedDatasource?.datasourceUid, |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
} else { |
||||
datasource = await getDataSourceSrv().get(datasourceToLoad); |
||||
dsSettings = getDataSourceSrv().getInstanceSettings(datasourceToLoad); |
||||
} |
||||
|
||||
if (datasource && dsSettings) { |
||||
this.setState({ datasource, dsSettings }); |
||||
|
||||
storeLastUsedDataSourceInLocalStorage(getDataSourceRef(dsSettings) || { default: true }); |
||||
} |
||||
} catch (err) { |
||||
//set default datasource if we fail to load the datasource
|
||||
const datasource = await getDataSourceSrv().get(config.defaultDatasource); |
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(config.defaultDatasource); |
||||
|
||||
if (datasource && dsSettings) { |
||||
this.setState({ |
||||
datasource, |
||||
dsSettings, |
||||
}); |
||||
|
||||
this.queryRunner.setState({ |
||||
datasource: getDataSourceRef(dsSettings), |
||||
}); |
||||
} |
||||
|
||||
console.error(err); |
||||
} |
||||
} |
||||
|
||||
public changePluginType(pluginId: string) { |
||||
const { options: prevOptions, fieldConfig: prevFieldConfig, pluginId: prevPluginId } = this.state.panel.state; |
||||
|
||||
// clear custom options
|
||||
let newFieldConfig: FieldConfigSource = { |
||||
defaults: { |
||||
...prevFieldConfig.defaults, |
||||
custom: {}, |
||||
}, |
||||
overrides: filterFieldConfigOverrides(prevFieldConfig.overrides, isStandardFieldProp), |
||||
}; |
||||
|
||||
this._cachedPluginOptions[prevPluginId] = { options: prevOptions, fieldConfig: prevFieldConfig }; |
||||
|
||||
const cachedOptions = this._cachedPluginOptions[pluginId]?.options; |
||||
const cachedFieldConfig = this._cachedPluginOptions[pluginId]?.fieldConfig; |
||||
|
||||
if (cachedFieldConfig) { |
||||
newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig); |
||||
} |
||||
|
||||
// When changing from non-data to data panel, we need to add a new data provider
|
||||
if (!this.state.panel.state.$data && !config.panels[pluginId].skipDataQuery) { |
||||
let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid; |
||||
|
||||
if (!ds) { |
||||
ds = config.defaultDatasource; |
||||
} |
||||
|
||||
this.state.panel.setState({ |
||||
$data: new SceneDataTransformer({ |
||||
$data: new SceneQueryRunner({ |
||||
datasource: { |
||||
uid: ds, |
||||
}, |
||||
queries: [{ refId: 'A' }], |
||||
}), |
||||
transformations: [], |
||||
}), |
||||
}); |
||||
} |
||||
|
||||
this.setState({ pluginId }); |
||||
this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig); |
||||
|
||||
this.loadDataSource(); |
||||
} |
||||
|
||||
public async changePanelDataSource( |
||||
newSettings: DataSourceInstanceSettings, |
||||
defaultQueries?: DataQuery[] | GrafanaQuery[] |
||||
) { |
||||
const { dsSettings } = this.state; |
||||
const queryRunner = this.queryRunner; |
||||
|
||||
const currentDS = dsSettings ? await getDataSourceSrv().get({ uid: dsSettings.uid }) : undefined; |
||||
const nextDS = await getDataSourceSrv().get({ uid: newSettings.uid }); |
||||
|
||||
const currentQueries = queryRunner.state.queries; |
||||
|
||||
// We need to pass in newSettings.uid as well here as that can be a variable expression and we want to store that in the query model not the current ds variable value
|
||||
const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS)); |
||||
|
||||
queryRunner.setState({ |
||||
datasource: getDataSourceRef(newSettings), |
||||
queries, |
||||
}); |
||||
if (defaultQueries) { |
||||
queryRunner.runQueries(); |
||||
} |
||||
|
||||
this.loadDataSource(); |
||||
} |
||||
|
||||
public changeQueryOptions(options: QueryGroupOptions) { |
||||
const panelObj = this.state.panel; |
||||
const dataObj = this.queryRunner; |
||||
const timeRangeObj = panelObj.state.$timeRange; |
||||
|
||||
const dataObjStateUpdate: Partial<SceneQueryRunner['state']> = {}; |
||||
const timeRangeObjStateUpdate: Partial<PanelTimeRangeState> = {}; |
||||
|
||||
if (options.maxDataPoints !== dataObj.state.maxDataPoints) { |
||||
dataObjStateUpdate.maxDataPoints = options.maxDataPoints ?? undefined; |
||||
} |
||||
|
||||
if (options.minInterval !== dataObj.state.minInterval && options.minInterval !== null) { |
||||
dataObjStateUpdate.minInterval = options.minInterval; |
||||
} |
||||
|
||||
if (options.timeRange) { |
||||
timeRangeObjStateUpdate.timeFrom = options.timeRange.from ?? undefined; |
||||
timeRangeObjStateUpdate.timeShift = options.timeRange.shift ?? undefined; |
||||
timeRangeObjStateUpdate.hideTimeOverride = options.timeRange.hide; |
||||
} |
||||
|
||||
if (timeRangeObj instanceof PanelTimeRange) { |
||||
if (timeRangeObjStateUpdate.timeFrom !== undefined || timeRangeObjStateUpdate.timeShift !== undefined) { |
||||
// update time override
|
||||
timeRangeObj.setState(timeRangeObjStateUpdate); |
||||
} else { |
||||
// remove time override
|
||||
panelObj.setState({ $timeRange: undefined }); |
||||
} |
||||
} else { |
||||
// no time override present on the panel, let's create one first
|
||||
panelObj.setState({ $timeRange: new PanelTimeRange(timeRangeObjStateUpdate) }); |
||||
} |
||||
|
||||
if (options.cacheTimeout !== dataObj?.state.cacheTimeout) { |
||||
dataObjStateUpdate.cacheTimeout = options.cacheTimeout; |
||||
} |
||||
|
||||
if (options.queryCachingTTL !== dataObj?.state.queryCachingTTL) { |
||||
dataObjStateUpdate.queryCachingTTL = options.queryCachingTTL; |
||||
} |
||||
|
||||
dataObj.setState(dataObjStateUpdate); |
||||
dataObj.runQueries(); |
||||
} |
||||
|
||||
public changeQueries<T extends DataQuery>(queries: T[]) { |
||||
const runner = this.queryRunner; |
||||
runner.setState({ queries }); |
||||
} |
||||
|
||||
public changeTransformations(transformations: DataTransformerConfig[]) { |
||||
const dataprovider = this.dataTransformer; |
||||
dataprovider.setState({ transformations }); |
||||
dataprovider.reprocessTransformations(); |
||||
} |
||||
|
||||
public inspectPanel() { |
||||
const panel = this.state.panel; |
||||
const panelId = getPanelIdForVizPanel(panel); |
||||
|
||||
locationService.partial({ |
||||
inspect: panelId, |
||||
inspectTab: 'query', |
||||
}); |
||||
} |
||||
|
||||
get queryRunner(): SceneQueryRunner { |
||||
// Panel data object is always SceneQueryRunner wrapped in a SceneDataTransformer
|
||||
const runner = getQueryRunnerFor(this.state.panel); |
||||
|
||||
if (!runner) { |
||||
throw new Error('Query runner not found'); |
||||
} |
||||
|
||||
return runner; |
||||
} |
||||
|
||||
get dataTransformer(): SceneDataTransformer { |
||||
const provider = this.state.panel.state.$data; |
||||
if (!provider || !(provider instanceof SceneDataTransformer)) { |
||||
throw new Error('Could not find SceneDataTransformer for panel'); |
||||
} |
||||
return provider; |
||||
} |
||||
|
||||
public toggleTableView() { |
||||
if (this.state.tableView) { |
||||
this.setState({ tableView: undefined }); |
||||
return; |
||||
} |
||||
|
||||
this.setState({ |
||||
tableView: PanelBuilders.table() |
||||
.setTitle('') |
||||
.setOption('showTypeIcons', true) |
||||
.setOption('showHeader', true) |
||||
// Here we are breaking a scene rule and changing the parent of the main panel data provider
|
||||
// But we need to share this same instance as the queries tab is subscribing to it
|
||||
.setData(this.dataTransformer) |
||||
.build(), |
||||
}); |
||||
} |
||||
|
||||
public unlinkLibraryPanel() { |
||||
const sourcePanel = this.state.sourcePanel.resolve(); |
||||
if (!isLibraryPanel(sourcePanel)) { |
||||
throw new Error('VizPanel is not a library panel'); |
||||
} |
||||
|
||||
const gridItem = sourcePanel.parent; |
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) { |
||||
throw new Error('Library panel not a child of a grid item'); |
||||
} |
||||
|
||||
const newSourcePanel = this.state.panel.clone({ $data: sourcePanel.state.$data?.clone(), $behaviors: undefined }); |
||||
gridItem.setState({ |
||||
body: newSourcePanel, |
||||
}); |
||||
|
||||
this.state.panel.setState({ $behaviors: undefined }); |
||||
this.setState({ sourcePanel: newSourcePanel.getRef() }); |
||||
} |
||||
|
||||
public commitChanges() { |
||||
const sourcePanel = this.state.sourcePanel.resolve(); |
||||
this.commitChangesTo(sourcePanel); |
||||
} |
||||
|
||||
public commitChangesTo(sourcePanel: VizPanel) { |
||||
const repeatUpdate = { |
||||
variableName: this.state.repeat, |
||||
repeatDirection: this.state.repeatDirection, |
||||
maxPerRow: this.state.maxPerRow, |
||||
}; |
||||
|
||||
const vizPanel = this.state.panel.clone(); |
||||
|
||||
if (sourcePanel.parent instanceof DashboardGridItem) { |
||||
sourcePanel.parent.setState({ |
||||
...repeatUpdate, |
||||
body: vizPanel, |
||||
}); |
||||
} |
||||
|
||||
if (isLibraryPanel(vizPanel)) { |
||||
saveLibPanel(vizPanel); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Used from inspect json tab to view the current persisted model |
||||
*/ |
||||
public getPanelSaveModel(): Panel | object { |
||||
const sourcePanel = this.state.sourcePanel.resolve(); |
||||
const gridItem = sourcePanel.parent; |
||||
|
||||
if (!(gridItem instanceof DashboardGridItem)) { |
||||
return { error: 'Unsupported panel parent' }; |
||||
} |
||||
|
||||
const parentClone = gridItem.clone({ |
||||
body: this.state.panel.clone(), |
||||
}); |
||||
|
||||
return gridItemToPanel(parentClone); |
||||
} |
||||
|
||||
public setPanelTitle(newTitle: string) { |
||||
this.state.panel.setState({ title: newTitle, hoverHeader: newTitle === '' }); |
||||
} |
||||
|
||||
public static Component = ({ model }: SceneComponentProps<VizPanelManager>) => { |
||||
const { panel, tableView } = model.useState(); |
||||
const styles = useStyles2(getStyles); |
||||
const panelToShow = tableView ?? panel; |
||||
const dataProvider = panelToShow.state.$data; |
||||
|
||||
// This is to preserve SceneQueryRunner stays alive when switching between visualizations and table view
|
||||
useEffect(() => { |
||||
return dataProvider?.activate(); |
||||
}, [dataProvider]); |
||||
|
||||
return ( |
||||
<> |
||||
<div className={styles.wrapper}>{<panelToShow.Component model={panelToShow} />}</div> |
||||
</> |
||||
); |
||||
}; |
||||
} |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
wrapper: css({ |
||||
height: '100%', |
||||
width: '100%', |
||||
paddingLeft: theme.spacing(2), |
||||
}), |
||||
}; |
||||
} |
Loading…
Reference in new issue