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,
|
shouldApply: () => true,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
console.log('Init tegistry');
|
|
||||||
standardFieldConfigEditorRegistry.setInit(() => {
|
standardFieldConfigEditorRegistry.setInit(() => {
|
||||||
return [mappings];
|
return [mappings];
|
||||||
});
|
});
|
||||||
@ -168,48 +167,57 @@ describe('FieldDisplay', () => {
|
|||||||
|
|
||||||
describe('Value mapping', () => {
|
describe('Value mapping', () => {
|
||||||
it('should apply value mapping', () => {
|
it('should apply value mapping', () => {
|
||||||
const options = createDisplayOptions({
|
const mappingConfig = [
|
||||||
fieldOptions: {
|
|
||||||
calcs: [ReducerID.first],
|
|
||||||
override: {},
|
|
||||||
defaults: {
|
|
||||||
mappings: [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
operator: '',
|
operator: '',
|
||||||
text: 'Value mapped to text',
|
text: 'Value mapped to text',
|
||||||
type: MappingType.ValueToText,
|
type: MappingType.ValueToText,
|
||||||
value: 1,
|
value: '1',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
const options = createDisplayOptions({
|
||||||
|
fieldOptions: {
|
||||||
|
calcs: [ReducerID.first],
|
||||||
|
override: {},
|
||||||
|
defaults: {
|
||||||
|
mappings: mappingConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
options.data![0].fields[1]!.config = { mappings: mappingConfig };
|
||||||
|
options.data![0].fields[2]!.config = { mappings: mappingConfig };
|
||||||
|
|
||||||
const result = getFieldDisplayValues(options);
|
const result = getFieldDisplayValues(options);
|
||||||
expect(result[0].display.text).toEqual('Value mapped to text');
|
expect(result[0].display.text).toEqual('Value mapped to text');
|
||||||
});
|
});
|
||||||
it('should apply range value mapping', () => {
|
it('should apply range value mapping', () => {
|
||||||
const mappedValue = 'Range mapped to text';
|
const mappedValue = 'Range mapped to text';
|
||||||
const options = createDisplayOptions({
|
const mappingConfig = [
|
||||||
fieldOptions: {
|
|
||||||
values: true,
|
|
||||||
override: {},
|
|
||||||
defaults: {
|
|
||||||
mappings: [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
operator: '',
|
operator: '',
|
||||||
text: mappedValue,
|
text: mappedValue,
|
||||||
type: MappingType.RangeToText,
|
type: MappingType.RangeToText,
|
||||||
value: 1,
|
value: 1,
|
||||||
from: 1,
|
from: '1',
|
||||||
to: 3,
|
to: '3',
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
const options = createDisplayOptions({
|
||||||
|
fieldOptions: {
|
||||||
|
values: true,
|
||||||
|
override: {},
|
||||||
|
defaults: {
|
||||||
|
mappings: mappingConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
options.data![0].fields[1]!.config = { mappings: mappingConfig };
|
||||||
|
options.data![0].fields[2]!.config = { mappings: mappingConfig };
|
||||||
|
|
||||||
const result = getFieldDisplayValues(options);
|
const result = getFieldDisplayValues(options);
|
||||||
|
|
||||||
expect(result[0].display.text).toEqual(mappedValue);
|
expect(result[0].display.text).toEqual(mappedValue);
|
||||||
|
@ -18,7 +18,6 @@ import { GrafanaTheme } from '../types/theme';
|
|||||||
import { ReducerID, reduceField } from '../transformations/fieldReducer';
|
import { ReducerID, reduceField } from '../transformations/fieldReducer';
|
||||||
import { ScopedVars } from '../types/ScopedVars';
|
import { ScopedVars } from '../types/ScopedVars';
|
||||||
import { getTimeField } from '../dataframe/processDataFrame';
|
import { getTimeField } from '../dataframe/processDataFrame';
|
||||||
import { applyFieldOverrides } from './fieldOverrides';
|
|
||||||
|
|
||||||
export interface FieldDisplayOptions extends FieldConfigSource {
|
export interface FieldDisplayOptions extends FieldConfigSource {
|
||||||
values?: boolean; // If true show each row value
|
values?: boolean; // If true show each row value
|
||||||
@ -91,8 +90,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
const values: FieldDisplay[] = [];
|
const values: FieldDisplay[] = [];
|
||||||
|
|
||||||
if (options.data) {
|
if (options.data) {
|
||||||
const data = applyFieldOverrides(options);
|
// Field overrides are applied already
|
||||||
|
const data = options.data;
|
||||||
let hitLimit = false;
|
let hitLimit = false;
|
||||||
const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||||
const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
|
const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
GrafanaTheme,
|
|
||||||
DynamicConfigValue,
|
DynamicConfigValue,
|
||||||
FieldConfig,
|
FieldConfig,
|
||||||
InterpolateFunction,
|
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
FieldConfigSource,
|
|
||||||
ThresholdsMode,
|
ThresholdsMode,
|
||||||
FieldColorMode,
|
FieldColorMode,
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
TimeZone,
|
|
||||||
FieldConfigEditorRegistry,
|
FieldConfigEditorRegistry,
|
||||||
FieldOverrideContext,
|
FieldOverrideContext,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
|
ApplyFieldOverrideOptions,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
import { fieldMatchers, ReducerID, reduceField } from '../transformations';
|
||||||
import { FieldMatcher } from '../types/transformations';
|
import { FieldMatcher } from '../types/transformations';
|
||||||
@ -32,17 +29,6 @@ interface GlobalMinMax {
|
|||||||
max: number;
|
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 {
|
export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax {
|
||||||
let min = Number.MAX_VALUE;
|
let min = Number.MAX_VALUE;
|
||||||
let max = Number.MIN_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 };
|
export type KeyValue<T = any> = { [s: string]: T };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent panel data loading state.
|
||||||
|
*/
|
||||||
export enum LoadingState {
|
export enum LoadingState {
|
||||||
NotStarted = 'NotStarted',
|
NotStarted = 'NotStarted',
|
||||||
Loading = 'Loading',
|
Loading = 'Loading',
|
||||||
@ -90,3 +96,11 @@ export interface AnnotationEvent {
|
|||||||
// Currently used to merge annotations from alerts and dashboard
|
// Currently used to merge annotations from alerts and dashboard
|
||||||
source?: any; // source.type === '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 { 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 { Registry, RegistryItem } from '../utils';
|
||||||
import { InterpolateFunction } from './panel';
|
import { InterpolateFunction } from './panel';
|
||||||
|
|
||||||
@ -62,3 +71,14 @@ export interface FieldPropertyEditorItem<TValue = any, TSettings = any> extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;
|
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 {
|
export interface PanelData {
|
||||||
state: LoadingState;
|
state: LoadingState;
|
||||||
|
/**
|
||||||
|
* Contains data frames with field overrides applied
|
||||||
|
*/
|
||||||
series: DataFrame[];
|
series: DataFrame[];
|
||||||
request?: DataQueryRequest;
|
request?: DataQueryRequest;
|
||||||
timings?: DataQueryTimings;
|
timings?: DataQueryTimings;
|
||||||
error?: DataQueryError;
|
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;
|
timeRange: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,11 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const item = editorsRegistry?.getIfExists(property.prop);
|
const item = editorsRegistry?.getIfExists(property.prop);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<OverrideHeader onRemove={onRemove} title={item.name} description={item.description} />
|
<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 () => {
|
it('should discard changes when shouldDiscardChanges is true', async () => {
|
||||||
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
const sourcePanel = new PanelModel({ id: 12, type: 'graph' });
|
||||||
|
sourcePanel.plugin = {
|
||||||
|
customFieldConfigs: {},
|
||||||
|
} as any;
|
||||||
|
|
||||||
const dashboard = new DashboardModel({
|
const dashboard = new DashboardModel({
|
||||||
panels: [{ id: 12, type: 'graph' }],
|
panels: [{ id: 12, type: 'graph' }],
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,6 @@ export function panelEditorCleanUp(): ThunkResult<void> {
|
|||||||
return (dispatch, getStore) => {
|
return (dispatch, getStore) => {
|
||||||
const dashboard = getStore().dashboard.getModel();
|
const dashboard = getStore().dashboard.getModel();
|
||||||
const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew;
|
const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew;
|
||||||
|
|
||||||
if (!shouldDiscardChanges) {
|
if (!shouldDiscardChanges) {
|
||||||
const panel = getPanel();
|
const panel = getPanel();
|
||||||
const modifiedSaveModel = panel.getSaveModel();
|
const modifiedSaveModel = panel.getSaveModel();
|
||||||
|
@ -68,6 +68,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -185,6 +186,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -282,6 +284,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -411,6 +414,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -526,6 +530,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -626,6 +631,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -723,6 +729,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
|||||||
},
|
},
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"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 { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
import { profiler } from 'app/core/profiler';
|
import { profiler } from 'app/core/profiler';
|
||||||
import { getProcessedDataFrames } from '../state/runRequest';
|
import { getProcessedDataFrames } from '../state/runRequest';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
// Types
|
// Types
|
||||||
import { DashboardModel, PanelModel } from '../state';
|
import { DashboardModel, PanelModel } from '../state';
|
||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import {
|
import {
|
||||||
LoadingState,
|
LoadingState,
|
||||||
ScopedVars,
|
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
DefaultTimeRange,
|
DefaultTimeRange,
|
||||||
toUtc,
|
toUtc,
|
||||||
@ -212,7 +210,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
onRender = () => {
|
onRender = () => {
|
||||||
const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
|
const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
|
||||||
|
|
||||||
this.setState(stateUpdate);
|
this.setState(stateUpdate);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,14 +217,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
this.props.panel.updateOptions(options);
|
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) => {
|
onPanelError = (message: string) => {
|
||||||
if (this.state.errorMessage !== message) {
|
if (this.state.errorMessage !== message) {
|
||||||
this.setState({ errorMessage: message });
|
this.setState({ errorMessage: message });
|
||||||
@ -273,16 +262,15 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
const PanelComponent = plugin.panel;
|
const PanelComponent = plugin.panel;
|
||||||
const timeRange = data.timeRange || this.timeSrv.timeRange();
|
const timeRange = data.timeRange || this.timeSrv.timeRange();
|
||||||
|
|
||||||
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
|
const headerHeight = this.hasOverlayHeader() ? 0 : theme.panelHeaderHeight;
|
||||||
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
|
const chromePadding = plugin.noPadding ? 0 : theme.panelPadding;
|
||||||
const panelWidth = width - chromePadding * 2 - PANEL_BORDER;
|
const panelWidth = width - chromePadding * 2 - PANEL_BORDER;
|
||||||
const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER;
|
const innerPanelHeight = height - headerHeight - chromePadding * 2 - PANEL_BORDER;
|
||||||
|
|
||||||
const panelContentClassNames = classNames({
|
const panelContentClassNames = classNames({
|
||||||
'panel-content': true,
|
'panel-content': true,
|
||||||
'panel-content--no-padding': plugin.noPadding,
|
'panel-content--no-padding': plugin.noPadding,
|
||||||
});
|
});
|
||||||
|
const panelOptions = panel.getOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -292,12 +280,12 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
data={data}
|
data={data}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={this.props.dashboard.getTimezone()}
|
timeZone={this.props.dashboard.getTimezone()}
|
||||||
options={panel.getOptions()}
|
options={panelOptions}
|
||||||
transparent={panel.transparent}
|
transparent={panel.transparent}
|
||||||
width={panelWidth}
|
width={panelWidth}
|
||||||
height={innerPanelHeight}
|
height={innerPanelHeight}
|
||||||
renderCounter={renderCounter}
|
renderCounter={renderCounter}
|
||||||
replaceVariables={this.replaceVariables}
|
replaceVariables={panel.replaceVariables}
|
||||||
onOptionsChange={this.onOptionsChange}
|
onOptionsChange={this.onOptionsChange}
|
||||||
onChangeTimeRange={this.onChangeTimeRange}
|
onChangeTimeRange={this.onChangeTimeRange}
|
||||||
/>
|
/>
|
||||||
|
@ -144,6 +144,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -171,6 +172,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -198,6 +200,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -225,6 +228,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -278,6 +282,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -390,6 +395,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -417,6 +423,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -444,6 +451,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -471,6 +479,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -524,6 +533,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -636,6 +646,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -663,6 +674,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -690,6 +702,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -717,6 +730,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -770,6 +784,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -882,6 +897,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -909,6 +925,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 2,
|
"id": 2,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -936,6 +953,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 3,
|
"id": 3,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -963,6 +981,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
@ -1016,6 +1035,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
|||||||
"id": 4,
|
"id": 4,
|
||||||
"isInView": false,
|
"isInView": false,
|
||||||
"options": Object {},
|
"options": Object {},
|
||||||
|
"replaceVariables": [Function],
|
||||||
"targets": Array [
|
"targets": Array [
|
||||||
Object {
|
Object {
|
||||||
"refId": "A",
|
"refId": "A",
|
||||||
|
@ -111,6 +111,25 @@ describe('PanelModel', () => {
|
|||||||
expect(saveModel.events).toBe(undefined);
|
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', () => {
|
describe('when changing panel type', () => {
|
||||||
const newPanelPluginDefaults = {
|
const newPanelPluginDefaults = {
|
||||||
showThresholdLabels: false,
|
showThresholdLabels: false,
|
||||||
@ -141,11 +160,6 @@ describe('PanelModel', () => {
|
|||||||
model.changePlugin(getPanelPlugin({ id: 'table' }));
|
model.changePlugin(getPanelPlugin({ id: 'table' }));
|
||||||
expect(model.alert).toBe(undefined);
|
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', () => {
|
describe('when changing to react panel from angular panel', () => {
|
||||||
@ -171,5 +185,29 @@ describe('PanelModel', () => {
|
|||||||
expect(panelQueryRunner).toBe(sameQueryRunner);
|
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
|
// Utils
|
||||||
import { Emitter } from 'app/core/utils/emitter';
|
import { Emitter } from 'app/core/utils/emitter';
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
|
DataConfigSource,
|
||||||
DataLink,
|
DataLink,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataQueryResponseData,
|
DataQueryResponseData,
|
||||||
@ -41,6 +43,7 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
|||||||
cachedPluginOptions: true,
|
cachedPluginOptions: true,
|
||||||
plugin: true,
|
plugin: true,
|
||||||
queryRunner: true,
|
queryRunner: true,
|
||||||
|
replaceVariables: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// For angular panels we need to clean up properties when changing type
|
// For angular panels we need to clean up properties when changing type
|
||||||
@ -88,7 +91,7 @@ const defaults: any = {
|
|||||||
options: {},
|
options: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PanelModel {
|
export class PanelModel implements DataConfigSource {
|
||||||
/* persisted id, used in URL to identify a panel */
|
/* persisted id, used in URL to identify a panel */
|
||||||
id: number;
|
id: number;
|
||||||
gridPos: GridPos;
|
gridPos: GridPos;
|
||||||
@ -144,6 +147,7 @@ export class PanelModel {
|
|||||||
// this should not be removed in save model as exporter needs to templatize it
|
// this should not be removed in save model as exporter needs to templatize it
|
||||||
this.datasource = null;
|
this.datasource = null;
|
||||||
this.restoreModel(model);
|
this.restoreModel(model);
|
||||||
|
this.replaceVariables = this.replaceVariables.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Given a persistened PanelModel restores property values */
|
/** Given a persistened PanelModel restores property values */
|
||||||
@ -176,6 +180,7 @@ export class PanelModel {
|
|||||||
|
|
||||||
updateOptions(options: object) {
|
updateOptions(options: object) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.resendLastResult();
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +288,7 @@ export class PanelModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.applyPluginOptionDefaults(plugin);
|
this.applyPluginOptionDefaults(plugin);
|
||||||
|
this.resendLastResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
changePlugin(newPlugin: PanelPlugin) {
|
changePlugin(newPlugin: PanelPlugin) {
|
||||||
@ -319,6 +325,9 @@ export class PanelModel {
|
|||||||
// switch
|
// switch
|
||||||
this.type = pluginId;
|
this.type = pluginId;
|
||||||
this.plugin = newPlugin;
|
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);
|
this.applyPluginOptionDefaults(newPlugin);
|
||||||
|
|
||||||
if (newPlugin.onPanelMigration) {
|
if (newPlugin.onPanelMigration) {
|
||||||
@ -363,10 +372,26 @@ export class PanelModel {
|
|||||||
return clone;
|
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 {
|
getQueryRunner(): PanelQueryRunner {
|
||||||
if (!this.queryRunner) {
|
if (!this.queryRunner) {
|
||||||
this.queryRunner = new PanelQueryRunner();
|
this.queryRunner = new PanelQueryRunner(this);
|
||||||
this.setTransformations(this.transformations);
|
|
||||||
}
|
}
|
||||||
return this.queryRunner;
|
return this.queryRunner;
|
||||||
}
|
}
|
||||||
@ -390,7 +415,22 @@ export class PanelModel {
|
|||||||
|
|
||||||
setTransformations(transformations: DataTransformerConfig[]) {
|
setTransformations(transformations: DataTransformerConfig[]) {
|
||||||
this.transformations = transformations;
|
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 { 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 { DashboardModel } from './index';
|
||||||
import { setEchoSrv } from '@grafana/runtime';
|
import { setEchoSrv } from '@grafana/runtime';
|
||||||
import { Echo } from '../../../core/services/echo/Echo';
|
import { Echo } from '../../../core/services/echo/Echo';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
|
jest.mock('app/core/config', () => ({
|
||||||
|
config: { featureToggles: { transformations: true } },
|
||||||
|
getConfig: () => ({
|
||||||
|
featureToggles: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const dashboardModel = new DashboardModel({
|
const dashboardModel = new DashboardModel({
|
||||||
panels: [{ id: 1, type: 'graph' }],
|
panels: [{ id: 1, type: 'graph' }],
|
||||||
@ -37,16 +45,19 @@ interface ScenarioContext {
|
|||||||
|
|
||||||
type ScenarioFn = (ctx: ScenarioContext) => void;
|
type ScenarioFn = (ctx: ScenarioContext) => void;
|
||||||
|
|
||||||
function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn) {
|
function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn, panelConfig?: DataConfigSource) {
|
||||||
describe(description, () => {
|
describe(description, () => {
|
||||||
let setupFn = () => {};
|
let setupFn = () => {};
|
||||||
|
const defaultPanelConfig: DataConfigSource = {
|
||||||
|
getFieldOverrideOptions: () => undefined,
|
||||||
|
getTransformations: () => undefined,
|
||||||
|
};
|
||||||
const ctx: ScenarioContext = {
|
const ctx: ScenarioContext = {
|
||||||
widthPixels: 200,
|
widthPixels: 200,
|
||||||
scopedVars: {
|
scopedVars: {
|
||||||
server: { text: 'Server1', value: 'server-1' },
|
server: { text: 'Server1', value: 'server-1' },
|
||||||
},
|
},
|
||||||
runner: new PanelQueryRunner(),
|
runner: new PanelQueryRunner(panelConfig || defaultPanelConfig),
|
||||||
setup: (fn: () => void) => {
|
setup: (fn: () => void) => {
|
||||||
setupFn = fn;
|
setupFn = fn;
|
||||||
},
|
},
|
||||||
@ -85,15 +96,15 @@ function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn
|
|||||||
widthPixels: ctx.widthPixels,
|
widthPixels: ctx.widthPixels,
|
||||||
maxDataPoints: ctx.maxDataPoints,
|
maxDataPoints: ctx.maxDataPoints,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
from: dateTime().subtract(1, 'days'),
|
from: grafanaData.dateTime().subtract(1, 'days'),
|
||||||
to: dateTime(),
|
to: grafanaData.dateTime(),
|
||||||
raw: { from: '1h', to: 'now' },
|
raw: { from: '1h', to: 'now' },
|
||||||
},
|
},
|
||||||
panelId: 1,
|
panelId: 1,
|
||||||
queries: [{ refId: 'A', test: 1 }],
|
queries: [{ refId: 'A', test: 1 }],
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.runner = new PanelQueryRunner();
|
ctx.runner = new PanelQueryRunner(panelConfig || defaultPanelConfig);
|
||||||
ctx.runner.getData().subscribe({
|
ctx.runner.getData().subscribe({
|
||||||
next: (data: PanelData) => {
|
next: (data: PanelData) => {
|
||||||
ctx.res = data;
|
ctx.res = data;
|
||||||
@ -182,4 +193,56 @@ describe('PanelQueryRunner', () => {
|
|||||||
expect(ctx.queryCalledWith?.maxDataPoints).toBe(10);
|
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,
|
DataTransformerConfig,
|
||||||
transformDataFrame,
|
transformDataFrame,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
|
applyFieldOverrides,
|
||||||
|
DataConfigSource,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
export interface QueryRunnerOptions<
|
export interface QueryRunnerOptions<
|
||||||
@ -53,36 +55,51 @@ function getNextRequestId() {
|
|||||||
export class PanelQueryRunner {
|
export class PanelQueryRunner {
|
||||||
private subject?: ReplaySubject<PanelData>;
|
private subject?: ReplaySubject<PanelData>;
|
||||||
private subscription?: Unsubscribable;
|
private subscription?: Unsubscribable;
|
||||||
private transformations?: DataTransformerConfig[];
|
|
||||||
private lastResult?: PanelData;
|
private lastResult?: PanelData;
|
||||||
|
private dataConfigSource: DataConfigSource;
|
||||||
|
|
||||||
constructor() {
|
constructor(dataConfigSource: DataConfigSource) {
|
||||||
this.subject = new ReplaySubject(1);
|
this.subject = new ReplaySubject(1);
|
||||||
|
this.dataConfigSource = dataConfigSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable that subscribes to the shared multi-cast subject (that reply last result).
|
* Returns an observable that subscribes to the shared multi-cast subject (that reply last result).
|
||||||
*/
|
*/
|
||||||
getData(transform = true): Observable<PanelData> {
|
getData(transform = true): Observable<PanelData> {
|
||||||
if (transform) {
|
|
||||||
return this.subject.pipe(
|
return this.subject.pipe(
|
||||||
map((data: PanelData) => {
|
map((data: PanelData) => {
|
||||||
if (this.hasTransformations()) {
|
let processedData = data;
|
||||||
const newSeries = transformDataFrame(this.transformations, data.series);
|
// apply transformations
|
||||||
return { ...data, series: newSeries };
|
if (transform && this.hasTransformations()) {
|
||||||
|
processedData = {
|
||||||
|
...processedData,
|
||||||
|
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return data;
|
// apply overrides
|
||||||
|
if (this.hasFieldOverrideOptions()) {
|
||||||
|
processedData = {
|
||||||
|
...processedData,
|
||||||
|
series: applyFieldOverrides({
|
||||||
|
data: processedData.series,
|
||||||
|
...this.dataConfigSource.getFieldOverrideOptions(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return processedData;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just pass it directly
|
hasTransformations = () => {
|
||||||
return this.subject.pipe();
|
const transformations = this.dataConfigSource.getTransformations();
|
||||||
}
|
return config.featureToggles.transformations && transformations && transformations.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
hasTransformations() {
|
hasFieldOverrideOptions = () => {
|
||||||
return config.featureToggles.transformations && this.transformations && this.transformations.length > 0;
|
return this.dataConfigSource.getFieldOverrideOptions();
|
||||||
}
|
};
|
||||||
|
|
||||||
async run(options: QueryRunnerOptions) {
|
async run(options: QueryRunnerOptions) {
|
||||||
const {
|
const {
|
||||||
@ -98,7 +115,6 @@ export class PanelQueryRunner {
|
|||||||
maxDataPoints,
|
maxDataPoints,
|
||||||
scopedVars,
|
scopedVars,
|
||||||
minInterval,
|
minInterval,
|
||||||
// delayStateNotification,
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (isSharedDashboardQuery(datasource)) {
|
if (isSharedDashboardQuery(datasource)) {
|
||||||
@ -164,6 +180,7 @@ export class PanelQueryRunner {
|
|||||||
this.subscription = observable.subscribe({
|
this.subscription = observable.subscribe({
|
||||||
next: (data: PanelData) => {
|
next: (data: PanelData) => {
|
||||||
this.lastResult = preProcessPanelData(data, this.lastResult);
|
this.lastResult = preProcessPanelData(data, this.lastResult);
|
||||||
|
// Store preprocessed query results for applying overrides later on in the pipeline
|
||||||
this.subject.next(this.lastResult);
|
this.subject.next(this.lastResult);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -174,9 +191,11 @@ export class PanelQueryRunner {
|
|||||||
this.lastResult = data;
|
this.lastResult = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
setTransformations(transformations?: DataTransformerConfig[]) {
|
resendLastResult = () => {
|
||||||
this.transformations = transformations;
|
if (this.lastResult) {
|
||||||
|
this.subject.next(this.lastResult);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the panel is closed
|
* Called when the panel is closed
|
||||||
|
@ -3,10 +3,8 @@ import React, { Component } from 'react';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Table } from '@grafana/ui';
|
import { Table } from '@grafana/ui';
|
||||||
import { PanelProps, applyFieldOverrides } from '@grafana/data';
|
import { PanelProps } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { config } from 'app/core/config';
|
|
||||||
import { tableFieldRegistry } from './custom';
|
|
||||||
|
|
||||||
interface Props extends PanelProps<Options> {}
|
interface Props extends PanelProps<Options> {}
|
||||||
|
|
||||||
@ -18,20 +16,12 @@ export class TablePanel extends Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, height, width, replaceVariables, options } = this.props;
|
const { data, height, width } = this.props;
|
||||||
|
|
||||||
if (data.series.length < 1) {
|
if (data.series.length < 1) {
|
||||||
return <div>No Table Data...</div>;
|
return <div>No Table Data...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataProcessed = applyFieldOverrides({
|
return <Table height={height - paddingBottom} width={width} data={data.series[0]} />;
|
||||||
data: data.series,
|
|
||||||
fieldOptions: options.fieldOptions,
|
|
||||||
theme: config.theme,
|
|
||||||
replaceVariables,
|
|
||||||
custom: tableFieldRegistry,
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
return <Table height={height - paddingBottom} width={width} data={dataProcessed} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
echo -e "Collecting code stats (typescript errors & more)"
|
echo -e "Collecting code stats (typescript errors & more)"
|
||||||
|
|
||||||
|
|
||||||
ERROR_COUNT_LIMIT=824
|
ERROR_COUNT_LIMIT=821
|
||||||
DIRECTIVES_LIMIT=172
|
DIRECTIVES_LIMIT=172
|
||||||
CONTROLLERS_LIMIT=139
|
CONTROLLERS_LIMIT=139
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user