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