From 28fa2849dfa0659add5c1224a1be2963631478c1 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:34:21 -0700 Subject: [PATCH] Dashboard-Scene: View panel as table in edit mode (#83077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: working functionality * betterer * Fully working: Alerts show up, toggling table view doesn't update viz type in options pane * betterer * improve * betterer * Refactoring a bit * wrong step * move data provider to vizPanel * Update * update * More refactorings * Fix InspectJsonTab tests (except 1); remove obsolete PanelControls * Fixed test * Update * minor fix --------- Co-authored-by: Torkel Ödegaard --- .../inspect/InspectJsonTab.test.tsx | 80 +++++++---- .../inspect/InspectJsonTab.tsx | 17 ++- .../PanelDataAlertingTab.test.tsx | 4 +- .../PanelDataTransformationsTab.test.tsx | 2 +- .../panel-edit/PanelEditControls.tsx | 33 +++++ .../panel-edit/PanelEditor.tsx | 21 +-- .../panel-edit/PanelEditorRenderer.tsx | 7 - .../panel-edit/ShareDataProvider.tsx | 35 +++++ .../panel-edit/VizPanelManager.test.tsx | 13 +- .../panel-edit/VizPanelManager.tsx | 126 ++++++++++++++---- .../scene/DashboardControls.tsx | 2 + .../features/dashboard-scene/utils/utils.ts | 10 +- 12 files changed, 251 insertions(+), 99 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx create mode 100644 public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 0ba2851eaea..f155cb206fa 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -1,12 +1,21 @@ -import { FieldType, getDefaultTimeRange, LoadingState, standardTransformersRegistry, toDataFrame } from '@grafana/data'; +import { of } from 'rxjs'; + +import { + FieldType, + getDefaultTimeRange, + LoadingState, + PanelData, + standardTransformersRegistry, + toDataFrame, +} from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneCanvasText, - SceneDataNode, SceneDataTransformer, SceneGridItem, SceneGridLayout, + SceneQueryRunner, VizPanel, } from '@grafana/scenes'; import * as libpanels from 'app/features/library-panels/state/api'; @@ -34,8 +43,45 @@ jest.mock('@grafana/runtime', () => ({ getPluginLinkExtensions: jest.fn(() => ({ extensions: [], })), + getDataSourceSrv: () => { + return { + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds1' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }), + }; + }, })); +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + series: [ + toDataFrame({ + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], + }), + ], + request: { + app: 'dashboard', + requestId: 'request-id', + dashboardUID: 'asd', + interval: '1s', + panelId: 1, + range: getDefaultTimeRange(), + targets: [], + timezone: 'utc', + intervalMs: 1000, + startTime: 1, + scopedVars: { + __sceneObject: { value: new SceneCanvasText({ text: 'asd' }) }, + }, + }, + }) +); + +setRunRequest(runRequestMock); + describe('InspectJsonTab', () => { it('Can show panel json', async () => { const { tab } = await buildTestScene(); @@ -121,31 +167,9 @@ function buildTestPanel() { }, }, ], - $data: new SceneDataNode({ - data: { - state: LoadingState.Done, - series: [ - toDataFrame({ - fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3] }], - }), - ], - timeRange: getDefaultTimeRange(), - request: { - app: 'dashboard', - requestId: 'request-id', - dashboardUID: 'asd', - interval: '1s', - panelId: 1, - range: getDefaultTimeRange(), - targets: [], - timezone: 'utc', - intervalMs: 1000, - startTime: 1, - scopedVars: { - __sceneObject: { value: new SceneCanvasText({ text: 'asd' }) }, - }, - }, - }, + $data: new SceneQueryRunner({ + datasource: { uid: 'abcdef' }, + queries: [{ refId: 'A' }], }), }), }); diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index f8abf99f087..74725d1d6c5 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -26,6 +26,7 @@ import { InspectTab } from 'app/features/inspector/types'; import { getPrettyJSON } from 'app/features/inspector/utils/utils'; import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; +import { VizPanelManager } from '../panel-edit/VizPanelManager'; import { LibraryVizPanel } from '../scene/LibraryVizPanel'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; @@ -60,7 +61,7 @@ export class InspectJsonTab extends SceneObjectBase { public getOptions(): Array> { const panel = this.state.panelRef.resolve(); - const dataProvider = panel.state.$data; + const dataProvider = panel.state.$data ?? panel.parent?.state.$data; const options: Array> = [ { @@ -201,10 +202,14 @@ function getJsonText(show: ShowContent, panel: VizPanel): string { case 'panel-json': { reportPanelInspectInteraction(InspectTab.JSON, 'panelData'); - if (panel.parent instanceof SceneGridItem || panel.parent instanceof PanelRepeaterGridItem) { - objToStringify = gridItemToPanel(panel.parent); - } else if (panel.parent instanceof LibraryVizPanel) { + const parent = panel.parent!; + + if (parent instanceof SceneGridItem || parent instanceof PanelRepeaterGridItem) { + objToStringify = gridItemToPanel(parent); + } else if (parent instanceof LibraryVizPanel) { objToStringify = libraryPanelChildToLegacyRepresentation(panel); + } else if (parent instanceof VizPanelManager) { + objToStringify = parent.getPanelSaveModel(); } break; } @@ -246,18 +251,22 @@ function libraryPanelChildToLegacyRepresentation(panel: VizPanel<{}, {}>) { if (!(panel.parent instanceof LibraryVizPanel)) { throw 'Panel not child of LibraryVizPanel'; } + if (!(panel.parent.parent instanceof SceneGridItem)) { throw 'LibraryPanel not child of SceneGridItem'; } + const gridItem = panel.parent.parent; const libraryPanelObj = gridItemToPanel(gridItem); const panelObj = vizPanelToPanel(panel); + panelObj.gridPos = { x: gridItem.state.x || 0, y: gridItem.state.y || 0, h: gridItem.state.height || 0, w: gridItem.state.width || 0, }; + return { ...libraryPanelObj, ...panelObj }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index d79fdaee329..1786c59882b 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -349,8 +349,8 @@ async function clickNewButton() { function createModel(dashboard: DashboardModel) { const scene = createDashboardSceneFromDashboardModel(dashboard); - const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34)); - const model = new PanelDataAlertingTab(new VizPanelManager(vizPanel!.clone())); + const vizPanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(34))!; + const model = new PanelDataAlertingTab(VizPanelManager.createFor(vizPanel)); jest.spyOn(utils, 'getDashboardSceneFor').mockReturnValue(scene); return model; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx index 010b1b3b918..2b9766c54d1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx @@ -174,7 +174,7 @@ const setupVizPanelManger = (panelId: string) => { const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} }); const panel = findVizPanelByKey(scene, panelId)!; - const vizPanelManager = new VizPanelManager(panel.clone()); + 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 diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx new file mode 100644 index 00000000000..6a2478a0ac2 --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditControls.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { selectors } from '@grafana/e2e-selectors'; +import { config } from '@grafana/runtime'; +import { InlineSwitch } from '@grafana/ui'; + +import { PanelEditor } from './PanelEditor'; + +export interface Props { + panelEditor: PanelEditor; +} + +export function PanelEditControls({ panelEditor }: Props) { + const vizManager = panelEditor.state.vizManager; + const { panel, tableView } = vizManager.useState(); + const skipDataQuery = config.panels[panel.state.pluginId].skipDataQuery; + + return ( + <> + {!skipDataQuery && ( + vizManager.toggleTableView()} + aria-label="toggle-table-view" + data-testid={selectors.components.PanelEditor.toggleTableView} + /> + )} + + ); +} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 91840641603..3d0655a2d73 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -2,14 +2,9 @@ import * as H from 'history'; import { NavIndex } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; -import { SceneGridItem, SceneObject, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; -import { - findVizPanelByKey, - getDashboardSceneFor, - getPanelIdForVizPanel, - getVizPanelKeyForPanelId, -} from '../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils'; import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelEditorRenderer } from './PanelEditorRenderer'; @@ -17,7 +12,6 @@ import { PanelOptionsPane } from './PanelOptionsPane'; import { VizPanelManager } from './VizPanelManager'; export interface PanelEditorState extends SceneObjectState { - controls?: SceneObject[]; isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; @@ -32,7 +26,6 @@ export class PanelEditor extends SceneObjectBase { public constructor(state: PanelEditorState) { super(state); - this.addActivationHandler(this._activationHandler.bind(this)); } @@ -90,25 +83,19 @@ export class PanelEditor extends SceneObjectBase { public commitChanges() { const dashboard = getDashboardSceneFor(this); - const sourcePanel = findVizPanelByKey(dashboard.state.body, getVizPanelKeyForPanelId(this.state.panelId)); if (!dashboard.state.isEditing) { dashboard.onEnterEditMode(); } - if (sourcePanel!.parent instanceof SceneGridItem) { - sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() }); - } + this.state.vizManager.commitChanges(); } } export function buildPanelEditScene(panel: VizPanel): PanelEditor { - const panelClone = panel.clone(); - const vizPanelMgr = new VizPanelManager(panelClone); - return new PanelEditor({ panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), - vizManager: vizPanelMgr, + vizManager: VizPanelManager.createFor(panel), }); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index 2b81f59779d..9850f787517 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -127,13 +127,6 @@ function getStyles(theme: GrafanaTheme2) { minHeight: 0, gap: '8px', }), - controls: css({ - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - gap: theme.spacing(1), - padding: theme.spacing(2, 0, 2, 2), - }), optionsPane: css({ flexDirection: 'column', borderLeft: `1px solid ${theme.colors.border.weak}`, diff --git a/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx b/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx new file mode 100644 index 00000000000..dcf9ca08a9d --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/ShareDataProvider.tsx @@ -0,0 +1,35 @@ +import { SceneDataProvider, SceneDataState, SceneObjectBase } from '@grafana/scenes'; + +export class ShareDataProvider extends SceneObjectBase implements SceneDataProvider { + public constructor(private _source: SceneDataProvider) { + super(_source.state); + this.addActivationHandler(() => this.activationHandler()); + } + + private activationHandler() { + this._subs.add(this._source.subscribeToState((state) => this.setState({ data: state.data }))); + this.setState(this._source.state); + } + + public setContainerWidth(width: number) { + if (this.state.$data && this.state.$data.setContainerWidth) { + this.state.$data.setContainerWidth(width); + } + } + + public isDataReadyToDisplay() { + if (!this._source.isDataReadyToDisplay) { + return true; + } + + return this._source.isDataReadyToDisplay?.(); + } + + public cancelQuery() { + this._source.cancelQuery?.(); + } + + public getResultsStream() { + return this._source.getResultsStream!(); + } +} diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx index b71340323ba..a33670c822a 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.test.tsx @@ -2,7 +2,7 @@ import { map, of } from 'rxjs'; import { DataQueryRequest, DataSourceApi, DataSourceInstanceSettings, LoadingState, PanelData } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { SceneDataTransformer, SceneQueryRunner, VizPanel } from '@grafana/scenes'; +import { SceneQueryRunner, VizPanel } 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'; @@ -140,7 +140,7 @@ jest.mock('@grafana/runtime', () => ({ })); describe('VizPanelManager', () => { - describe('changePluginType', () => { + describe('When changing plugin', () => { it('Should successfully change from one viz type to another', () => { const { vizPanelManager } = setupTest('panel-1'); expect(vizPanelManager.state.panel.state.pluginId).toBe('timeseries'); @@ -169,7 +169,7 @@ describe('VizPanelManager', () => { }, }); - const vizPanelManager = new VizPanelManager(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); @@ -193,7 +193,7 @@ describe('VizPanelManager', () => { fieldConfig: { defaults: { custom: 'Custom' }, overrides: [] }, }); - const vizPanelManager = new VizPanelManager(vizPanel); + const vizPanelManager = VizPanelManager.createFor(vizPanel); vizPanelManager.changePluginType('timeseries'); //@ts-ignore @@ -599,7 +599,6 @@ describe('VizPanelManager', () => { }, ]); - expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer); expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id); // Changing dashboard query to a panel with queries only @@ -613,7 +612,6 @@ describe('VizPanelManager', () => { }, ]); - expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer); expect(vizPanelManager.queryRunner.state.queries[0].panelId).toBe(panelWithQueriesOnly.id); }); }); @@ -624,8 +622,7 @@ const setupTest = (panelId: string) => { const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} }); const panel = findVizPanelByKey(scene, panelId)!; - const vizPanelManager = new VizPanelManager(panel.clone()); - + 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)); diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx index 6a87c2c64c5..cc2919c4b49 100644 --- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx +++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx @@ -21,10 +21,12 @@ import { DeepPartial, SceneQueryRunner, sceneGraph, - SceneDataProvider, SceneDataTransformer, + PanelBuilders, + SceneGridItem, + SceneObjectRef, } from '@grafana/scenes'; -import { DataQuery, DataTransformerConfig } from '@grafana/schema'; +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'; @@ -34,12 +36,21 @@ import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; import { QueryGroupOptions } from 'app/types'; import { PanelTimeRange, PanelTimeRangeState } from '../scene/PanelTimeRange'; +import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; interface VizPanelManagerState extends SceneObjectState { panel: VizPanel; + sourcePanel: SceneObjectRef; datasource?: DataSourceApi; dsSettings?: DataSourceInstanceSettings; + tableView?: VizPanel; +} + +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. @@ -49,18 +60,29 @@ export class VizPanelManager extends SceneObjectBase { { options: DeepPartial<{}>; fieldConfig: FieldConfigSource> } | undefined > = {}; - public constructor(panel: VizPanel) { - super({ panel }); - + 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) { + return new VizPanelManager({ + panel: sourcePanel.clone({ $data: undefined }), + $data: sourcePanel.state.$data?.clone(), + sourcePanel: sourcePanel.getRef(), + }); + } + private _onActivate() { this.loadDataSource(); } private async loadDataSource() { - const dataObj = this.state.panel.state.$data; + const dataObj = this.state.$data; if (!dataObj) { return; @@ -96,7 +118,7 @@ export class VizPanelManager extends SceneObjectBase { } } - public changePluginType(pluginType: string) { + public changePluginType(pluginId: string) { const { options: prevOptions, fieldConfig: prevFieldConfig, @@ -105,16 +127,19 @@ export class VizPanelManager extends SceneObjectBase { } = sceneUtils.cloneSceneObjectState(this.state.panel.state); // clear custom options - let newFieldConfig = { ...prevFieldConfig }; - newFieldConfig.defaults = { - ...newFieldConfig.defaults, - custom: {}, + let newFieldConfig: FieldConfigSource = { + defaults: { + ...prevFieldConfig.defaults, + custom: {}, + }, + overrides: filterFieldConfigOverrides(prevFieldConfig.overrides, isStandardFieldProp), }; - newFieldConfig.overrides = filterFieldConfigOverrides(newFieldConfig.overrides, isStandardFieldProp); this._cachedPluginOptions[prevPluginId] = { options: prevOptions, fieldConfig: prevFieldConfig }; - const cachedOptions = this._cachedPluginOptions[pluginType]?.options; - const cachedFieldConfig = this._cachedPluginOptions[pluginType]?.fieldConfig; + + const cachedOptions = this._cachedPluginOptions[pluginId]?.options; + const cachedFieldConfig = this._cachedPluginOptions[pluginId]?.fieldConfig; + if (cachedFieldConfig) { newFieldConfig = restoreCustomOverrideRules(newFieldConfig, cachedFieldConfig); } @@ -122,19 +147,19 @@ export class VizPanelManager extends SceneObjectBase { const newPanel = new VizPanel({ options: cachedOptions ?? {}, fieldConfig: newFieldConfig, - pluginId: pluginType, + pluginId: pluginId, ...restOfOldState, }); // When changing from non-data to data panel, we need to add a new data provider - if (!restOfOldState.$data && !config.panels[pluginType].skipDataQuery) { + if (!this.state.$data && !config.panels[pluginId].skipDataQuery) { let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid; if (!ds) { ds = config.defaultDatasource; } - newPanel.setState({ + this.setState({ $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ datasource: { @@ -153,8 +178,9 @@ export class VizPanelManager extends SceneObjectBase { options: newPanel.state.options, fieldConfig: newPanel.state.fieldConfig, id: 1, - type: pluginType, + type: pluginId, }; + const newOptions = newPlugin?.onPanelTypeChanged?.(panel, prevPluginId, prevOptions, prevFieldConfig); if (newOptions) { newPanel.onOptionsChange(newOptions, true); @@ -208,14 +234,17 @@ export class VizPanelManager extends SceneObjectBase { 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 @@ -264,35 +293,76 @@ export class VizPanelManager extends SceneObjectBase { get queryRunner(): SceneQueryRunner { // Panel data object is always SceneQueryRunner wrapped in a SceneDataTransformer - const runner = getQueryRunnerFor(this.state.panel); + const runner = getQueryRunnerFor(this); if (!runner) { throw new Error('Query runner not found'); } + return runner; } get dataTransformer(): SceneDataTransformer { - const provider = this.state.panel.state.$data; + const provider = this.state.$data; if (!provider || !(provider instanceof SceneDataTransformer)) { throw new Error('Could not find SceneDataTransformer for panel'); } return provider; } - get panelData(): SceneDataProvider { - return this.state.panel.state.$data!; + public toggleTableView() { + if (this.state.tableView) { + this.setState({ tableView: undefined }); + return; + } + + this.setState({ + tableView: PanelBuilders.table() + .setTitle('') + .setOption('showTypeIcons', true) + .setOption('showHeader', true) + .build(), + }); + } + + public commitChanges() { + const sourcePanel = this.state.sourcePanel.resolve(); + + if (sourcePanel.parent instanceof SceneGridItem) { + sourcePanel.parent.setState({ + body: this.state.panel.clone({ + $data: this.state.$data?.clone(), + }), + }); + } + } + + /** + * Used from inspect json tab to view the current persisted model + */ + public getPanelSaveModel(): Panel | object { + const sourcePanel = this.state.sourcePanel.resolve(); + + if (sourcePanel.parent instanceof SceneGridItem) { + const parentClone = sourcePanel.parent.clone({ + body: this.state.panel.clone({ + $data: this.state.$data?.clone(), + }), + }); + + return gridItemToPanel(parentClone); + } + + return { error: 'Unsupported panel parent' }; } public static Component = ({ model }: SceneComponentProps) => { - const { panel } = model.useState(); + const { panel, tableView } = model.useState(); const styles = useStyles2(getStyles); - return ( -
- -
- ); + const panelToShow = tableView ?? panel; + + return
{}
; }; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 2758c65bcf4..55e5e370eba 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -13,6 +13,7 @@ import { } from '@grafana/scenes'; import { Box, Stack, useStyles2 } from '@grafana/ui'; +import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; @@ -51,6 +52,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps {!editPanel && } + {editPanel && } {!hideTimeControls && ( diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index d964c15ca16..286659e7bb8 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -155,12 +155,14 @@ export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQu return undefined; } - if (sceneObject.state.$data instanceof SceneQueryRunner) { - return sceneObject.state.$data; + const dataProvider = sceneObject.state.$data ?? sceneObject.parent?.state.$data; + + if (dataProvider instanceof SceneQueryRunner) { + return dataProvider; } - if (sceneObject.state.$data instanceof SceneDataTransformer) { - return getQueryRunnerFor(sceneObject.state.$data); + if (dataProvider instanceof SceneDataTransformer) { + return getQueryRunnerFor(dataProvider); } return undefined;