From a385ae4fa51a99b526880b1d3eb9b3802f052306 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 7 Feb 2024 00:51:25 -0800 Subject: [PATCH] DashboardScene: Do not show data pane for panels with `skipDataQuery` (#81934) * Bump scenes * Don't show data pane for panels without data support * Add tests * Review --- .../PanelDataPane/PanelDataPane.tsx | 3 +- .../panel-edit/PanelEditor.test.ts | 95 +++++++++++++++++++ .../panel-edit/PanelEditor.tsx | 48 +++++++++- .../panel-edit/VizPanelManager.tsx | 27 +++++- yarn.lock | 6 +- 5 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index 834ae36e225..2175d6eba39 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -51,6 +51,7 @@ export class PanelDataPane extends SceneObjectBase { constructor(panelMgr: VizPanelManager) { super({ tab: TabId.Queries, + tabs: [], }); this.panelManager = panelMgr; @@ -97,6 +98,7 @@ export class PanelDataPane extends SceneObjectBase { private buildTabs() { const panelManager = this.panelManager; const panel = panelManager.state.panel; + const runner = this.panelManager.queryRunner; const tabs: PanelDataPaneTab[] = []; @@ -104,7 +106,6 @@ export class PanelDataPane extends SceneObjectBase { const plugin = panel.getPlugin(); if (!plugin) { - this.setState({ tabs }); return; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts new file mode 100644 index 00000000000..fef0c30dc0e --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -0,0 +1,95 @@ +import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data'; +import { SceneFlexItem, SplitLayout, VizPanel } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { activateFullSceneTree } from '../utils/test-utils'; + +import { PanelDataPane } from './PanelDataPane/PanelDataPane'; +import { buildPanelEditScene } from './PanelEditor'; + +let pluginToLoad: PanelPlugin | undefined; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getPluginImportUtils: () => ({ + getPanelPluginFromCache: jest.fn(() => pluginToLoad), + }), + config: { + panels: { + text: { + skipDataQuery: true, + }, + timeseries: { + skipDataQuery: false, + }, + }, + }, +})); + +describe('PanelEditor', () => { + describe('PanelDataPane', () => { + it('should not exist if panel is skipDataQuery', () => { + pluginToLoad = getTestPanelPlugin({ id: 'text', skipDataQuery: true }); + + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + }); + const editScene = buildPanelEditScene(panel); + const scene = new DashboardScene({ + editPanel: editScene, + }); + + activateFullSceneTree(scene); + + expect(((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary).toBeUndefined(); + }); + + it('should exist if panel is supporting querying', () => { + pluginToLoad = getTestPanelPlugin({ id: 'timeseries' }); + + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'timeseries', + }); + const editScene = buildPanelEditScene(panel); + const scene = new DashboardScene({ + editPanel: editScene, + }); + + activateFullSceneTree(scene); + const secondaryPane = ((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary; + + expect(secondaryPane).toBeInstanceOf(SceneFlexItem); + expect((secondaryPane as SceneFlexItem).state.body).toBeInstanceOf(PanelDataPane); + }); + }); +}); + +export function getTestPanelPlugin(options: Partial): PanelPlugin { + const plugin = new PanelPlugin(() => null); + plugin.meta = { + id: options.id!, + type: PluginType.panel, + name: options.id!, + sort: options.sort || 1, + info: { + author: { + name: options.id + 'name', + }, + description: '', + links: [], + logos: { + large: '', + small: '', + }, + screenshots: [], + updated: '', + version: '1.0.', + }, + hideFromList: options.hideFromList === true, + module: options.module ?? '', + baseUrl: '', + skipDataQuery: options.skipDataQuery ?? false, + }; + return plugin; +} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 640b38f91e9..5d4632fc58e 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -1,7 +1,7 @@ import * as H from 'history'; import { NavIndex } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { SceneFlexItem, SceneFlexLayout, @@ -118,14 +118,12 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor { direction: 'row', primary: new SplitLayout({ direction: 'column', + $behaviors: [conditionalDataPaneBehavior], primary: new SceneFlexLayout({ direction: 'column', minHeight: 200, children: [vizPanelMgr], }), - secondary: new SceneFlexItem({ - body: new PanelDataPane(vizPanelMgr), - }), primaryPaneStyles: { minHeight: 0, overflow: 'hidden', @@ -147,3 +145,45 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor { }), }); } + +// This function is used to conditionally add the data pane to the panel editor, +// depending on the type of a panel being edited. +function conditionalDataPaneBehavior(scene: SplitLayout) { + const dashboard = getDashboardSceneFor(scene); + + const editor = dashboard.state.editPanel; + + if (!editor) { + return; + } + + const panelManager = editor.state.panelRef.resolve(); + const panel = panelManager.state.panel; + + const getDataPane = () => + new SceneFlexItem({ + body: new PanelDataPane(panelManager), + }); + + if (!config.panels[panel.state.pluginId].skipDataQuery) { + scene.setState({ + secondary: getDataPane(), + }); + } + + const sub = panelManager.subscribeToState((n, p) => { + const hadDataSupport = !config.panels[p.panel.state.pluginId].skipDataQuery; + const willHaveDataSupport = !config.panels[n.panel.state.pluginId].skipDataQuery; + + if (hadDataSupport && !willHaveDataSupport) { + locationService.partial({ tab: null }, true); + scene.setState({ secondary: undefined }); + } else if (!hadDataSupport && willHaveDataSupport) { + scene.setState({ secondary: getDataPane() }); + } + }); + + return () => { + sub.unsubscribe(); + }; +} diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 1be3094d4d2..c396c0bddf3 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -9,7 +9,7 @@ import { isStandardFieldProp, restoreCustomOverrideRules, } from '@grafana/data'; -import { getDataSourceSrv, locationService } from '@grafana/runtime'; +import { config, getDataSourceSrv, locationService } from '@grafana/runtime'; import { SceneObjectState, VizPanel, @@ -24,13 +24,14 @@ import { } from '@grafana/scenes'; import { DataQuery, DataTransformerConfig } from '@grafana/schema'; 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 { updateQueries } from 'app/features/query/state/updateQueries'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; -import { getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; interface VizPanelManagerState extends SceneObjectState { panel: VizPanel; @@ -128,6 +129,27 @@ export class VizPanelManager extends SceneObjectBase { ...restOfOldState, }); + // When changing from non-data to data panel, we need to add a new data provider + if (!restOfOldState.$data && !config.panels[pluginType].skipDataQuery) { + let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid; + + if (!ds) { + ds = config.defaultDatasource; + } + + newPanel.setState({ + $data: new SceneDataTransformer({ + $data: new SceneQueryRunner({ + datasource: { + uid: ds, + }, + queries: [{ refId: 'A' }], + }), + transformations: [], + }), + }); + } + const newPlugin = newPanel.getPlugin(); const panel: PanelModel = { title: newPanel.state.title, @@ -146,6 +168,7 @@ export class VizPanelManager extends SceneObjectBase { } this.setState({ panel: newPanel }); + this.loadDataSource(); } public async changePanelDataSource( diff --git a/yarn.lock b/yarn.lock index 350a1566621..9484f606cc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25531,11 +25531,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.50.1 - resolution: "react-hook-form@npm:7.50.1" + version: 7.50.0 + resolution: "react-hook-form@npm:7.50.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 10/54a9daa2143c601a9867e96a2159a0bbe98707b5bbeb5953bfdf3342d2f04bfcaa6169907ed167c5c1f3a044630860d4f43685f4ac4e15b9cd892d1b00d54dde + checksum: 10/3b85cc179053af72a2734f2e77767de8f9b3ecbefeee282b73e81141c4b7bb97308ec00da61fdc25a28299a2defb74bff66417bb85a66357f5ceddba7b697ae7 languageName: node linkType: hard