From f23ecc40b43e85f0ec91cd39eb72c18a9f9ce9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 May 2020 13:03:34 +0200 Subject: [PATCH] Inspect: Allow showing data without transformations and field config is applied (#24314) * Inspect: Should not subscribe to transformed data * PQR- allow controll whether or not field overrides and transformations should be applied * UI for inspector data options * fix * Null check fix * Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx * Update public/app/features/dashboard/components/Inspector/InspectDataTab.tsx * Apply transformations by default * Update panel inspect docs * Fix apply overrides * Apply time formatting in panel inspect * fix ts * Post review update * Update docs/sources/panels/inspect-panel.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * lazy numbering * fix ts * Renames * Renames 2 * Layout update * Run shared request without field config * Minor details * fix ts Co-authored-by: Dominik Prokop Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> --- docs/sources/panels/inspect-panel.md | 12 +- .../src/components/Layout/Layout.tsx | 4 +- .../src/components/Table/DefaultCell.tsx | 8 +- .../components/Inspector/InspectDataTab.tsx | 104 +++++++++++++++--- .../components/Inspector/PanelInspector.tsx | 38 ++++++- .../dashboard/components/Inspector/styles.ts | 25 ++++- .../components/PanelEditor/state/actions.ts | 2 +- .../dashboard/dashgrid/PanelChromeAngular.tsx | 2 +- .../dashboard/panel_editor/QueriesTab.tsx | 4 +- .../dashboard/state/PanelQueryRunner.test.ts | 45 ++++++++ .../dashboard/state/PanelQueryRunner.ts | 47 +++++--- .../datasource/dashboard/runSharedRequest.ts | 2 +- 12 files changed, 233 insertions(+), 60 deletions(-) diff --git a/docs/sources/panels/inspect-panel.md b/docs/sources/panels/inspect-panel.md index 55d43f8a7b5..edba09c4d04 100644 --- a/docs/sources/panels/inspect-panel.md +++ b/docs/sources/panels/inspect-panel.md @@ -19,7 +19,7 @@ The panel inspector displays Inspect: at the top of The panel inspector consists of four tabs: -* **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied. +* **Data tab -** Shows the raw data returned by the query with transformations applied. Field options such as overrides and value mappings are not applied by default. * **Stats tab -** Shows how long your query takes and how much it returns. * **JSON tab -** Allows you to view and copy the panel JSON, panel data JSON, and data frame structure JSON. This is useful if you are provisioning or administering Grafana. * **Query tab -** Shows you the requests to the server sent when Grafana queries the data source. @@ -42,14 +42,18 @@ The panel inspector pane opens on the right side of the screen. ### Inspect raw query results -View raw query results in a table. This is the data returned by the query with transformations applied and before the panel applies field configurations or overrides. +View raw query results in a table. This is the data returned by the query with transformations applied and before the panel applies field options or field option overrides. 1. Open the panel inspector and then click the **Data** tab or in the panel menu click **Inspect > Data**. - -2. If your panel contains multiple queries or queries multiple nodes, then you have additional options. +1. If your panel contains multiple queries or queries multiple nodes, then you have additional options. * **Select result -** Choose which result set data you want to view. * **Transform data** * **Join by time -** View raw data from all your queries at once, one result set per column. Click a column heading to reorder the data. + + View raw query results in a table with field options and options overrides applied: + 1. Open the **Data** tab in panel inspector. + 1. Click on **Data display options** above the table. + 1. Click on **Apply field configuration** toggle button. ### Download raw query results as CSV diff --git a/packages/grafana-ui/src/components/Layout/Layout.tsx b/packages/grafana-ui/src/components/Layout/Layout.tsx index 3cc6c8e12b3..26a2510a899 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.tsx @@ -136,8 +136,8 @@ const getStyles = stylesFactory( align-items: ${align}; &:last-child { - margin-bottom: 0; - margin-right: 0; + margin-bottom: ${orientation === Orientation.Vertical && 0}; + margin-right: ${orientation === Orientation.Horizontal && 0}; } `, }; diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index f0682ef04e7..ad9e0d3dc30 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -7,18 +7,14 @@ export const DefaultCell: FC = props => { const { field, cell, tableStyles, row } = props; let link: LinkModel | undefined; - if (!field.display) { - return null; - } - - const displayValue = field.display(cell.value); + const displayValue = field.display ? field.display(cell.value) : cell.value; if (field.getLinks) { link = field.getLinks({ valueRowIndex: row.index, })[0]; } - const value = formattedValueToString(displayValue); + const value = field.display ? formattedValueToString(displayValue) : displayValue; return (
diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index 11e294a7167..73a62cc7c0d 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -8,18 +8,27 @@ import { transformDataFrame, getFrameDisplayName, } from '@grafana/data'; -import { Button, Field, Icon, Select, Table } from '@grafana/ui'; +import { Button, Field, Icon, LegacyForms, Select, Table } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import AutoSizer from 'react-virtualized-auto-sizer'; import { getPanelInspectorStyles } from './styles'; import { config } from 'app/core/config'; import { saveAs } from 'file-saver'; -import { cx } from 'emotion'; +import { css, cx } from 'emotion'; +import { GetDataOptions } from '../../state/PanelQueryRunner'; +import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; + +const { Switch } = LegacyForms; interface Props { + dashboard: DashboardModel; + panel: PanelModel; data: DataFrame[]; isLoading: boolean; + options: GetDataOptions; + onOptionsChange: (options: GetDataOptions) => void; } interface State { @@ -55,6 +64,10 @@ export class InspectDataTab extends PureComponent { onTransformationChange = (value: SelectableValue) => { this.setState({ transformId: value.value, dataFrameIndex: 0 }); + this.props.onOptionsChange({ + ...this.props.options, + withTransforms: false, + }); }; getTransformedData(): DataFrame[] { @@ -74,10 +87,19 @@ export class InspectDataTab extends PureComponent { } getProcessedData(): DataFrame[] { + const { options } = this.props; + let data = this.props.data; + + if (this.state.transformId !== DataTransformerID.noop) { + data = this.getTransformedData(); + } + + // We need to apply field config even though it was already applied in the PanelQueryRunner. + // That's because transformers create new fields and data frames, so i.e. display processor is no longer there return applyFieldOverrides({ - data: this.getTransformedData(), + data, theme: config.theme, - fieldConfig: { defaults: {}, overrides: [] }, + fieldConfig: options.withFieldConfig ? this.props.panel.fieldConfig : { defaults: {}, overrides: [] }, replaceVariables: (value: string) => { return value; }, @@ -85,7 +107,7 @@ export class InspectDataTab extends PureComponent { } render() { - const { isLoading, data } = this.props; + const { isLoading, data, options, onOptionsChange } = this.props; const { dataFrameIndex, transformId, transformationOptions } = this.state; const styles = getPanelInspectorStyles(); @@ -110,25 +132,73 @@ export class InspectDataTab extends PureComponent { }; }); + const panelTransformations = this.props.panel.getTransformations(); + return (
-
- {data.length > 1 && ( - - - - )} -
+
+
+
+ {data.length > 1 && ( + + + + )} +
+ +
+ + {panelTransformations && panelTransformations.length > 0 && (transformId as any) !== 'join by time' && ( +
+ onOptionsChange({ ...options, withTransforms: !options.withTransforms })} + /> +
+ )} +
+ onOptionsChange({ ...options, withFieldConfig: !options.withFieldConfig })} + /> +
+
+
+
+ +
+
{({ width, height }) => { diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index a3beef147e8..bed7fe01f3e 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -28,6 +28,7 @@ import { getPanelInspectorStyles } from './styles'; import { StoreState } from 'app/types'; import { InspectDataTab } from './InspectDataTab'; import { supportsDataQuery } from '../PanelEditor/utils'; +import { GetDataOptions } from '../../state/PanelQueryRunner'; interface OwnProps { dashboard: DashboardModel; @@ -62,6 +63,8 @@ interface State { metaDS?: DataSourceApi; // drawer width drawerWidth: string; + withTransforms: boolean; + withFieldConfig: boolean; } export class PanelInspectorUnconnected extends PureComponent { @@ -76,6 +79,8 @@ export class PanelInspectorUnconnected extends PureComponent { data: [], currentTab: props.defaultTab ?? InspectTab.Data, drawerWidth: '50%', + withTransforms: true, + withFieldConfig: false, }; } @@ -87,8 +92,12 @@ export class PanelInspectorUnconnected extends PureComponent { } } - componentDidUpdate(prevProps: Props) { - if (prevProps.plugin !== this.props.plugin) { + componentDidUpdate(prevProps: Props, prevState: State) { + if ( + prevProps.plugin !== this.props.plugin || + this.state.withTransforms !== prevState.withTransforms || + this.state.withFieldConfig !== prevState.withFieldConfig + ) { this.init(); } } @@ -99,11 +108,15 @@ export class PanelInspectorUnconnected extends PureComponent { */ init() { const { plugin, panel } = this.props; + const { withTransforms, withFieldConfig } = this.state; if (plugin && !plugin.meta.skipDataQuery) { + if (this.querySubscription) { + this.querySubscription.unsubscribe(); + } this.querySubscription = panel .getQueryRunner() - .getData() + .getData({ withTransforms, withFieldConfig }) .subscribe({ next: data => this.onUpdateData(data), }); @@ -164,6 +177,9 @@ export class PanelInspectorUnconnected extends PureComponent { onSelectTab = (item: SelectableValue) => { this.setState({ currentTab: item.value || InspectTab.Data }); }; + onDataTabOptionsChange = (options: GetDataOptions) => { + this.setState({ withTransforms: !!options.withTransforms, withFieldConfig: !!options.withFieldConfig }); + }; renderMetadataInspector() { const { metaDS, data } = this.state; @@ -174,8 +190,20 @@ export class PanelInspectorUnconnected extends PureComponent { } renderDataTab() { - const { last, isLoading } = this.state; - return ; + const { last, isLoading, withFieldConfig, withTransforms } = this.state; + return ( + + ); } renderErrorTab(error?: DataQueryError) { diff --git a/public/app/features/dashboard/components/Inspector/styles.ts b/public/app/features/dashboard/components/Inspector/styles.ts index 981861b3575..6a5e007ccce 100644 --- a/public/app/features/dashboard/components/Inspector/styles.ts +++ b/public/app/features/dashboard/components/Inspector/styles.ts @@ -41,9 +41,6 @@ export const getPanelInspectorStyles = stylesFactory(() => { dataFrameSelect: css` flex-grow: 2; `, - downloadCsv: css` - margin-left: 16px; - `, tabContent: css` height: 100%; display: flex; @@ -55,5 +52,27 @@ export const getPanelInspectorStyles = stylesFactory(() => { height: 100%; width: 100%; `, + actionsWrapper: css` + display: flex; + flex-wrap: wrap; + `, + leftActions: css` + display: flex; + flex-grow: 1; + `, + options: css` + margin-top: 19px; + `, + dataDisplayOptions: css` + flex-grow: 1; + min-width: 300px; + margin-right: ${config.theme.spacing.sm}; + `, + selects: css` + display: flex; + > * { + margin-right: ${config.theme.spacing.sm}; + } + `, }; }); diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index 3aff52d282c..1c5c04aab7f 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -17,7 +17,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod const panel = dashboard.initEditPanel(sourcePanel); const queryRunner = panel.getQueryRunner(); - const querySubscription = queryRunner.getData(false).subscribe({ + const querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({ next: (data: PanelData) => dispatch(setEditorPanelData(data)), }); diff --git a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx index 7e6dec8c20e..04b3d8fbbbe 100644 --- a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx @@ -78,7 +78,7 @@ export class PanelChromeAngularUnconnected extends PureComponent { // subscribe to data events const queryRunner = panel.getQueryRunner(); - this.querySubscription = queryRunner.getData(false).subscribe({ + this.querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); } diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index e2ccc3f35b3..2e3f145d25b 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -50,7 +50,7 @@ interface State { export class QueriesTab extends PureComponent { datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); backendSrv = backendSrv; - querySubscription: Unsubscribable; + querySubscription: Unsubscribable | null; state: State = { isLoadingHelp: false, @@ -71,7 +71,7 @@ export class QueriesTab extends PureComponent { const { panel } = this.props; const queryRunner = panel.getQueryRunner(); - this.querySubscription = queryRunner.getData(false).subscribe({ + this.querySubscription = queryRunner.getData({ withTransforms: false }).subscribe({ next: (data: PanelData) => this.onPanelDataUpdate(data), }); diff --git a/public/app/features/dashboard/state/PanelQueryRunner.test.ts b/public/app/features/dashboard/state/PanelQueryRunner.test.ts index e3e0c2300e9..e4ad0e18956 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.test.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.test.ts @@ -230,6 +230,7 @@ describe('PanelQueryRunner', () => { ctx => { it('should apply when transformations are set', async () => { const spy = jest.spyOn(grafanaData, 'transformDataFrame'); + spy.mockClear(); ctx.runner.getData().subscribe({ next: (data: PanelData) => { @@ -246,4 +247,48 @@ describe('PanelQueryRunner', () => { getTransformations: () => [{}], } ); + + describeQueryRunnerScenario( + 'getData', + ctx => { + it('should not apply transformations when transform option is false', async () => { + const spy = jest.spyOn(grafanaData, 'transformDataFrame'); + spy.mockClear(); + ctx.runner.getData({ withTransforms: false }).subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + + expect(spy).not.toBeCalled(); + }); + + it('should not apply field config when applyFieldConfig option is false', async () => { + const spy = jest.spyOn(grafanaData, 'applyFieldOverrides'); + spy.mockClear(); + ctx.runner.getData({ withFieldConfig: false }).subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + + expect(spy).not.toBeCalled(); + }); + }, + { + getFieldOverrideOptions: () => ({ + fieldConfig: { + defaults: { + unit: 'm/s', + }, + // @ts-ignore + overrides: [], + }, + replaceVariables: v => v, + theme: {} as GrafanaTheme, + }), + // @ts-ignore + getTransformations: () => [{}], + } + ); }); diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index c870f631d32..1338112bb1f 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -51,6 +51,15 @@ function getNextRequestId() { return 'Q' + counter++; } +export interface GetDataOptions { + withTransforms?: boolean; + withFieldConfig?: boolean; +} +const DEFAULT_GET_DATA_OPTIONS: GetDataOptions = { + withTransforms: true, + withFieldConfig: true, +}; + export class PanelQueryRunner { private subject?: ReplaySubject; private subscription?: Unsubscribable; @@ -66,37 +75,39 @@ export class PanelQueryRunner { /** * Returns an observable that subscribes to the shared multi-cast subject (that reply last result). */ - getData(transform = true): Observable { + getData(options: GetDataOptions = DEFAULT_GET_DATA_OPTIONS): Observable { + const { withFieldConfig, withTransforms } = options; + return this.subject.pipe( map((data: PanelData) => { let processedData = data; - // Apply transformations - - if (transform) { + // Apply transformation + if (withTransforms) { const transformations = this.dataConfigSource.getTransformations(); if (transformations && transformations.length > 0) { processedData = { ...processedData, - series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series), + series: transformDataFrame(transformations, data.series), }; } } - // Apply field defaults & overrides - const fieldConfig = this.dataConfigSource.getFieldOverrideOptions(); - - if (fieldConfig) { - processedData = { - ...processedData, - series: applyFieldOverrides({ - timeZone: this.timeZone, - autoMinMax: true, - data: processedData.series, - ...fieldConfig, - }), - }; + if (withFieldConfig) { + // Apply field defaults & overrides + const fieldConfig = this.dataConfigSource.getFieldOverrideOptions(); + if (fieldConfig) { + processedData = { + ...processedData, + series: applyFieldOverrides({ + timeZone: this.timeZone, + autoMinMax: true, + data: processedData.series, + ...fieldConfig, + }), + }; + } } return processedData; diff --git a/public/app/plugins/datasource/dashboard/runSharedRequest.ts b/public/app/plugins/datasource/dashboard/runSharedRequest.ts index e243db51129..1d64dfdcd84 100644 --- a/public/app/plugins/datasource/dashboard/runSharedRequest.ts +++ b/public/app/plugins/datasource/dashboard/runSharedRequest.ts @@ -35,7 +35,7 @@ export function runSharedRequest(options: QueryRunnerOptions): Observable { subscriber.next(data); },