Move panel plugin change logic into scenes (#90535)
* wip * move logic into vizPanel * add tests * bump scenes * set pluginId synchronously for component rerenders in table view * fix bugs around table view * fix * fix * use panelPlugin meta straight from config * fix * fix * fix
This commit is contained in:
@@ -9,10 +9,12 @@ import {
|
||||
SceneObjectState,
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Container, CustomScrollbar, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { shouldShowAlertingTab } from 'app/features/dashboard/components/PanelEditor/state/selectors';
|
||||
import { config, getConfig } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { VizPanelManager } from '../VizPanelManager';
|
||||
|
||||
@@ -29,7 +31,6 @@ export interface PanelDataPaneState extends SceneObjectState {
|
||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
static Component = PanelDataPaneRendered;
|
||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
|
||||
private _initialTabsBuilt = false;
|
||||
private panelSubscription: Unsubscribable | undefined;
|
||||
public panelManager: VizPanelManager;
|
||||
|
||||
@@ -59,16 +60,13 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
}
|
||||
|
||||
private onActivate() {
|
||||
const panel = this.panelManager.state.panel;
|
||||
this.setupPanelSubscription(panel);
|
||||
this.buildTabs();
|
||||
|
||||
this._subs.add(
|
||||
// Setup subscription for the case when panel type changed
|
||||
this.panelManager.subscribeToState((n, p) => {
|
||||
if (n.panel !== p.panel) {
|
||||
if (n.pluginId !== p.pluginId) {
|
||||
this.buildTabs();
|
||||
this.setupPanelSubscription(n.panel);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -81,35 +79,16 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
};
|
||||
}
|
||||
|
||||
private setupPanelSubscription(panel: VizPanel) {
|
||||
if (this.panelSubscription) {
|
||||
this._initialTabsBuilt = false;
|
||||
this.panelSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this.panelSubscription = panel.subscribeToState(() => {
|
||||
if (panel.getPlugin() && !this._initialTabsBuilt) {
|
||||
this.buildTabs();
|
||||
this._initialTabsBuilt = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildTabs() {
|
||||
const panelManager = this.panelManager;
|
||||
const panel = panelManager.state.panel;
|
||||
const pluginId = panelManager.state.pluginId;
|
||||
|
||||
const runner = this.panelManager.queryRunner;
|
||||
const tabs: PanelDataPaneTab[] = [];
|
||||
|
||||
if (panel) {
|
||||
const plugin = panel.getPlugin();
|
||||
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (plugin.meta.skipDataQuery) {
|
||||
if (config.panels[pluginId]?.skipDataQuery) {
|
||||
this.setState({ tabs });
|
||||
return;
|
||||
} else {
|
||||
@@ -119,7 +98,7 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
|
||||
tabs.push(new PanelDataTransformationsTab(this.panelManager));
|
||||
|
||||
if (shouldShowAlertingTab(plugin)) {
|
||||
if (shouldShowAlertingTab(panelManager.state.pluginId)) {
|
||||
tabs.push(new PanelDataAlertingTab(this.panelManager));
|
||||
}
|
||||
}
|
||||
@@ -137,7 +116,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const { tab, tabs } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!tabs) {
|
||||
if (!tabs || !tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,6 +144,20 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldShowAlertingTab(pluginId: string) {
|
||||
const { unifiedAlertingEnabled = false } = getConfig();
|
||||
const hasRuleReadPermissions = contextSrv.hasPermission(getRulesPermissions(GRAFANA_RULES_SOURCE_NAME).read);
|
||||
const isAlertingAvailable = unifiedAlertingEnabled && hasRuleReadPermissions;
|
||||
if (!isAlertingAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isGraph = pluginId === 'graph';
|
||||
const isTimeseries = pluginId === 'timeseries';
|
||||
|
||||
return isGraph || isTimeseries;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
dataPane: css({
|
||||
|
||||
@@ -49,8 +49,8 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
|
||||
this._subs.add(
|
||||
panelManager.subscribeToState((n, p) => {
|
||||
if (n.panel.state.pluginId !== p.panel.state.pluginId) {
|
||||
this._initDataPane(n.panel.state.pluginId);
|
||||
if (n.pluginId !== p.pluginId) {
|
||||
this._initDataPane(n.pluginId);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
||||
const { isVizPickerOpen, searchQuery, listMode } = model.useState();
|
||||
const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager;
|
||||
const { pluginId } = vizManager.state.panel.useState();
|
||||
const { pluginId } = vizManager.useState();
|
||||
const { data } = sceneGraph.getData(vizManager.state.panel).useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
||||
@@ -4,14 +4,7 @@ import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingSta
|
||||
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 {
|
||||
LocalValueVariable,
|
||||
SceneGridRow,
|
||||
SceneQueryRunner,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { 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';
|
||||
@@ -193,93 +186,80 @@ jest.useFakeTimers();
|
||||
|
||||
describe('VizPanelManager', () => {
|
||||
describe('When changing plugin', () => {
|
||||
it('Should successfully change from one viz type to another', () => {
|
||||
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.state.panel.state.pluginId).toBe('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 clear custom options', () => {
|
||||
const overrides = [
|
||||
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: 'matcherOne' },
|
||||
properties: [{ id: 'custom.propertyOne' }, { id: 'custom.propertyTwo' }, { id: 'standardProperty' }],
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({
|
||||
key: 'data-query-runner',
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
},
|
||||
queries: [{ refId: 'A' }],
|
||||
}),
|
||||
options: undefined,
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: 'Custom',
|
||||
},
|
||||
overrides,
|
||||
},
|
||||
vizPanelManager.state.panel.setState({
|
||||
fieldConfig: fieldConfig,
|
||||
});
|
||||
|
||||
new DashboardGridItem({
|
||||
body: vizPanel,
|
||||
});
|
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(vizPanel);
|
||||
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides).toBe(overrides);
|
||||
|
||||
vizPanelManager.changePluginType('timeseries');
|
||||
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toStrictEqual({});
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides[0].properties).toHaveLength(1);
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.overrides[0].properties[0].id).toBe('standardProperty');
|
||||
});
|
||||
|
||||
it('Should restore cached options/fieldConfig if they exist', () => {
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({
|
||||
key: 'data-query-runner',
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
},
|
||||
queries: [{ refId: 'A' }],
|
||||
}),
|
||||
options: {
|
||||
customOption: 'A',
|
||||
},
|
||||
fieldConfig: { defaults: { custom: 'Custom' }, overrides: [] },
|
||||
});
|
||||
|
||||
new DashboardGridItem({
|
||||
body: vizPanel,
|
||||
});
|
||||
|
||||
const vizPanelManager = VizPanelManager.createFor(vizPanel);
|
||||
|
||||
vizPanelManager.changePluginType('timeseries');
|
||||
//@ts-ignore
|
||||
expect(vizPanelManager.state.panel.state.options['customOption']).toBeUndefined();
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toStrictEqual({});
|
||||
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');
|
||||
|
||||
//@ts-ignore
|
||||
expect(vizPanelManager.state.panel.state.options['customOption']).toBe('A');
|
||||
expect(vizPanelManager.state.panel.state.fieldConfig.defaults.custom).toBe('Custom');
|
||||
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({});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
DataSourceInstanceSettings,
|
||||
FieldConfigSource,
|
||||
GrafanaTheme2,
|
||||
PanelModel,
|
||||
filterFieldConfigOverrides,
|
||||
getDataSourceRef,
|
||||
isStandardFieldProp,
|
||||
@@ -26,11 +25,9 @@ import {
|
||||
SceneQueryRunner,
|
||||
SceneVariables,
|
||||
VizPanel,
|
||||
sceneUtils,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery, DataTransformerConfig, Panel } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { getPluginVersion } from 'app/features/dashboard/state/PanelModel';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
import { updateLibraryVizPanel } from 'app/features/library-panels/state/api';
|
||||
@@ -49,6 +46,7 @@ import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '
|
||||
export interface VizPanelManagerState extends SceneObjectState {
|
||||
panel: VizPanel;
|
||||
sourcePanel: SceneObjectRef<VizPanel>;
|
||||
pluginId: string;
|
||||
datasource?: DataSourceApi;
|
||||
dsSettings?: DataSourceInstanceSettings;
|
||||
tableView?: VizPanel;
|
||||
@@ -103,6 +101,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
$variables: variables,
|
||||
panel: sourcePanel.clone(),
|
||||
sourcePanel: sourcePanel.getRef(),
|
||||
pluginId: sourcePanel.state.pluginId,
|
||||
...repeatOptions,
|
||||
});
|
||||
}
|
||||
@@ -197,12 +196,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
}
|
||||
|
||||
public changePluginType(pluginId: string) {
|
||||
const {
|
||||
options: prevOptions,
|
||||
fieldConfig: prevFieldConfig,
|
||||
pluginId: prevPluginId,
|
||||
...restOfOldState
|
||||
} = sceneUtils.cloneSceneObjectState(this.state.panel.state);
|
||||
const { options: prevOptions, fieldConfig: prevFieldConfig, pluginId: prevPluginId } = this.state.panel.state;
|
||||
|
||||
// clear custom options
|
||||
let newFieldConfig: FieldConfigSource = {
|
||||
@@ -222,13 +216,6 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig);
|
||||
}
|
||||
|
||||
const newPanel = new VizPanel({
|
||||
options: cachedOptions ?? {},
|
||||
fieldConfig: newFieldConfig,
|
||||
pluginId: pluginId,
|
||||
...restOfOldState,
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -237,7 +224,7 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
ds = config.defaultDatasource;
|
||||
}
|
||||
|
||||
newPanel.setState({
|
||||
this.state.panel.setState({
|
||||
$data: new SceneDataTransformer({
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: {
|
||||
@@ -250,26 +237,12 @@ export class VizPanelManager extends SceneObjectBase<VizPanelManagerState> {
|
||||
});
|
||||
}
|
||||
|
||||
const newPlugin = newPanel.getPlugin();
|
||||
const panel: PanelModel = {
|
||||
title: newPanel.state.title,
|
||||
options: newPanel.state.options,
|
||||
fieldConfig: newPanel.state.fieldConfig,
|
||||
id: 1,
|
||||
type: pluginId,
|
||||
};
|
||||
this.setState({
|
||||
pluginId,
|
||||
});
|
||||
|
||||
const newOptions = newPlugin?.onPanelTypeChanged?.(panel, prevPluginId, prevOptions, prevFieldConfig);
|
||||
this.state.panel.changePluginType(pluginId, cachedOptions, newFieldConfig);
|
||||
|
||||
if (newOptions) {
|
||||
newPanel.onOptionsChange(newOptions, true, true);
|
||||
}
|
||||
|
||||
if (newPlugin?.onPanelMigration) {
|
||||
newPanel.setState({ pluginVersion: getPluginVersion(newPlugin) });
|
||||
}
|
||||
|
||||
this.setState({ panel: newPanel });
|
||||
this.loadDataSource();
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ describe('DashboardDatasourceBehaviour', () => {
|
||||
panel: dashboardDSPanel.clone(),
|
||||
$data: dashboardDSPanel.state.$data?.clone(),
|
||||
sourcePanel: dashboardDSPanel.getRef(),
|
||||
pluginId: dashboardDSPanel.state.pluginId,
|
||||
});
|
||||
|
||||
vizPanelManager.activate();
|
||||
|
||||
Reference in New Issue
Block a user