Commit 642c1a16 by Dominik Prokop Committed by GitHub

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
parent ab0238ec
......@@ -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);
......
......@@ -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);
......
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;
......
import { DataTransformerConfig } from './transformations';
import { ApplyFieldOverrideOptions } from './fieldOverrides';
export type KeyValue<T = any> = { [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;
}
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<TValue = any, TSettings = any> extends
}
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;
export interface ApplyFieldOverrideOptions {
data?: DataFrame[];
fieldOptions: FieldConfigSource;
replaceVariables: InterpolateFunction;
theme: GrafanaTheme;
timeZone?: TimeZone;
autoMinMax?: boolean;
standard?: FieldConfigEditorRegistry;
custom?: FieldConfigEditorRegistry;
}
......@@ -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;
}
......
......@@ -22,6 +22,11 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
const theme = useTheme();
const styles = getStyles(theme);
const item = editorsRegistry?.getIfExists(property.prop);
if (!item) {
return null;
}
return (
<div className={styles.wrapper}>
<OverrideHeader onRemove={onRemove} title={item.name} description={item.description} />
......
......@@ -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' }],
});
......
......@@ -35,7 +35,6 @@ export function panelEditorCleanUp(): ThunkResult<void> {
return (dispatch, getStore) => {
const dashboard = getStore().dashboard.getModel();
const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew;
if (!shouldDiscardChanges) {
const panel = getPanel();
const modifiedSaveModel = panel.getSaveModel();
......
......@@ -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",
......
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');
});
});
......@@ -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<Props, State> {
onRender = () => {
const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
this.setState(stateUpdate);
};
......@@ -220,14 +217,6 @@ export class PanelChrome extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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}
/>
......
......@@ -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",
......
......@@ -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,14 +160,33 @@ describe('PanelModel', () => {
model.changePlugin(getPanelPlugin({ id: 'table' }));
expect(model.alert).toBe(undefined);
});
});
describe('when changing to react panel from angular panel', () => {
let panelQueryRunner: any;
const onPanelTypeChanged = jest.fn();
const reactPlugin = getPanelPlugin({ id: 'react' }).setPanelChangeHandler(onPanelTypeChanged as any);
beforeEach(() => {
model.changePlugin(reactPlugin);
panelQueryRunner = model.getQueryRunner();
});
it('panelQueryRunner should be cleared', () => {
const panelQueryRunner = (model as any).queryRunner;
expect(panelQueryRunner).toBeFalsy();
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);
});
});
describe('when changing to react panel from angular panel', () => {
describe('variables interpolation', () => {
let panelQueryRunner: any;
const onPanelTypeChanged = jest.fn();
......
......@@ -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();
}
}
......
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: () => [{}],
}
);
});
......@@ -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<PanelData>;
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<PanelData> {
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
......
......@@ -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<Options> {}
......@@ -18,20 +16,12 @@ export class TablePanel extends Component<Props> {
}
render() {
const { data, height, width, replaceVariables, options } = this.props;
const { data, height, width } = this.props;
if (data.series.length < 1) {
return <div>No Table Data...</div>;
}
const dataProcessed = applyFieldOverrides({
data: data.series,
fieldOptions: options.fieldOptions,
theme: config.theme,
replaceVariables,
custom: tableFieldRegistry,
})[0];
return <Table height={height - paddingBottom} width={width} data={dataProcessed} />;
return <Table height={height - paddingBottom} width={width} data={data.series[0]} />;
}
}
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment