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:
Victor Marin
2024-07-22 20:26:52 +03:00
committed by GitHub
parent bf09d8f615
commit 771933bd74
6 changed files with 96 additions and 149 deletions
@@ -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();