mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
ab0238eced
commit
642c1a16dd
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,11 @@
|
||||
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;
|
||||
}
|
||||
|
@ -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<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",
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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: () => [{}],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user