From bdbc3f351c419c5aca1ad2f25fc899943be49864 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Tue, 16 Jan 2024 07:04:31 -0700 Subject: [PATCH] Explore: Format data in Data tab in Query Inspector (#80004) * Fix betterer * Improve formatting logic --- .../explore/ExploreQueryInspector.test.tsx | 59 +++++++++++++++++++ .../explore/ExploreQueryInspector.tsx | 12 +++- .../features/inspector/InspectDataOptions.tsx | 13 ++-- .../app/features/inspector/InspectDataTab.tsx | 18 +++--- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/public/app/features/explore/ExploreQueryInspector.test.tsx b/public/app/features/explore/ExploreQueryInspector.test.tsx index 23f3f227c0a..d9d56863a48 100644 --- a/public/app/features/explore/ExploreQueryInspector.test.tsx +++ b/public/app/features/explore/ExploreQueryInspector.test.tsx @@ -1,5 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/react'; import React, { ComponentProps } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; import { Observable } from 'rxjs'; import { LoadingState, InternalTimeZones, getDefaultTimeRange } from '@grafana/data'; @@ -10,6 +11,7 @@ type ExploreQueryInspectorProps = ComponentProps; jest.mock('../inspector/styles', () => ({ getPanelInspectorStyles: () => ({}), + getPanelInspectorStyles2: () => ({}), })); jest.mock('app/core/services/backend_srv', () => ({ @@ -33,6 +35,15 @@ jest.mock('@grafana/runtime', () => ({ reportInteraction: () => null, })); +jest.mock('react-virtualized-auto-sizer', () => { + return { + __esModule: true, + default(props: ComponentProps) { + return
{props.children({ height: 1000, width: 1000 })}
; + }, + }; +}); + const setup = (propOverrides = {}) => { const props: ExploreQueryInspectorProps = { width: 100, @@ -82,6 +93,54 @@ describe('ExploreQueryInspector', () => { fireEvent.click(screen.getByText(/expand all/i)); expect(screen.getByText(/very unique test value/i)).toBeInTheDocument(); }); + it('should display formatted data', () => { + setup({ + queryResponse: { + state: LoadingState.Done, + series: [ + { + refId: 'A', + fields: [ + { + name: 'time', + type: 'time', + typeInfo: { + frame: 'time.Time', + nullable: true, + }, + config: { + interval: 30000, + }, + values: [1704285124682, 1704285154682], + entities: {}, + }, + { + name: 'A-series', + type: 'number', + typeInfo: { + frame: 'float64', + nullable: true, + }, + labels: {}, + config: {}, + values: [71.202732378676928, 72.348839082431916], + entities: {}, + }, + ], + length: 2, + }, + ], + }, + }); + + fireEvent.click(screen.getByLabelText(/tab data/i)); + // assert series values are formatted to 3 digits (xx.x or x.xx) + expect(screen.getByText(/71.2/i)).toBeInTheDocument(); + expect(screen.getByText(/72.3/i)).toBeInTheDocument(); + // assert timestamps are formatted + expect(screen.getByText(/2024-01-03 12:32:04.682/i)).toBeInTheDocument(); + expect(screen.getByText(/2024-01-03 12:32:34.682/i)).toBeInTheDocument(); + }); }); const response = (hideFromInspector = false) => ({ diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index d9f6d817142..1d6204c5fce 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { CoreApp, LoadingState } from '@grafana/data'; @@ -13,6 +13,8 @@ import { InspectStatsTab } from 'app/features/inspector/InspectStatsTab'; import { QueryInspector } from 'app/features/inspector/QueryInspector'; import { StoreState, ExploreItemState } from 'app/types'; +import { GetDataOptions } from '../query/state/PanelQueryRunner'; + import { runQueries } from './state/query'; interface DispatchProps { @@ -26,6 +28,10 @@ type Props = DispatchProps & ConnectedProps; export function ExploreQueryInspector(props: Props) { const { width, onClose, queryResponse, timeZone } = props; + const [dataOptions, setDataOptions] = useState({ + withTransforms: false, + withFieldConfig: true, + }); const dataFrames = queryResponse?.series || []; let errors = queryResponse?.errors; if (!errors?.length && queryResponse?.error) { @@ -59,9 +65,11 @@ export function ExploreQueryInspector(props: Props) { data={dataFrames} dataName={'Explore'} isLoading={queryResponse.state === LoadingState.Loading} - options={{ withTransforms: false, withFieldConfig: false }} + options={dataOptions} timeZone={timeZone} app={CoreApp.Explore} + formattedDataDescription="Matches the format in the panel" + onOptionsChange={setDataOptions} /> ), }; diff --git a/public/app/features/inspector/InspectDataOptions.tsx b/public/app/features/inspector/InspectDataOptions.tsx index 830a1e1c3bb..910569f2c0f 100644 --- a/public/app/features/inspector/InspectDataOptions.tsx +++ b/public/app/features/inspector/InspectDataOptions.tsx @@ -19,6 +19,7 @@ interface Props { toggleDownloadForExcel: () => void; data?: DataFrame[]; hasTransformations?: boolean; + formattedDataDescription?: string; onOptionsChange?: (options: GetDataOptions) => void; actions?: React.ReactNode; } @@ -26,6 +27,7 @@ interface Props { export const InspectDataOptions = ({ options, actions, + formattedDataDescription, onOptionsChange, hasTransformations, data, @@ -129,10 +131,13 @@ export const InspectDataOptions = ({ {onOptionsChange && ( void; } @@ -176,11 +177,15 @@ export class InspectDataTab extends PureComponent { const { options, panelPluginId, fieldConfig, timeZone } = this.props; const data = this.state.transformedData; - if (!options.withFieldConfig || !panelPluginId || !fieldConfig) { + if (!options.withFieldConfig) { return applyRawFieldOverrides(data); } - const fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(panelPluginId, fieldConfig); + let fieldConfigCleaned = fieldConfig ?? { defaults: {}, overrides: [] }; + // Because we visualize this data in a table we have to remove any custom table display settings + if (panelPluginId === 'table' && fieldConfig) { + fieldConfigCleaned = this.cleanTableConfigFromFieldConfig(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. @@ -194,11 +199,7 @@ export class InspectDataTab extends PureComponent { } // Because we visualize this data in a table we have to remove any custom table display settings - cleanTableConfigFromFieldConfig(panelPluginId: string, fieldConfig: FieldConfigSource): FieldConfigSource { - if (panelPluginId !== 'table') { - return fieldConfig; - } - + cleanTableConfigFromFieldConfig(fieldConfig: FieldConfigSource): FieldConfigSource { fieldConfig = cloneDeep(fieldConfig); // clear all table specific options fieldConfig.defaults.custom = {}; @@ -242,7 +243,7 @@ export class InspectDataTab extends PureComponent { } render() { - const { isLoading, options, data, onOptionsChange, hasTransformations } = this.props; + const { isLoading, options, data, formattedDataDescription, onOptionsChange, hasTransformations } = this.props; const { dataFrameIndex, transformationOptions, selectedDataFrame, downloadForExcel } = this.state; const styles = getPanelInspectorStyles(); @@ -278,6 +279,7 @@ export class InspectDataTab extends PureComponent { transformationOptions={transformationOptions} selectedDataFrame={selectedDataFrame} downloadForExcel={downloadForExcel} + formattedDataDescription={formattedDataDescription} onOptionsChange={onOptionsChange} onDataFrameChange={this.onDataFrameChange} toggleDownloadForExcel={this.onToggleDownloadForExcel}