From 73a675af0234099ccd042040c1b2cf9b20054e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Sep 2023 16:11:22 +0200 Subject: [PATCH] DashboardScene: Inspect panel data tab (#74646) * Refactor data tab to be usable from scenes * DashboardScene: Inspect data tab * Everything seem to work now * don't change drawer size in this PR * Remove uncommented code * Fix layout issues for data actions * Added comment explaining retry --- .../QueryOperationRowHeader.tsx | 5 +- .../inspect/InspectDataTab.tsx | 75 ++++++++-- .../inspect/InspectJsonTab.tsx | 9 +- .../inspect/InspectStatsTab.tsx | 12 +- .../inspect/PanelInspectDrawer.tsx | 39 +++-- .../features/dashboard-scene/inspect/types.ts | 3 +- .../components/Inspector/InspectContent.tsx | 5 +- .../TransformationOperationRow.tsx | 6 +- .../explore/ExploreQueryInspector.tsx | 1 + .../features/inspector/InspectDataOptions.tsx | 18 +-- .../app/features/inspector/InspectDataTab.tsx | 140 ++++++++---------- .../query/components/QueryEditorRow.tsx | 6 +- 12 files changed, 183 insertions(+), 136 deletions(-) diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index e3cbace1aeb..98cc9adb3ec 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -3,6 +3,7 @@ import React, { MouseEventHandler } from 'react'; import { DraggableProvided } from 'react-beautiful-dnd'; import { GrafanaTheme2 } from '@grafana/data'; +import { Stack } from '@grafana/experimental'; import { Icon, IconButton, useStyles2 } from '@grafana/ui'; export interface QueryOperationRowHeaderProps { @@ -58,7 +59,7 @@ export const QueryOperationRowHeader = ({ {headerElement} -
+ {actionsElement} {draggable && ( )} -
+ ); }; diff --git a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx index 489b39b738a..0c964e7de06 100644 --- a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx @@ -1,20 +1,77 @@ import React from 'react'; -import { SceneComponentProps, SceneObjectBase, VizPanel } from '@grafana/scenes'; -import { t } from 'app/core/internationalization'; +import { LoadingState } from '@grafana/data'; +import { + SceneComponentProps, + SceneDataProvider, + SceneDataTransformer, + sceneGraph, + SceneObjectBase, +} from '@grafana/scenes'; +import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; -import { InspectTab } from '../../inspector/types'; +import { InspectDataTab as InspectDataTabOld } from '../../inspector/InspectDataTab'; import { InspectTabState } from './types'; -export class InspectDataTab extends SceneObjectBase { - constructor(public panel: VizPanel) { - super({ label: t('dashboard.inspect.data-tab', 'Data'), value: InspectTab.Data }); +export interface InspectDataTabState extends InspectTabState { + options: GetDataOptions; +} + +export class InspectDataTab extends SceneObjectBase { + public constructor(state: Omit) { + super({ + ...state, + options: { + withTransforms: true, + withFieldConfig: true, + }, + }); } - static Component = ({ model }: SceneComponentProps) => { - //const data = sceneGraph.getData(model.panel).useState(); + public onOptionsChange = (options: GetDataOptions) => { + this.setState({ options }); + }; - return
Data tab
; + static Component = ({ model }: SceneComponentProps) => { + const { options } = model.useState(); + const panel = model.state.panelRef.resolve(); + const dataProvider = sceneGraph.getData(panel); + const { data } = getDataProviderToSubscribeTo(dataProvider, options.withTransforms).useState(); + const timeRange = sceneGraph.getTimeRange(panel); + + if (!data) { +
No data found
; + } + + return ( + + ); }; } + +function hasTransformations(dataProvider: SceneDataProvider) { + if (dataProvider instanceof SceneDataTransformer) { + return dataProvider.state.transformations.length > 0; + } + + return false; +} + +function getDataProviderToSubscribeTo(dataProvider: SceneDataProvider, withTransforms: boolean) { + if (withTransforms && dataProvider instanceof SceneDataTransformer) { + return dataProvider.state.$data!; + } + + return dataProvider; +} diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx index 2689c1dbc42..715e92f1e40 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx @@ -1,17 +1,10 @@ import React from 'react'; -import { SceneComponentProps, SceneObjectBase, VizPanel } from '@grafana/scenes'; -import { t } from 'app/core/internationalization'; - -import { InspectTab } from '../../inspector/types'; +import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { InspectTabState } from './types'; export class InspectJsonTab extends SceneObjectBase { - constructor(public panel: VizPanel) { - super({ label: t('dashboard.inspect.json-tab', 'JSON'), value: InspectTab.JSON }); - } - static Component = ({ model }: SceneComponentProps) => { return
JSON
; }; diff --git a/public/app/features/dashboard-scene/inspect/InspectStatsTab.tsx b/public/app/features/dashboard-scene/inspect/InspectStatsTab.tsx index 9fa07c7896e..6571a5a6aeb 100644 --- a/public/app/features/dashboard-scene/inspect/InspectStatsTab.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectStatsTab.tsx @@ -1,21 +1,15 @@ import React from 'react'; -import { SceneComponentProps, sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes'; -import { t } from 'app/core/internationalization'; +import { SceneComponentProps, sceneGraph, SceneObjectBase } from '@grafana/scenes'; import { InspectStatsTab as OldInspectStatsTab } from '../../inspector/InspectStatsTab'; -import { InspectTab } from '../../inspector/types'; import { InspectTabState } from './types'; export class InspectStatsTab extends SceneObjectBase { - constructor(public panel: VizPanel) { - super({ label: t('dashboard.inspect.stats-tab', 'Stats'), value: InspectTab.Stats }); - } - static Component = ({ model }: SceneComponentProps) => { - const data = sceneGraph.getData(model.panel).useState(); - const timeRange = sceneGraph.getTimeRange(model.panel); + const data = sceneGraph.getData(model.state.panelRef.resolve()).useState(); + const timeRange = sceneGraph.getTimeRange(model.state.panelRef.resolve()); if (!data.data) { return null; diff --git a/public/app/features/dashboard-scene/inspect/PanelInspectDrawer.tsx b/public/app/features/dashboard-scene/inspect/PanelInspectDrawer.tsx index 940a7849a71..ff193b34b1f 100644 --- a/public/app/features/dashboard-scene/inspect/PanelInspectDrawer.tsx +++ b/public/app/features/dashboard-scene/inspect/PanelInspectDrawer.tsx @@ -12,8 +12,10 @@ import { VizPanel, SceneObjectRef, } from '@grafana/scenes'; -import { Drawer, Tab, TabsBar } from '@grafana/ui'; +import { Alert, Drawer, Tab, TabsBar } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils'; +import { InspectTab } from 'app/features/inspector/types'; import { InspectDataTab } from './InspectDataTab'; import { InspectJsonTab } from './InspectJsonTab'; @@ -23,6 +25,7 @@ import { InspectTabState } from './types'; interface PanelInspectDrawerState extends SceneObjectState { tabs?: Array>; panelRef: SceneObjectRef; + pluginNotLoaded?: boolean; } export class PanelInspectDrawer extends SceneObjectBase { @@ -31,22 +34,35 @@ export class PanelInspectDrawer extends SceneObjectBase constructor(state: PanelInspectDrawerState) { super(state); - this.buildTabs(); + this.buildTabs(0); } - buildTabs() { - const panel = this.state.panelRef.resolve(); + /** + * We currently have no async await to get the panel plugin from the VizPanel. + * That is why there is a retry argument here and a setTimeout, to try again a bit later. + */ + buildTabs(retry: number) { + const panelRef = this.state.panelRef; + const panel = panelRef.resolve(); const plugin = panel.getPlugin(); const tabs: Array> = []; if (plugin) { if (supportsDataQuery(plugin)) { - tabs.push(new InspectDataTab(panel)); - tabs.push(new InspectStatsTab(panel)); + tabs.push( + new InspectDataTab({ panelRef, label: t('dashboard.inspect.data-tab', 'Data'), value: InspectTab.Data }) + ); + tabs.push( + new InspectStatsTab({ panelRef, label: t('dashboard.inspect.stats-tab', 'Stats'), value: InspectTab.Stats }) + ); } + } else if (retry < 2000) { + setTimeout(() => this.buildTabs(retry + 100), 100); + } else { + this.setState({ pluginNotLoaded: true }); } - tabs.push(new InspectJsonTab(panel)); + tabs.push(new InspectJsonTab({ panelRef, label: t('dashboard.inspect.json-tab', 'JSON'), value: InspectTab.JSON })); this.setState({ tabs }); } @@ -62,7 +78,7 @@ export class PanelInspectDrawer extends SceneObjectBase } function PanelInspectRenderer({ model }: SceneComponentProps) { - const { tabs } = model.useState(); + const { tabs, pluginNotLoaded } = model.useState(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); @@ -78,7 +94,7 @@ function PanelInspectRenderer({ model }: SceneComponentProps title={model.getDrawerTitle()} scrollableContent onClose={model.onClose} - size="md" + size="lg" tabs={ {tabs.map((tab) => { @@ -94,6 +110,11 @@ function PanelInspectRenderer({ model }: SceneComponentProps } > + {pluginNotLoaded && ( + + Make sure the panel you want to inspect is visible and has been displayed before opening inspect. + + )} {currentTab.Component && } ); diff --git a/public/app/features/dashboard-scene/inspect/types.ts b/public/app/features/dashboard-scene/inspect/types.ts index 209b3218ce2..48b24b8e97b 100644 --- a/public/app/features/dashboard-scene/inspect/types.ts +++ b/public/app/features/dashboard-scene/inspect/types.ts @@ -1,7 +1,8 @@ -import { SceneObjectState } from '@grafana/scenes'; +import { SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; import { InspectTab } from 'app/features/inspector/types'; export interface InspectTabState extends SceneObjectState { label: string; value: InspectTab; + panelRef: SceneObjectRef; } diff --git a/public/app/features/dashboard/components/Inspector/InspectContent.tsx b/public/app/features/dashboard/components/Inspector/InspectContent.tsx index 96a81bb7838..b4ac829fce8 100644 --- a/public/app/features/dashboard/components/Inspector/InspectContent.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectContent.tsx @@ -88,7 +88,10 @@ export const InspectContent = ({ > {activeTab === InspectTab.Data && ( { return ( - + <> {uiConfig.state && } setShowDeleteModal(false)} /> )} - + ); }; diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index 5a34d588ad5..9b7e93f2870 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -56,6 +56,7 @@ export function ExploreQueryInspector(props: Props) { content: ( >; selectedDataFrame: number | DataTransformerID; downloadForExcel: boolean; onDataFrameChange: (item: SelectableValue) => void; toggleDownloadForExcel: () => void; data?: DataFrame[]; - panel?: PanelModel; + hasTransformations?: boolean; onOptionsChange?: (options: GetDataOptions) => void; + actions?: React.ReactNode; } export const InspectDataOptions = ({ options, + actions, onOptionsChange, - panel, + hasTransformations, data, dataFrames, - transformId, transformationOptions, selectedDataFrame, onDataFrameChange, @@ -39,10 +38,6 @@ export const InspectDataOptions = ({ }: Props) => { const styles = useStyles2(getPanelInspectorStyles2); - const panelTransformations = panel?.getTransformations(); - const showPanelTransformationsOption = Boolean(panelTransformations?.length); - const showFieldConfigsOption = panel && !panel.plugin?.fieldConfigRegistry.isEmpty(); - let dataSelect = dataFrames; if (selectedDataFrame === DataTransformerID.joinByField) { dataSelect = data!; @@ -100,6 +95,7 @@ export const InspectDataOptions = ({ title={t('dashboard.inspect-data.data-options', 'Data options')} headerElement={{getActiveString()}} isOpen={false} + actions={actions} >
@@ -116,7 +112,7 @@ export const InspectDataOptions = ({ )} - {showPanelTransformationsOption && onOptionsChange && ( + {hasTransformations && onOptionsChange && ( )} - {showFieldConfigsOption && onOptionsChange && ( + {onOptionsChange && ( void; } @@ -91,15 +92,20 @@ export class InspectDataTab extends PureComponent { } } - exportCsv = (dataFrame: DataFrame, csvConfig: CSVConfig = {}) => { - const { panel } = this.props; + exportCsv(dataFrames: DataFrame[], hasLogs: boolean) { + const { dataName } = this.props; const { transformId } = this.state; + const dataFrame = dataFrames[this.state.dataFrameIndex]; - downloadDataFrameAsCsv(dataFrame, panel ? panel.getDisplayTitle() : 'Explore', csvConfig, transformId); - }; + if (hasLogs) { + reportInteraction('grafana_logs_download_clicked', { app: this.props.app, format: 'csv' }); + } - exportLogsAsTxt = () => { - const { data, panel, app } = this.props; + downloadDataFrameAsCsv(dataFrame, dataName, { useExcelHeader: this.state.downloadForExcel }, transformId); + } + + onExportLogsAsTxt = () => { + const { data, dataName, app } = this.props; reportInteraction('grafana_logs_download_logs_clicked', { app, @@ -108,11 +114,11 @@ export class InspectDataTab extends PureComponent { }); const logsModel = dataFrameToLogsModel(data || []); - downloadLogsModelAsTxt(logsModel, panel ? panel.getDisplayTitle() : 'Explore'); + downloadLogsModelAsTxt(logsModel, dataName); }; - exportTracesAsJson = () => { - const { data, panel, app } = this.props; + onExportTracesAsJson = () => { + const { data, dataName, app } = this.props; if (!data) { return; @@ -123,7 +129,8 @@ export class InspectDataTab extends PureComponent { if (df.meta?.preferredVisualisationType !== 'trace') { continue; } - const traceFormat = downloadTraceAsJson(df, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces'); + + const traceFormat = downloadTraceAsJson(df, dataName + '-traces'); reportInteraction('grafana_traces_download_traces_clicked', { app, @@ -134,8 +141,9 @@ export class InspectDataTab extends PureComponent { } }; - exportServiceGraph = () => { - const { data, panel, app } = this.props; + onExportServiceGraph = () => { + const { data, dataName, app } = this.props; + reportInteraction('grafana_traces_download_service_graph_clicked', { app, grafana_version: config.buildInfo.version, @@ -146,7 +154,7 @@ export class InspectDataTab extends PureComponent { return; } - downloadAsJson(data, panel ? panel.getDisplayTitle() : 'Explore'); + downloadAsJson(data, dataName); }; onDataFrameChange = (item: SelectableValue) => { @@ -158,28 +166,28 @@ export class InspectDataTab extends PureComponent { }); }; - toggleDownloadForExcel = () => { + onToggleDownloadForExcel = () => { this.setState((prevState) => ({ downloadForExcel: !prevState.downloadForExcel, })); }; getProcessedData(): DataFrame[] { - const { options, panel, timeZone } = this.props; + const { options, panelPluginId, fieldConfig, timeZone } = this.props; const data = this.state.transformedData; - if (!options.withFieldConfig || !panel) { + if (!options.withFieldConfig || !panelPluginId || !fieldConfig) { return applyRawFieldOverrides(data); } - const fieldConfig = this.cleanTableConfigFromFieldConfig(panel.type, panel.fieldConfig); + const fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(panelPluginId, fieldConfig); // We need to apply field config as it's not done by PanelQueryRunner (even when withFieldConfig is true). // It's because transformers create new fields and data frames, and we need to clean field config of any table settings. return applyFieldOverrides({ data, theme: config.theme2, - fieldConfig, + fieldConfig: fieldConfigCleaned, timeZone, replaceVariables: (value: string) => { return value; @@ -210,9 +218,34 @@ export class InspectDataTab extends PureComponent { return fieldConfig; } + renderActions(dataFrames: DataFrame[], hasLogs: boolean, hasTraces: boolean, hasServiceGraph: boolean) { + return ( + <> + + {hasLogs && ( + + )} + {hasTraces && ( + + )} + {hasServiceGraph && ( + + )} + + ); + } + render() { - const { isLoading, options, data, panel, onOptionsChange, app } = this.props; - const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state; + const { isLoading, options, data, onOptionsChange, hasTransformations } = this.props; + const { dataFrameIndex, transformationOptions, selectedDataFrame, downloadForExcel } = this.state; const styles = getPanelInspectorStyles(); if (isLoading) { @@ -241,70 +274,17 @@ export class InspectDataTab extends PureComponent {
- - {hasLogs && ( - - )} - {hasTraces && ( - - )} - {hasServiceGraph && ( - - )}
diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index c380d543157..28456e6b4f8 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -23,7 +23,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { AngularComponent, getAngularLoader, getDataSourceSrv } from '@grafana/runtime'; -import { Badge, ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui'; +import { Badge, ErrorBoundaryAlert } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { QueryOperationAction, @@ -439,7 +439,7 @@ export class QueryEditorRow extends PureComponent + <> {hasEditorHelp && ( extends PureComponent ) : null} - + ); };