From 642c1a16dd4743fe1df660cf1e009594ce7b2ea3 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 16 Mar 2020 14:26:03 +0100 Subject: [PATCH] FieldOverrides: Apply field overrides in PanelQueryRunner (#22439) * Apply field overrides in PanelChrome * Move applyFieldOverrides to panel query runner * Review updates * Make sure overrides are applied back on souce panel when exiting the new edit mode * TS ignores in est * Make field display work in viz repeater * Review updates * Review and test updates * Change the way overrides and trransformations are retrieved in PQR * Minor updates after review * Fix null checks --- .../src/field/fieldDisplay.test.ts | 50 +++++++----- .../grafana-data/src/field/fieldDisplay.ts | 5 +- .../grafana-data/src/field/fieldOverrides.ts | 16 +--- packages/grafana-data/src/types/data.ts | 14 ++++ .../grafana-data/src/types/fieldOverrides.ts | 22 +++++- packages/grafana-data/src/types/panel.ts | 7 +- .../PanelEditor/DynamicConfigValueEditor.tsx | 5 ++ .../PanelEditor/state/actions.test.ts | 4 + .../components/PanelEditor/state/actions.ts | 1 - .../__snapshots__/DashboardPage.test.tsx.snap | 7 ++ .../dashboard/dashgrid/PanelChrome.test.tsx | 28 ------- .../dashboard/dashgrid/PanelChrome.tsx | 18 +---- .../__snapshots__/DashboardGrid.test.tsx.snap | 20 +++++ .../dashboard/state/PanelModel.test.ts | 48 ++++++++++-- .../features/dashboard/state/PanelModel.ts | 48 +++++++++++- .../dashboard/state/PanelQueryRunner.test.ts | 77 +++++++++++++++++-- .../dashboard/state/PanelQueryRunner.ts | 65 ++++++++++------ .../app/plugins/panel/table2/TablePanel.tsx | 16 +--- scripts/ci-frontend-metrics.sh | 2 +- 19 files changed, 315 insertions(+), 138 deletions(-) delete mode 100644 public/app/features/dashboard/dashgrid/PanelChrome.test.tsx diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index d274c688e3f..83d3ea54f5a 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -19,7 +19,6 @@ describe('FieldDisplay', () => { shouldApply: () => true, } as any; - console.log('Init tegistry'); standardFieldConfigEditorRegistry.setInit(() => { return [mappings]; }); @@ -168,48 +167,57 @@ describe('FieldDisplay', () => { describe('Value mapping', () => { it('should apply value mapping', () => { + const mappingConfig = [ + { + id: 1, + operator: '', + text: 'Value mapped to text', + type: MappingType.ValueToText, + value: '1', + }, + ]; const options = createDisplayOptions({ fieldOptions: { calcs: [ReducerID.first], override: {}, defaults: { - mappings: [ - { - id: 1, - operator: '', - text: 'Value mapped to text', - type: MappingType.ValueToText, - value: 1, - }, - ], + mappings: mappingConfig, }, }, }); + options.data![0].fields[1]!.config = { mappings: mappingConfig }; + options.data![0].fields[2]!.config = { mappings: mappingConfig }; + const result = getFieldDisplayValues(options); expect(result[0].display.text).toEqual('Value mapped to text'); }); it('should apply range value mapping', () => { const mappedValue = 'Range mapped to text'; + const mappingConfig = [ + { + id: 1, + operator: '', + text: mappedValue, + type: MappingType.RangeToText, + value: 1, + from: '1', + to: '3', + }, + ]; const options = createDisplayOptions({ fieldOptions: { values: true, override: {}, defaults: { - mappings: [ - { - id: 1, - operator: '', - text: mappedValue, - type: MappingType.RangeToText, - value: 1, - from: 1, - to: 3, - }, - ], + mappings: mappingConfig, }, }, }); + + options.data![0].fields[1]!.config = { mappings: mappingConfig }; + options.data![0].fields[2]!.config = { mappings: mappingConfig }; + const result = getFieldDisplayValues(options); expect(result[0].display.text).toEqual(mappedValue); diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index eb633999d1c..647c172b650 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -18,7 +18,6 @@ import { GrafanaTheme } from '../types/theme'; import { ReducerID, reduceField } from '../transformations/fieldReducer'; import { ScopedVars } from '../types/ScopedVars'; import { getTimeField } from '../dataframe/processDataFrame'; -import { applyFieldOverrides } from './fieldOverrides'; export interface FieldDisplayOptions extends FieldConfigSource { values?: boolean; // If true show each row value @@ -91,8 +90,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi const values: FieldDisplay[] = []; if (options.data) { - const data = applyFieldOverrides(options); - + // Field overrides are applied already + const data = options.data; let hitLimit = false; const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 134158a33ec..234eb786ee4 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -1,19 +1,16 @@ import { - GrafanaTheme, DynamicConfigValue, FieldConfig, - InterpolateFunction, DataFrame, Field, FieldType, - FieldConfigSource, ThresholdsMode, FieldColorMode, ColorScheme, - TimeZone, FieldConfigEditorRegistry, FieldOverrideContext, ScopedVars, + ApplyFieldOverrideOptions, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; @@ -32,17 +29,6 @@ interface GlobalMinMax { max: number; } -export interface ApplyFieldOverrideOptions { - data?: DataFrame[]; - fieldOptions: FieldConfigSource; - replaceVariables: InterpolateFunction; - theme: GrafanaTheme; - timeZone?: TimeZone; - autoMinMax?: boolean; - standard?: FieldConfigEditorRegistry; - custom?: FieldConfigEditorRegistry; -} - export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax { let min = Number.MAX_VALUE; let max = Number.MIN_VALUE; diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 59cbd95dc26..0c2568a4b33 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -1,5 +1,11 @@ +import { DataTransformerConfig } from './transformations'; +import { ApplyFieldOverrideOptions } from './fieldOverrides'; + export type KeyValue = { [s: string]: T }; +/** + * Represent panel data loading state. + */ export enum LoadingState { NotStarted = 'NotStarted', Loading = 'Loading', @@ -90,3 +96,11 @@ export interface AnnotationEvent { // Currently used to merge annotations from alerts and dashboard source?: any; // source.type === 'dashboard' } + +/** + * Describes and API for exposing panel specific data configurations. + */ +export interface DataConfigSource { + getTransformations: () => DataTransformerConfig[] | undefined; + getFieldOverrideOptions: () => ApplyFieldOverrideOptions | undefined; +} diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index eb852a54e6f..1dbb41ace44 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -1,5 +1,14 @@ import { ComponentType } from 'react'; -import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestionsScope, VariableSuggestion } from '../types'; +import { + MatcherConfig, + FieldConfig, + Field, + DataFrame, + VariableSuggestionsScope, + VariableSuggestion, + GrafanaTheme, + TimeZone, +} from '../types'; import { Registry, RegistryItem } from '../utils'; import { InterpolateFunction } from './panel'; @@ -62,3 +71,14 @@ export interface FieldPropertyEditorItem extends } export type FieldConfigEditorRegistry = Registry; + +export interface ApplyFieldOverrideOptions { + data?: DataFrame[]; + fieldOptions: FieldConfigSource; + replaceVariables: InterpolateFunction; + theme: GrafanaTheme; + timeZone?: TimeZone; + autoMinMax?: boolean; + standard?: FieldConfigEditorRegistry; + custom?: FieldConfigEditorRegistry; +} diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index 93c34ed99d6..c91afe8ba14 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -17,11 +17,16 @@ export interface PanelPluginMeta extends PluginMeta { export interface PanelData { state: LoadingState; + /** + * Contains data frames with field overrides applied + */ series: DataFrame[]; request?: DataQueryRequest; timings?: DataQueryTimings; error?: DataQueryError; - // Contains the range from the request or a shifted time range if a request uses relative time + /** + * Contains the range from the request or a shifted time range if a request uses relative time + */ timeRange: TimeRange; } diff --git a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx index 0792d81e117..11b410f927e 100644 --- a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx @@ -22,6 +22,11 @@ export const DynamicConfigValueEditor: React.FC = const theme = useTheme(); const styles = getStyles(theme); const item = editorsRegistry?.getIfExists(property.prop); + + if (!item) { + return null; + } + return (
diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts index c4b675ecf40..14d01bbbf52 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts @@ -92,6 +92,10 @@ describe('panelEditor actions', () => { it('should discard changes when shouldDiscardChanges is true', async () => { const sourcePanel = new PanelModel({ id: 12, type: 'graph' }); + sourcePanel.plugin = { + customFieldConfigs: {}, + } as any; + const dashboard = new DashboardModel({ panels: [{ id: 12, type: 'graph' }], }); diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index db72d35e3cd..1b9124438a3 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -35,7 +35,6 @@ export function panelEditorCleanUp(): ThunkResult { return (dispatch, getStore) => { const dashboard = getStore().dashboard.getModel(); const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew; - if (!shouldDiscardChanges) { const panel = getPanel(); const modifiedSaveModel = panel.getSaveModel(); diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index e7ab9259feb..d05b05a89e5 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -68,6 +68,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -185,6 +186,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -282,6 +284,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -411,6 +414,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -526,6 +530,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -626,6 +631,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -723,6 +729,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti }, "id": 1, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx deleted file mode 100644 index 32ce937b91e..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { PanelChrome } from './PanelChrome'; - -describe('PanelChrome', () => { - let chrome: PanelChrome; - - beforeEach(() => { - chrome = new PanelChrome({ - panel: { - scopedVars: { - aaa: { value: 'AAA', text: 'upperA' }, - bbb: { value: 'BBB', text: 'upperB' }, - }, - }, - isFullscreen: false, - } as any); - }); - - it('Should replace a panel variable', () => { - const out = chrome.replaceVariables('hello $aaa'); - expect(out).toBe('hello AAA'); - }); - - it('But it should prefer the local variable value', () => { - const extra = { aaa: { text: '???', value: 'XXX' } }; - const out = chrome.replaceVariables('hello $aaa and $bbb', extra); - expect(out).toBe('hello XXX and BBB'); - }); -}); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 2eca35e1162..ff59e4e129b 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -10,14 +10,12 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { profiler } from 'app/core/profiler'; import { getProcessedDataFrames } from '../state/runRequest'; -import templateSrv from 'app/features/templating/template_srv'; import config from 'app/core/config'; // Types import { DashboardModel, PanelModel } from '../state'; import { PANEL_BORDER } from 'app/core/constants'; import { LoadingState, - ScopedVars, AbsoluteTimeRange, DefaultTimeRange, toUtc, @@ -212,7 +210,6 @@ export class PanelChrome extends PureComponent { onRender = () => { const stateUpdate = { renderCounter: this.state.renderCounter + 1 }; - this.setState(stateUpdate); }; @@ -220,14 +217,6 @@ export class PanelChrome extends PureComponent { this.props.panel.updateOptions(options); }; - replaceVariables = (value: string, extraVars?: ScopedVars, format?: string) => { - let vars = this.props.panel.scopedVars; - if (extraVars) { - vars = vars ? { ...vars, ...extraVars } : extraVars; - } - return templateSrv.replace(value, vars, format); - }; - onPanelError = (message: string) => { if (this.state.errorMessage !== message) { this.setState({ errorMessage: message }); @@ -273,16 +262,15 @@ export class PanelChrome extends PureComponent { const PanelComponent = plugin.panel; const timeRange = data.timeRange || this.timeSrv.timeRange(); - const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight; const chromePadding = plugin.noPadding ? 0 : theme.panelPadding; const panelWidth = width - chromePadding * 2 - PANEL_BORDER; const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER; - const panelContentClassNames = classNames({ 'panel-content': true, 'panel-content--no-padding': plugin.noPadding, }); + const panelOptions = panel.getOptions(); return ( <> @@ -292,12 +280,12 @@ export class PanelChrome extends PureComponent { data={data} timeRange={timeRange} timeZone={this.props.dashboard.getTimezone()} - options={panel.getOptions()} + options={panelOptions} transparent={panel.transparent} width={panelWidth} height={innerPanelHeight} renderCounter={renderCounter} - replaceVariables={this.replaceVariables} + replaceVariables={panel.replaceVariables} onOptionsChange={this.onOptionsChange} onChangeTimeRange={this.onChangeTimeRange} /> diff --git a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap index 175a6108d64..86a60132f24 100644 --- a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap +++ b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap @@ -144,6 +144,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 1, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -171,6 +172,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 2, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -198,6 +200,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 3, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -225,6 +228,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 4, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -278,6 +282,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 1, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -390,6 +395,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 1, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -417,6 +423,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 2, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -444,6 +451,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 3, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -471,6 +479,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 4, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -524,6 +533,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 2, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -636,6 +646,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 1, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -663,6 +674,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 2, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -690,6 +702,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 3, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -717,6 +730,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 4, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -770,6 +784,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 3, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -882,6 +897,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 1, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -909,6 +925,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 2, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -936,6 +953,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 3, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -963,6 +981,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 4, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", @@ -1016,6 +1035,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` "id": 4, "isInView": false, "options": Object {}, + "replaceVariables": [Function], "targets": Array [ Object { "refId": "A", diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index a8b21121037..60f209949ab 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -111,6 +111,25 @@ describe('PanelModel', () => { expect(saveModel.events).toBe(undefined); }); + describe('variables interpolation', () => { + beforeEach(() => { + model.scopedVars = { + aaa: { value: 'AAA', text: 'upperA' }, + bbb: { value: 'BBB', text: 'upperB' }, + }; + }); + it('should interpolate variables', () => { + const out = model.replaceVariables('hello $aaa'); + expect(out).toBe('hello AAA'); + }); + + it('should prefer the local variable value', () => { + const extra = { aaa: { text: '???', value: 'XXX' } }; + const out = model.replaceVariables('hello $aaa and $bbb', extra); + expect(out).toBe('hello XXX and BBB'); + }); + }); + describe('when changing panel type', () => { const newPanelPluginDefaults = { showThresholdLabels: false, @@ -141,11 +160,6 @@ describe('PanelModel', () => { model.changePlugin(getPanelPlugin({ id: 'table' })); expect(model.alert).toBe(undefined); }); - - it('panelQueryRunner should be cleared', () => { - const panelQueryRunner = (model as any).queryRunner; - expect(panelQueryRunner).toBeFalsy(); - }); }); describe('when changing to react panel from angular panel', () => { @@ -171,5 +185,29 @@ describe('PanelModel', () => { expect(panelQueryRunner).toBe(sameQueryRunner); }); }); + + describe('variables interpolation', () => { + let panelQueryRunner: any; + + const onPanelTypeChanged = jest.fn(); + const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any); + + beforeEach(() => { + model.changePlugin(reactPlugin); + panelQueryRunner = model.getQueryRunner(); + }); + + it('should call react onPanelTypeChanged', () => { + expect(onPanelTypeChanged.mock.calls.length).toBe(1); + expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table'); + expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined(); + }); + + it('getQueryRunner() should return same instance after changing to another react panel', () => { + model.changePlugin(getPanelPlugin({ id: 'react2' })); + const sameQueryRunner = model.getQueryRunner(); + expect(panelQueryRunner).toBe(sameQueryRunner); + }); + }); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 1c36b15ce91..8b37445e8cd 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,8 +3,10 @@ import _ from 'lodash'; // Utils import { Emitter } from 'app/core/utils/emitter'; import { getNextRefIdChar } from 'app/core/utils/query'; +import templateSrv from 'app/features/templating/template_srv'; // Types import { + DataConfigSource, DataLink, DataQuery, DataQueryResponseData, @@ -41,6 +43,7 @@ const notPersistedProperties: { [str: string]: boolean } = { cachedPluginOptions: true, plugin: true, queryRunner: true, + replaceVariables: true, }; // For angular panels we need to clean up properties when changing type @@ -88,7 +91,7 @@ const defaults: any = { options: {}, }; -export class PanelModel { +export class PanelModel implements DataConfigSource { /* persisted id, used in URL to identify a panel */ id: number; gridPos: GridPos; @@ -144,6 +147,7 @@ export class PanelModel { // this should not be removed in save model as exporter needs to templatize it this.datasource = null; this.restoreModel(model); + this.replaceVariables = this.replaceVariables.bind(this); } /** Given a persistened PanelModel restores property values */ @@ -176,6 +180,7 @@ export class PanelModel { updateOptions(options: object) { this.options = options; + this.resendLastResult(); this.render(); } @@ -283,6 +288,7 @@ export class PanelModel { } this.applyPluginOptionDefaults(plugin); + this.resendLastResult(); } changePlugin(newPlugin: PanelPlugin) { @@ -319,6 +325,9 @@ export class PanelModel { // switch this.type = pluginId; this.plugin = newPlugin; + + // For some reason I need to rebind replace variables here, otherwise the viz repeater does not work + this.replaceVariables = this.replaceVariables.bind(this); this.applyPluginOptionDefaults(newPlugin); if (newPlugin.onPanelMigration) { @@ -363,10 +372,26 @@ export class PanelModel { return clone; } + getTransformations() { + return this.transformations; + } + + getFieldOverrideOptions() { + if (!this.plugin) { + return undefined; + } + + return { + fieldOptions: this.options.fieldOptions, + replaceVariables: this.replaceVariables, + custom: this.plugin.customFieldConfigs, + theme: config.theme, + }; + } + getQueryRunner(): PanelQueryRunner { if (!this.queryRunner) { - this.queryRunner = new PanelQueryRunner(); - this.setTransformations(this.transformations); + this.queryRunner = new PanelQueryRunner(this); } return this.queryRunner; } @@ -390,7 +415,22 @@ export class PanelModel { setTransformations(transformations: DataTransformerConfig[]) { this.transformations = transformations; - this.getQueryRunner().setTransformations(transformations); + } + + replaceVariables(value: string, extraVars?: ScopedVars, format?: string) { + let vars = this.scopedVars; + if (extraVars) { + vars = vars ? { ...vars, ...extraVars } : extraVars; + } + return templateSrv.replace(value, vars, format); + } + + resendLastResult() { + if (!this.plugin) { + return; + } + + this.getQueryRunner().resendLastResult(); } } diff --git a/public/app/features/dashboard/state/PanelQueryRunner.test.ts b/public/app/features/dashboard/state/PanelQueryRunner.test.ts index 536f2fb2b51..2d44a648130 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.test.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.test.ts @@ -1,10 +1,18 @@ import { PanelQueryRunner } from './PanelQueryRunner'; -import { DataQueryRequest, dateTime, PanelData, ScopedVars } from '@grafana/data'; +// Importing this way to be able to spy on grafana/data +import * as grafanaData from '@grafana/data'; +import { DataConfigSource, DataQueryRequest, GrafanaTheme, PanelData, ScopedVars } from '@grafana/data'; import { DashboardModel } from './index'; import { setEchoSrv } from '@grafana/runtime'; import { Echo } from '../../../core/services/echo/Echo'; jest.mock('app/core/services/backend_srv'); +jest.mock('app/core/config', () => ({ + config: { featureToggles: { transformations: true } }, + getConfig: () => ({ + featureToggles: {}, + }), +})); const dashboardModel = new DashboardModel({ panels: [{ id: 1, type: 'graph' }], @@ -37,16 +45,19 @@ interface ScenarioContext { type ScenarioFn = (ctx: ScenarioContext) => void; -function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn) { +function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn, panelConfig?: DataConfigSource) { describe(description, () => { let setupFn = () => {}; - + const defaultPanelConfig: DataConfigSource = { + getFieldOverrideOptions: () => undefined, + getTransformations: () => undefined, + }; const ctx: ScenarioContext = { widthPixels: 200, scopedVars: { server: { text: 'Server1', value: 'server-1' }, }, - runner: new PanelQueryRunner(), + runner: new PanelQueryRunner(panelConfig || defaultPanelConfig), setup: (fn: () => void) => { setupFn = fn; }, @@ -85,15 +96,15 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn widthPixels: ctx.widthPixels, maxDataPoints: ctx.maxDataPoints, timeRange: { - from: dateTime().subtract(1, 'days'), - to: dateTime(), + from: grafanaData.dateTime().subtract(1, 'days'), + to: grafanaData.dateTime(), raw: { from: '1h', to: 'now' }, }, panelId: 1, queries: [{ refId: 'A', test: 1 }], }; - ctx.runner = new PanelQueryRunner(); + ctx.runner = new PanelQueryRunner(panelConfig || defaultPanelConfig); ctx.runner.getData().subscribe({ next: (data: PanelData) => { ctx.res = data; @@ -182,4 +193,56 @@ describe('PanelQueryRunner', () => { expect(ctx.queryCalledWith?.maxDataPoints).toBe(10); }); }); + + describeQueryRunnerScenario( + 'field overrides', + ctx => { + it('should apply when field override options are set', async () => { + const spy = jest.spyOn(grafanaData, 'applyFieldOverrides'); + + ctx.runner.getData().subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + expect(spy).toBeCalled(); + }); + }, + { + getFieldOverrideOptions: () => ({ + fieldOptions: { + defaults: { + unit: 'm/s', + }, + // @ts-ignore + overrides: [], + }, + replaceVariables: v => v, + theme: {} as GrafanaTheme, + }), + getTransformations: () => undefined, + } + ); + + describeQueryRunnerScenario( + 'transformations', + ctx => { + it('should apply when transformations are set', async () => { + const spy = jest.spyOn(grafanaData, 'transformDataFrame'); + + ctx.runner.getData().subscribe({ + next: (data: PanelData) => { + return data; + }, + }); + + expect(spy).toBeCalled(); + }); + }, + { + getFieldOverrideOptions: () => undefined, + // @ts-ignore + getTransformations: () => [{}], + } + ); }); diff --git a/public/app/features/dashboard/state/PanelQueryRunner.ts b/public/app/features/dashboard/state/PanelQueryRunner.ts index 6e20d650e67..1952ad34b1b 100644 --- a/public/app/features/dashboard/state/PanelQueryRunner.ts +++ b/public/app/features/dashboard/state/PanelQueryRunner.ts @@ -23,6 +23,8 @@ import { DataTransformerConfig, transformDataFrame, ScopedVars, + applyFieldOverrides, + DataConfigSource, } from '@grafana/data'; export interface QueryRunnerOptions< @@ -53,36 +55,51 @@ function getNextRequestId() { export class PanelQueryRunner { private subject?: ReplaySubject; private subscription?: Unsubscribable; - private transformations?: DataTransformerConfig[]; private lastResult?: PanelData; + private dataConfigSource: DataConfigSource; - constructor() { + constructor(dataConfigSource: DataConfigSource) { this.subject = new ReplaySubject(1); + this.dataConfigSource = dataConfigSource; } /** * Returns an observable that subscribes to the shared multi-cast subject (that reply last result). */ getData(transform = true): Observable { - if (transform) { - return this.subject.pipe( - map((data: PanelData) => { - if (this.hasTransformations()) { - const newSeries = transformDataFrame(this.transformations, data.series); - return { ...data, series: newSeries }; - } - return data; - }) - ); - } - - // Just pass it directly - return this.subject.pipe(); + return this.subject.pipe( + map((data: PanelData) => { + let processedData = data; + // apply transformations + if (transform && this.hasTransformations()) { + processedData = { + ...processedData, + series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series), + }; + } + // apply overrides + if (this.hasFieldOverrideOptions()) { + processedData = { + ...processedData, + series: applyFieldOverrides({ + data: processedData.series, + ...this.dataConfigSource.getFieldOverrideOptions(), + }), + }; + } + return processedData; + }) + ); } - hasTransformations() { - return config.featureToggles.transformations && this.transformations && this.transformations.length > 0; - } + hasTransformations = () => { + const transformations = this.dataConfigSource.getTransformations(); + return config.featureToggles.transformations && transformations && transformations.length > 0; + }; + + hasFieldOverrideOptions = () => { + return this.dataConfigSource.getFieldOverrideOptions(); + }; async run(options: QueryRunnerOptions) { const { @@ -98,7 +115,6 @@ export class PanelQueryRunner { maxDataPoints, scopedVars, minInterval, - // delayStateNotification, } = options; if (isSharedDashboardQuery(datasource)) { @@ -164,6 +180,7 @@ export class PanelQueryRunner { this.subscription = observable.subscribe({ next: (data: PanelData) => { this.lastResult = preProcessPanelData(data, this.lastResult); + // Store preprocessed query results for applying overrides later on in the pipeline this.subject.next(this.lastResult); }, }); @@ -174,9 +191,11 @@ export class PanelQueryRunner { this.lastResult = data; }; - setTransformations(transformations?: DataTransformerConfig[]) { - this.transformations = transformations; - } + resendLastResult = () => { + if (this.lastResult) { + this.subject.next(this.lastResult); + } + }; /** * Called when the panel is closed diff --git a/public/app/plugins/panel/table2/TablePanel.tsx b/public/app/plugins/panel/table2/TablePanel.tsx index 5e3219c5335..4021c26b4ba 100644 --- a/public/app/plugins/panel/table2/TablePanel.tsx +++ b/public/app/plugins/panel/table2/TablePanel.tsx @@ -3,10 +3,8 @@ import React, { Component } from 'react'; // Types import { Table } from '@grafana/ui'; -import { PanelProps, applyFieldOverrides } from '@grafana/data'; +import { PanelProps } from '@grafana/data'; import { Options } from './types'; -import { config } from 'app/core/config'; -import { tableFieldRegistry } from './custom'; interface Props extends PanelProps {} @@ -18,20 +16,12 @@ export class TablePanel extends Component { } render() { - const { data, height, width, replaceVariables, options } = this.props; + const { data, height, width } = this.props; if (data.series.length < 1) { return
No Table Data...
; } - const dataProcessed = applyFieldOverrides({ - data: data.series, - fieldOptions: options.fieldOptions, - theme: config.theme, - replaceVariables, - custom: tableFieldRegistry, - })[0]; - - return ; + return
; } } diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 6bbcfd22c20..6b39ee7d9db 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -3,7 +3,7 @@ echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=824 +ERROR_COUNT_LIMIT=821 DIRECTIVES_LIMIT=172 CONTROLLERS_LIMIT=139