From 5e9f252962a338f4f308d30ab412e7fccc1b685a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 14 Sep 2023 12:17:04 +0200 Subject: [PATCH] DashboardScene: Inspect / Json tab (#74701) * DashboardScene: Inspect / Json tab * Fixing behaviors and writing tests * Progress * limit options based on data provider * Fixes * Add tracking * Remove unused function * Remove unused function * Fix test * Update * Move utils function * Rename to source --- .betterer.results | 3 + .../inspect/InspectDataTab.tsx | 18 +- .../inspect/InspectJsonTab.test.tsx | 162 ++++++++++++ .../inspect/InspectJsonTab.tsx | 245 +++++++++++++++++- .../inspect/InspectStatsTab.tsx | 25 +- .../inspect/PanelInspectDrawer.tsx | 44 ++-- .../features/dashboard-scene/inspect/types.ts | 9 +- .../dashboard-scene/scene/DashboardScene.tsx | 21 +- .../transformSaveModelToScene.ts | 1 + .../features/dashboard-scene/utils/utils.ts | 36 ++- public/app/features/inspector/DetailText.tsx | 7 +- .../app/features/inspector/InspectJSONTab.tsx | 36 +-- public/app/features/inspector/utils/utils.ts | 37 +++ 13 files changed, 563 insertions(+), 81 deletions(-) create mode 100644 public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx create mode 100644 public/app/features/inspector/utils/utils.ts diff --git a/.betterer.results b/.betterer.results index 3f9a9419a91..b0eb03225f6 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1789,6 +1789,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [ + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] + ], "public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx index 0c964e7de06..23471199372 100644 --- a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx @@ -7,14 +7,18 @@ import { SceneDataTransformer, sceneGraph, SceneObjectBase, + SceneObjectRef, + SceneObjectState, + VizPanel, } from '@grafana/scenes'; +import { t } from 'app/core/internationalization'; +import { InspectTab } from 'app/features/inspector/types'; import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; import { InspectDataTab as InspectDataTabOld } from '../../inspector/InspectDataTab'; -import { InspectTabState } from './types'; - -export interface InspectDataTabState extends InspectTabState { +export interface InspectDataTabState extends SceneObjectState { + panelRef: SceneObjectRef; options: GetDataOptions; } @@ -29,6 +33,14 @@ export class InspectDataTab extends SceneObjectBase { }); } + public getTabLabel() { + return t('dashboard.inspect.data-tab', 'Data'); + } + + public getTabValue() { + return InspectTab.Data; + } + public onOptionsChange = (options: GetDataOptions) => { this.setState({ options }); }; diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx new file mode 100644 index 00000000000..cec2bed4924 --- /dev/null +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -0,0 +1,162 @@ +import { FieldType, getDefaultTimeRange, LoadingState, standardTransformersRegistry, toDataFrame } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + SceneCanvasText, + SceneDataNode, + SceneDataTransformer, + SceneGridItem, + SceneGridLayout, + SceneObjectRef, + VizPanel, +} from '@grafana/scenes'; +import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { activateFullSceneTree } from '../utils/test-utils'; +import { findVizPanelByKey } from '../utils/utils'; + +import { InspectJsonTab } from './InspectJsonTab'; + +standardTransformersRegistry.setInit(getStandardTransformers); + +setPluginImportUtils({ + importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (id: string) => undefined, +}); + +describe('InspectJsonTab', () => { + it('Can show panel json', async () => { + const { tab } = await buildTestScene(); + + const obj = JSON.parse(tab.state.jsonText); + expect(obj.gridPos).toEqual({ x: 0, y: 0, w: 10, h: 12 }); + expect(tab.isEditable()).toBe(true); + }); + + it('Can show panel data with field config', async () => { + const { tab } = await buildTestScene(); + tab.onChangeSource({ value: 'panel-data' }); + expect(tab.isEditable()).toBe(false); + + const obj = JSON.parse(tab.state.jsonText); + expect(obj.series.length).toBe(1); + expect(obj.state).toBe(LoadingState.Done); + + // verify scopedVars __sceneObject is filtered out + expect(obj.request.scopedVars.__sceneObject).toEqual('Filtered out in JSON serialization'); + }); + + it('Can show raw data frames', async () => { + const { tab } = await buildTestScene(); + tab.onChangeSource({ value: 'data-frames' }); + + const obj = JSON.parse(tab.state.jsonText); + expect(Array.isArray(obj)).toBe(true); + expect(obj[0].schema.fields.length).toBe(1); + expect(tab.isEditable()).toBe(false); + }); + + it('Can update model', async () => { + const { tab, panel, scene } = await buildTestScene(); + + tab.onCodeEditorBlur(`{ + "id": 12, + "type": "table", + "title": "New title", + "gridPos": { + "x": 1, + "y": 2, + "w": 3, + "h": 4 + }, + "options": {}, + "fieldConfig": {}, + "transformations": [], + "transparent": false + }`); + + tab.onApplyChange(); + + const panel2 = findVizPanelByKey(scene, panel.state.key)!; + expect(panel2.state.title).toBe('New title'); + expect((panel2.parent as SceneGridItem).state.width!).toBe(3); + + expect(tab.state.onClose).toHaveBeenCalled(); + }); +}); + +async function buildTestScene() { + const panel = new VizPanel({ + title: 'Panel A', + pluginId: 'table', + key: 'panel-12', + $data: new SceneDataTransformer({ + transformations: [ + { + id: 'reduce', + options: { + reducers: ['last'], + }, + }, + ], + $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' }) }, + }, + }, + }, + }), + }), + }); + + const scene = new DashboardScene({ + title: 'hello', + uid: 'dash-1', + meta: { + canEdit: true, + }, + body: new SceneGridLayout({ + children: [ + new SceneGridItem({ + key: 'griditem-1', + x: 0, + y: 0, + width: 10, + height: 12, + body: panel, + }), + ], + }), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + const tab = new InspectJsonTab({ + panelRef: new SceneObjectRef(panel), + onClose: jest.fn(), + }); + + return { scene, tab, panel }; +} diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 715e92f1e40..1e7cafed357 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -1,11 +1,248 @@ +import { isEqual } from 'lodash'; import React from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; -import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; +import { SelectableValue } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { + SceneComponentProps, + SceneDataTransformer, + sceneGraph, + SceneGridItem, + SceneGridItemStateLike, + SceneObjectBase, + SceneObjectRef, + SceneObjectState, + SceneQueryRunner, + sceneUtils, + VizPanel, +} from '@grafana/scenes'; +import { Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils'; +import { PanelModel } from 'app/features/dashboard/state'; +import { getPanelInspectorStyles2 } from 'app/features/inspector/styles'; +import { InspectTab } from 'app/features/inspector/types'; +import { getPrettyJSON } from 'app/features/inspector/utils/utils'; +import { reportPanelInspectInteraction } from 'app/features/search/page/reporting'; -import { InspectTabState } from './types'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; +import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene'; +import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; +import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils'; + +export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames'; + +export interface InspectJsonTabState extends SceneObjectState { + panelRef: SceneObjectRef; + source: ShowContent; + jsonText: string; + onClose: () => void; +} + +export class InspectJsonTab extends SceneObjectBase { + public constructor(state: Omit) { + super({ + ...state, + source: 'panel-json', + jsonText: getJsonText('panel-json', state.panelRef.resolve()), + }); + } + + public getTabLabel() { + return t('dashboard.inspect.json-tab', 'JSON'); + } + + public getTabValue() { + return InspectTab.JSON; + } + + public getOptions(): Array> { + const panel = this.state.panelRef.resolve(); + const dataProvider = panel.state.$data; + + const options: Array> = [ + { + label: t('dashboard.inspect-json.panel-json-label', 'Panel JSON'), + description: t( + 'dashboard.inspect-json.panel-json-description', + 'The model saved in the dashboard JSON that configures how everything works.' + ), + value: 'panel-json', + }, + ]; + + if (dataProvider) { + options.push({ + label: t('dashboard.inspect-json.panel-data-label', 'Panel data'), + description: t( + 'dashboard.inspect-json.panel-data-description', + 'The raw model passed to the panel visualization' + ), + value: 'panel-data', + }); + options.push({ + label: t('dashboard.inspect-json.dataframe-label', 'DataFrame JSON (from Query)'), + description: t( + 'dashboard.inspect-json.dataframe-description', + 'Raw data without transformations and field config applied. ' + ), + value: 'data-frames', + }); + } + + return options; + } + + public onChangeSource = (value: SelectableValue) => { + this.setState({ source: value.value!, jsonText: getJsonText(value.value!, this.state.panelRef.resolve()) }); + }; + + public onApplyChange = () => { + const panel = this.state.panelRef.resolve(); + const dashboard = getDashboardSceneFor(panel); + const jsonObj = JSON.parse(this.state.jsonText); + + const panelModel = new PanelModel(jsonObj); + const gridItem = buildGridItemForPanel(panelModel); + const newState = sceneUtils.cloneSceneObjectState(gridItem.state); + + if (!(panel.parent instanceof SceneGridItem) || !(gridItem instanceof SceneGridItem)) { + console.error('Cannot update state of panel', panel, gridItem); + return; + } + + this.state.onClose(); + + if (!dashboard.state.isEditing) { + dashboard.onEnterEditMode(); + } + + panel.parent.setState(newState); + + //Report relevant updates + reportPanelInspectInteraction(InspectTab.JSON, 'apply', { + panel_type_changed: panel.state.pluginId !== panelModel.type, + panel_id_changed: getPanelIdForVizPanel(panel) !== panelModel.id, + panel_grid_pos_changed: hasGridPosChanged(panel.parent.state, newState), + panel_targets_changed: hasQueriesChanged(getQueryRunnerFor(panel), getQueryRunnerFor(gridItem.state.body)), + }); + }; + + public onCodeEditorBlur = (value: string) => { + this.setState({ jsonText: value }); + }; + + public isEditable() { + if (this.state.source !== 'panel-json') { + return false; + } + + const panel = this.state.panelRef.resolve(); + + // Only support normal grid items for now and not repeated items + if (!(panel.parent instanceof SceneGridItem)) { + return false; + } + + const dashboard = getDashboardSceneFor(panel); + return dashboard.state.meta.canEdit; + } -export class InspectJsonTab extends SceneObjectBase { static Component = ({ model }: SceneComponentProps) => { - return
JSON
; + const { source: show, jsonText } = model.useState(); + const styles = useStyles2(getPanelInspectorStyles2); + const options = model.getOptions(); + + return ( +
+
+ +