diff --git a/jest.config.js b/jest.config.js index 298146cc519..5b192159435 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,29 +1,13 @@ - module.exports = { verbose: false, - "transform": { - "^.+\\.(ts|tsx)$": "ts-jest" + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', }, - "moduleDirectories": ["node_modules", "public"], - "roots": [ - "/public/app", - "/public/test", - "/packages", - "/scripts", - ], - "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json" - ], - "setupFiles": [ - "./public/test/jest-shim.ts", - "./public/test/jest-setup.ts", - "jest-canvas-mock" - ], - "snapshotSerializers": ["enzyme-to-json/serializer"], - "globals": { "ts-jest": { "isolatedModules": true } }, + moduleDirectories: ['node_modules', 'public'], + roots: ['/public/app', '/public/test', '/packages', '/scripts'], + testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'], + snapshotSerializers: ['enzyme-to-json/serializer'], + globals: { 'ts-jest': { isolatedModules: true } }, }; diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index ea73280a296..2694ab24d1e 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -5,7 +5,7 @@ import { ReducerID } from '../transformations/fieldReducer'; import { ThresholdsMode } from '../types/thresholds'; import { GrafanaTheme } from '../types/theme'; import { MappingType, FieldConfig } from '../types'; -import { setFieldConfigDefaults } from './fieldOverrides'; +import { validateFieldConfig } from './fieldOverrides'; describe('FieldDisplay', () => { it('show first numeric values', () => { @@ -78,7 +78,7 @@ describe('FieldDisplay', () => { ], }, }; - setFieldConfigDefaults(config); + validateFieldConfig(config); expect(config.thresholds!.steps.length).toEqual(2); expect(config.thresholds!.steps[0].value).toBe(-Infinity); }); diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 64338fc9146..559ff8a3076 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -100,7 +100,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi for (let s = 0; s < data.length && !hitLimit; s++) { const series = data[s]; // Name is already set - scopedVars['__series'] = { text: 'Series', value: { name: series.name } }; const { timeField } = getTimeField(series); const view = new DataFrameView(series); @@ -114,13 +113,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi } const config = field.config; // already set by the prepare task - let name = field.name; - if (!name) { - name = `Field[${s}]`; - } - - scopedVars['__field'] = { text: 'Field', value: { name } }; - const display = field.display ?? getDisplayProcessor({ @@ -145,9 +137,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi }; } } - const displayValue = display(field.values.get(j)); - displayValue.title = replaceVariables(title, scopedVars); + displayValue.title = replaceVariables(title, { + ...field.config.scopedVars, // series and field scoped vars + ...scopedVars, + }); + values.push({ name, field: config, @@ -180,7 +175,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi for (const calc of calcs) { scopedVars[VAR_CALC] = { value: calc, text: calc }; const displayValue = display(results[calc]); - displayValue.title = replaceVariables(title, scopedVars); + displayValue.title = replaceVariables(title, { + ...field.config.scopedVars, // series and field scoped vars + ...scopedVars, + }); values.push({ name: calc, field: config, diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 3f8d32e9059..bcf3791982e 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -1,96 +1,41 @@ -import { setFieldConfigDefaults, findNumericFieldMinMax, applyFieldOverrides } from './fieldOverrides'; +import { FieldOverrideEnv, findNumericFieldMinMax, setFieldConfigDefaults } from './fieldOverrides'; import { MutableDataFrame } from '../dataframe'; -import { FieldConfig, FieldConfigSource, InterpolateFunction, GrafanaTheme } from '../types'; -import { FieldMatcherID } from '../transformations'; -import { FieldDisplayOptions } from './fieldDisplay'; +import { + FieldConfig, + FieldConfigEditorRegistry, + FieldOverrideContext, + FieldPropertyEditorItem, + FieldType, +} from '../types'; +import { Registry } from '../utils'; +import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; -describe('FieldOverrides', () => { - const f0 = new MutableDataFrame(); - f0.add({ title: 'AAA', value: 100, value2: 1234 }, true); - f0.add({ title: 'BBB', value: -20 }, true); - f0.add({ title: 'CCC', value: 200, value2: 1000 }, true); - expect(f0.length).toEqual(3); +const property1 = { + id: 'property1', // Match field properties + process: (value: any) => value, + shouldApply: () => true, +} as any; - // Hardcode the max value - f0.fields[1].config.max = 0; - f0.fields[1].config.decimals = 6; +const property2 = { + id: 'property2', // Match field properties + process: (value: any) => value, + shouldApply: () => true, +} as any; - const src: FieldConfigSource = { - defaults: { - unit: 'xyz', - decimals: 2, - }, - overrides: [ - { - matcher: { id: FieldMatcherID.numeric }, - properties: [ - { prop: 'decimals', value: 1 }, // Numeric - { prop: 'title', value: 'Kittens' }, // Text - ], - }, - ], - }; +const unit = { + id: 'unit', // Match field properties + process: (value: any) => value, + shouldApply: () => true, +} as any; - it('will merge FieldConfig with default values', () => { - const field: FieldConfig = { - min: 0, - max: 100, - }; - const f1 = { - unit: 'ms', - dateFormat: '', // should be ignored - max: parseFloat('NOPE'), // should be ignored - min: null, // should alo be ignored! - }; - setFieldConfigDefaults(field, f1 as FieldConfig); - expect(field.min).toEqual(0); - expect(field.max).toEqual(100); - expect(field.unit).toEqual('ms'); - }); +export const customFieldRegistry: FieldConfigEditorRegistry = new Registry(() => { + return [property1, property2]; +}); - it('will apply field overrides', () => { - const data = applyFieldOverrides({ - data: [f0], // the frame - fieldOptions: src as FieldDisplayOptions, // defaults + overrides - replaceVariables: (undefined as any) as InterpolateFunction, - theme: (undefined as any) as GrafanaTheme, - })[0]; - const valueColumn = data.fields[1]; - const config = valueColumn.config; - - // Keep max from the original setting - expect(config.max).toEqual(0); - - // Don't Automatically pick the min value - expect(config.min).toEqual(undefined); - - // The default value applied - expect(config.unit).toEqual('xyz'); - - // The default value applied - expect(config.title).toEqual('Kittens'); - - // The override applied - expect(config.decimals).toEqual(1); - }); - - it('will apply set min/max when asked', () => { - const data = applyFieldOverrides({ - data: [f0], // the frame - fieldOptions: src as FieldDisplayOptions, // defaults + overrides - replaceVariables: (undefined as any) as InterpolateFunction, - theme: (undefined as any) as GrafanaTheme, - autoMinMax: true, - })[0]; - const valueColumn = data.fields[1]; - const config = valueColumn.config; - - // Keep max from the original setting - expect(config.max).toEqual(0); - - // Don't Automatically pick the min value - expect(config.min).toEqual(-20); - }); +// For the need of this test we need to mock the standard registry +// as we cannot imporrt from grafana/ui +standardFieldConfigEditorRegistry.setInit(() => { + return [unit]; }); describe('Global MinMax', () => { @@ -106,3 +51,73 @@ describe('Global MinMax', () => { expect(minmax.max).toEqual(1234); }); }); + +describe('setFieldConfigDefaults', () => { + it('applies field config defaults', () => { + const dsFieldConfig: FieldConfig = { + decimals: 2, + min: 0, + max: 100, + }; + + const panelFieldConfig: FieldConfig = { + decimals: 1, + min: 10, + max: 50, + unit: 'km', + }; + + const context: FieldOverrideContext = { + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + }; + + console.log(standardFieldConfigEditorRegistry); + // we mutate dsFieldConfig + setFieldConfigDefaults(dsFieldConfig, panelFieldConfig, context); + + expect(dsFieldConfig).toMatchInlineSnapshot(` + Object { + "decimals": 2, + "max": 100, + "min": 0, + "unit": "km", + } + `); + }); + + it('applies field config defaults for custom properties', () => { + const dsFieldConfig: FieldConfig = { + custom: { + property1: 10, + }, + }; + + const panelFieldConfig: FieldConfig = { + custom: { + property1: 20, + property2: 10, + }, + }; + + const context: FieldOverrideEnv = { + data: [] as any, + field: { type: FieldType.number } as any, + dataFrameIndex: 0, + custom: customFieldRegistry, + }; + + // we mutate dsFieldConfig + setFieldConfigDefaults(dsFieldConfig, panelFieldConfig, context); + + expect(dsFieldConfig).toMatchInlineSnapshot(` + Object { + "custom": Object { + "property1": 10, + "property2": 10, + }, + } + `); + }); +}); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 3a6b1acdd3c..da66d3bfa68 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -1,4 +1,3 @@ -import set from 'lodash/set'; import { GrafanaTheme, DynamicConfigValue, @@ -12,13 +11,16 @@ import { FieldColorMode, ColorScheme, TimeZone, + FieldConfigEditorRegistry, + FieldOverrideContext, + ScopedVars, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; import isNumber from 'lodash/isNumber'; -import toNumber from 'lodash/toNumber'; import { getDisplayProcessor } from './displayProcessor'; import { guessFieldTypeForField } from '../dataframe'; +import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; interface OverrideProps { match: FieldMatcher; @@ -37,6 +39,8 @@ export interface ApplyFieldOverrideOptions { theme: GrafanaTheme; timeZone?: TimeZone; autoMinMax?: boolean; + standard?: FieldConfigEditorRegistry; + custom?: FieldConfigEditorRegistry; } export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax { @@ -65,6 +69,7 @@ export function findNumericFieldMinMax(data: DataFrame[]): GlobalMinMax { * Return a copy of the DataFrame with all rules applied */ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFrame[] { + const scopedVars: ScopedVars = {}; if (!options.data) { return []; } @@ -95,29 +100,42 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra if (!name) { name = `Series[${index}]`; } + scopedVars['__series'] = { text: 'Series', value: { name } }; - const fields: Field[] = frame.fields.map(field => { + const fields: Field[] = frame.fields.map((field, fieldIndex) => { // Config is mutable within this scope - const config: FieldConfig = { ...field.config } || {}; - if (field.type === FieldType.number) { - setFieldConfigDefaults(config, source.defaults); + let fieldName = field.name; + if (!fieldName) { + fieldName = `Field[${fieldIndex}]`; } + scopedVars['__field'] = { text: 'Field', value: { name: fieldName } }; + + const config: FieldConfig = { ...field.config, scopedVars } || {}; + const context = { + field, + data: options.data!, + dataFrameIndex: index, + replaceVariables: options.replaceVariables, + custom: options.custom, + }; + + // Anything in the field config that's not set by the datasource + // will be filled in by panel's field configuration + setFieldConfigDefaults(config, source.defaults, context); + // Find any matching rules and then override for (const rule of override) { if (rule.match(field)) { for (const prop of rule.properties) { - setDynamicConfigValue(config, { - value: prop, - config, - field, - data: frame, - replaceVariables: options.replaceVariables, - }); + // config.scopedVars is set already here + setDynamicConfigValue(config, prop, context); } } } + // console.log(config) + // Try harder to set a real value that is not 'other' let type = field.type; if (!type || type === FieldType.other) { @@ -182,65 +200,87 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra }); } -interface DynamicConfigValueOptions { - value: DynamicConfigValue; - config: FieldConfig; - field: Field; - data: DataFrame; - replaceVariables: InterpolateFunction; +export interface FieldOverrideEnv extends FieldOverrideContext { + custom?: FieldConfigEditorRegistry; } -const numericFieldProps: any = { - decimals: true, - min: true, - max: true, -}; +function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) { + const reg = value.custom ? context.custom : standardFieldConfigEditorRegistry; -function prepareConfigValue(key: string, input: any, options?: DynamicConfigValueOptions): any { - if (options) { - // TODO template variables etc + const item = reg?.getIfExists(value.prop); + if (!item || !item.shouldApply(context.field!)) { + return; } - if (numericFieldProps[key]) { - const num = toNumber(input); - if (isNaN(num)) { - return null; - } - return num; - } else if (input) { - // skips empty string - if (key === 'unit' && input === 'none') { - return null; - } - } - return input; -} + const val = item.process(value.value, context, item.settings); -export function setDynamicConfigValue(config: FieldConfig, options: DynamicConfigValueOptions) { - const { value } = options; - const v = prepareConfigValue(value.prop, value.value, options); - set(config, value.prop, v); -} + const remove = val === undefined || val === null; -/** - * For numeric values, only valid numbers will be applied - * for units, 'none' will be skipped - */ -export function setFieldConfigDefaults(config: FieldConfig, props?: FieldConfig) { - if (props) { - const keys = Object.keys(props); - for (const key of keys) { - const val = prepareConfigValue(key, (props as any)[key]); - if (val === null || val === undefined) { - continue; + if (remove) { + if (value.custom) { + delete (config?.custom as any)[value.prop]; + } else { + delete (config as any)[value.prop]; + } + } else { + if (value.custom) { + if (!config.custom) { + config.custom = {}; } - set(config, key, val); + config.custom[value.prop] = val; + } else { + (config as any)[value.prop] = val; } } +} +// config -> from DS +// defaults -> from Panel config +export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfig, context: FieldOverrideEnv) { + if (defaults) { + const keys = Object.keys(defaults); + + for (const key of keys) { + if (key === 'custom') { + if (!context.custom) { + continue; + } + if (!config.custom) { + config.custom = {}; + } + const customKeys = Object.keys(defaults.custom!); + + for (const customKey of customKeys) { + processFieldConfigValue(config.custom!, defaults.custom!, customKey, context.custom, context); + } + } else { + // when config from ds exists for a given field -> use it + processFieldConfigValue(config, defaults, key, standardFieldConfigEditorRegistry, context); + } + } + } validateFieldConfig(config); } +const processFieldConfigValue = ( + destination: Record, // it's mutable + source: Record, + key: string, + registry: FieldConfigEditorRegistry, + context: FieldOverrideContext +) => { + const currentConfig = destination[key]; + if (currentConfig === null || currentConfig === undefined) { + const item = registry.getIfExists(key); + if (item && item.shouldApply(context.field!)) { + const val = item.process(source[key], context, item.settings); + if (val !== undefined && val !== null) { + destination[key] = val; + } + } + } +}; + /** * This checks that all options on FieldConfig make sense. It mutates any value that needs * fixed. In particular this makes sure that the first threshold value is -Infinity (not valid in JSON) diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 94530b36c5c..87dfa77c138 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -1,5 +1,6 @@ export * from './fieldDisplay'; export * from './displayProcessor'; export * from './scale'; +export * from './standardFieldConfigEditorRegistry'; export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; diff --git a/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts new file mode 100644 index 00000000000..d1830320055 --- /dev/null +++ b/packages/grafana-data/src/field/standardFieldConfigEditorRegistry.ts @@ -0,0 +1,4 @@ +import { FieldConfigEditorRegistry, FieldPropertyEditorItem } from '../types/fieldOverrides'; +import { Registry } from '../utils/Registry'; + +export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry(); diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 1f628491361..c01543fa1cf 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -6,6 +6,7 @@ import { DataLink } from './dataLink'; import { Vector } from './vector'; import { FieldCalcs } from '../transformations/fieldReducer'; import { FieldColor } from './fieldColor'; +import { ScopedVars } from './ScopedVars'; export enum FieldType { time = 'time', // or date @@ -50,6 +51,8 @@ export interface FieldConfig { // Panel Specific Values custom?: Record; + + scopedVars?: ScopedVars; } export interface Field> { diff --git a/packages/grafana-data/src/types/fieldOverrides.ts b/packages/grafana-data/src/types/fieldOverrides.ts index b02fe3b57ad..eb852a54e6f 100644 --- a/packages/grafana-data/src/types/fieldOverrides.ts +++ b/packages/grafana-data/src/types/fieldOverrides.ts @@ -1,5 +1,5 @@ import { ComponentType } from 'react'; -import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestion, VariableSuggestionsScope } from '../types'; +import { MatcherConfig, FieldConfig, Field, DataFrame, VariableSuggestionsScope, VariableSuggestion } from '../types'; import { Registry, RegistryItem } from '../utils'; import { InterpolateFunction } from './panel'; @@ -30,8 +30,9 @@ export interface FieldConfigEditorProps { } export interface FieldOverrideContext { - data: DataFrame[]; field?: Field; + dataFrameIndex?: number; // The index for the selected field frame + data: DataFrame[]; // All results replaceVariables?: InterpolateFunction; getSuggestions?: (scope?: VariableSuggestionsScope) => VariableSuggestion[]; } @@ -55,6 +56,9 @@ export interface FieldPropertyEditorItem extends // Configuration options for the particular property settings: TSettings; + + // Checks if field should be processed + shouldApply: (field: Field) => boolean; } export type FieldConfigEditorRegistry = Registry; diff --git a/packages/grafana-data/src/utils/Registry.ts b/packages/grafana-data/src/utils/Registry.ts index ffe8f498500..3152de6991f 100644 --- a/packages/grafana-data/src/utils/Registry.ts +++ b/packages/grafana-data/src/utils/Registry.ts @@ -33,19 +33,26 @@ interface RegistrySelectInfo { export class Registry { private ordered: T[] = []; private byId = new Map(); - private initalized = false; + private initialized = false; constructor(private init?: () => T[]) {} + setInit = (init: () => T[]) => { + if (this.initialized) { + throw new Error('Registry already initialized'); + } + this.init = init; + }; + getIfExists(id: string | undefined): T | undefined { - if (!this.initalized) { + if (!this.initialized) { if (this.init) { for (const ext of this.init()) { this.register(ext); } } this.sort(); - this.initalized = true; + this.initialized = true; } if (id) { return this.byId.get(id); @@ -62,7 +69,7 @@ export class Registry { } selectOptions(current?: string[], filter?: (ext: T) => boolean): RegistrySelectInfo { - if (!this.initalized) { + if (!this.initialized) { this.getIfExists('xxx'); // will trigger init } @@ -114,7 +121,7 @@ export class Registry { } return found; } - if (!this.initalized) { + if (!this.initialized) { this.getIfExists('xxx'); // will trigger init } return [...this.ordered]; // copy of everythign just in case @@ -135,7 +142,7 @@ export class Registry { } } - if (this.initalized) { + if (this.initialized) { this.sort(); } } diff --git a/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts b/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts new file mode 100644 index 00000000000..4509905e29f --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/fieldOverrides.test.ts @@ -0,0 +1,125 @@ +import { + applyFieldOverrides, + FieldConfig, + FieldConfigSource, + InterpolateFunction, + GrafanaTheme, + FieldMatcherID, + FieldDisplayOptions, + MutableDataFrame, + DataFrame, + toDataFrame, + standardFieldConfigEditorRegistry, + FieldType, +} from '@grafana/data'; + +import { getTheme } from '../../themes'; +import { getStandardFieldConfigs } from './standardFieldConfigEditors'; + +describe('FieldOverrides', () => { + beforeAll(() => { + standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); + }); + + const f0 = new MutableDataFrame(); + f0.add({ title: 'AAA', value: 100, value2: 1234 }, true); + f0.add({ title: 'BBB', value: -20 }, true); + f0.add({ title: 'CCC', value: 200, value2: 1000 }, true); + expect(f0.length).toEqual(3); + + // Hardcode the max value + f0.fields[1].config.max = 0; + f0.fields[1].config.decimals = 6; + + const src: FieldConfigSource = { + defaults: { + unit: 'xyz', + decimals: 2, + }, + overrides: [ + { + matcher: { id: FieldMatcherID.numeric }, + properties: [ + { prop: 'decimals', value: 1 }, // Numeric + { prop: 'title', value: 'Kittens' }, // Text + ], + }, + ], + }; + + it('will merge FieldConfig with default values', () => { + const field: FieldConfig = { + min: 0, + max: 100, + }; + const f1 = { + unit: 'ms', + dateFormat: '', // should be ignored + max: parseFloat('NOPE'), // should be ignored + min: null, // should alo be ignored! + }; + + const f: DataFrame = toDataFrame({ + fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }], + }); + const processed = applyFieldOverrides({ + data: [f], + standard: standardFieldConfigEditorRegistry, + fieldOptions: { + defaults: f1 as FieldConfig, + overrides: [], + }, + replaceVariables: v => v, + theme: getTheme(), + })[0]; + const out = processed.fields[0].config; + + expect(out.min).toEqual(0); + expect(out.max).toEqual(100); + expect(out.unit).toEqual('ms'); + }); + + it('will apply field overrides', () => { + const data = applyFieldOverrides({ + data: [f0], // the frame + fieldOptions: src as FieldDisplayOptions, // defaults + overrides + replaceVariables: (undefined as any) as InterpolateFunction, + theme: (undefined as any) as GrafanaTheme, + })[0]; + const valueColumn = data.fields[1]; + const config = valueColumn.config; + + // Keep max from the original setting + expect(config.max).toEqual(0); + + // Don't Automatically pick the min value + expect(config.min).toEqual(undefined); + + // The default value applied + expect(config.unit).toEqual('xyz'); + + // The default value applied + expect(config.title).toEqual('Kittens'); + + // The override applied + expect(config.decimals).toEqual(1); + }); + + it('will apply set min/max when asked', () => { + const data = applyFieldOverrides({ + data: [f0], // the frame + fieldOptions: src as FieldDisplayOptions, // defaults + overrides + replaceVariables: (undefined as any) as InterpolateFunction, + theme: (undefined as any) as GrafanaTheme, + autoMinMax: true, + })[0]; + const valueColumn = data.fields[1]; + const config = valueColumn.config; + + // Keep max from the original setting + expect(config.max).toEqual(0); + + // Don't Automatically pick the min value + expect(config.min).toEqual(-20); + }); +}); diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx deleted file mode 100644 index fcca46d4566..00000000000 --- a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { - FieldConfigEditorRegistry, - Registry, - FieldPropertyEditorItem, - ThresholdsConfig, - DataLink, -} from '@grafana/data'; -import { StringValueEditor, StringOverrideEditor, stringOverrideProcessor, StringFieldConfigSettings } from './string'; -import { NumberValueEditor, NumberOverrideEditor, numberOverrideProcessor, NumberFieldConfigSettings } from './number'; -import { UnitValueEditor, UnitOverrideEditor } from './units'; -import { - ThresholdsValueEditor, - ThresholdsOverrideEditor, - thresholdsOverrideProcessor, - ThresholdsFieldConfigSettings, -} from './thresholds'; -import { DataLinksValueEditor, DataLinksOverrideEditor, dataLinksOverrideProcessor } from './links'; - -const title: FieldPropertyEditorItem = { - id: 'title', // Match field properties - name: 'Title', - description: 'The field title', - - editor: StringValueEditor, - override: StringOverrideEditor, - process: stringOverrideProcessor, - - settings: { - placeholder: 'auto', - }, -}; - -const unit: FieldPropertyEditorItem = { - id: 'unit', // Match field properties - name: 'Unit', - description: 'value units', - - editor: UnitValueEditor, - override: UnitOverrideEditor, - process: stringOverrideProcessor, - - settings: { - placeholder: 'none', - }, -}; - -const min: FieldPropertyEditorItem = { - id: 'min', // Match field properties - name: 'Min', - description: 'Minimum expected value', - - editor: NumberValueEditor, - override: NumberOverrideEditor, - process: numberOverrideProcessor, - - settings: { - placeholder: 'auto', - }, -}; - -const max: FieldPropertyEditorItem = { - id: 'max', // Match field properties - name: 'Max', - description: 'Maximum expected value', - - editor: NumberValueEditor, - override: NumberOverrideEditor, - process: numberOverrideProcessor, - - settings: { - placeholder: 'auto', - }, -}; - -const decimals: FieldPropertyEditorItem = { - id: 'decimals', // Match field properties - name: 'Decimals', - description: 'How many decimal places should be shown on a number', - - editor: NumberValueEditor, - override: NumberOverrideEditor, - process: numberOverrideProcessor, - - settings: { - placeholder: 'auto', - min: 0, - max: 15, - integer: true, - }, -}; - -const thresholds: FieldPropertyEditorItem = { - id: 'thresholds', // Match field properties - name: 'Thresholds', - description: 'Manage Thresholds', - - editor: ThresholdsValueEditor, - override: ThresholdsOverrideEditor, - process: thresholdsOverrideProcessor, - - settings: { - // ?? - }, -}; - -const noValue: FieldPropertyEditorItem = { - id: 'noValue', // Match field properties - name: 'No Value', - description: 'What to show when there is no value', - - editor: StringValueEditor, - override: StringOverrideEditor, - process: stringOverrideProcessor, - - settings: { - placeholder: '-', - }, -}; - -const links: FieldPropertyEditorItem = { - id: 'links', // Match field properties - name: 'DataLinks', - description: 'Manage date links', - editor: DataLinksValueEditor, - override: DataLinksOverrideEditor, - process: dataLinksOverrideProcessor, - settings: { - placeholder: '-', - }, -}; - -export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry( - () => { - return [title, unit, min, max, decimals, thresholds, noValue, links]; - } -); diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx new file mode 100644 index 00000000000..6be072e8348 --- /dev/null +++ b/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditors.tsx @@ -0,0 +1,152 @@ +import { DataLink, FieldPropertyEditorItem, FieldType, ThresholdsConfig } from '@grafana/data'; +import { StringFieldConfigSettings, StringOverrideEditor, stringOverrideProcessor, StringValueEditor } from './string'; +import { NumberFieldConfigSettings, NumberOverrideEditor, numberOverrideProcessor, NumberValueEditor } from './number'; +import { UnitOverrideEditor, UnitValueEditor } from './units'; +import { + ThresholdsFieldConfigSettings, + ThresholdsOverrideEditor, + thresholdsOverrideProcessor, + ThresholdsValueEditor, +} from './thresholds'; +import { DataLinksOverrideEditor, dataLinksOverrideProcessor, DataLinksValueEditor } from './links'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace StandardFieldConfigEditors { + export const title: FieldPropertyEditorItem = { + id: 'title', // Match field properties + name: 'Title', + description: 'The field title', + + editor: StringValueEditor, + override: StringOverrideEditor, + process: stringOverrideProcessor, + settings: { + placeholder: 'auto', + expandTemplateVars: true, + }, + shouldApply: field => field.type !== FieldType.time, + }; + + export const unit: FieldPropertyEditorItem = { + id: 'unit', // Match field properties + name: 'Unit', + description: 'value units', + + editor: UnitValueEditor, + override: UnitOverrideEditor, + process: stringOverrideProcessor, + + settings: { + placeholder: 'none', + }, + + shouldApply: field => field.type === FieldType.number, + }; + + export const min: FieldPropertyEditorItem = { + id: 'min', // Match field properties + name: 'Min', + description: 'Minimum expected value', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + }, + shouldApply: field => field.type === FieldType.number, + }; + + export const max: FieldPropertyEditorItem = { + id: 'max', // Match field properties + name: 'Max', + description: 'Maximum expected value', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + }, + + shouldApply: field => field.type === FieldType.number, + }; + + export const decimals: FieldPropertyEditorItem = { + id: 'decimals', // Match field properties + name: 'Decimals', + description: 'How many decimal places should be shown on a number', + + editor: NumberValueEditor, + override: NumberOverrideEditor, + process: numberOverrideProcessor, + + settings: { + placeholder: 'auto', + min: 0, + max: 15, + integer: true, + }, + + shouldApply: field => field.type === FieldType.number, + }; + + export const thresholds: FieldPropertyEditorItem = { + id: 'thresholds', // Match field properties + name: 'Thresholds', + description: 'Manage Thresholds', + + editor: ThresholdsValueEditor, + override: ThresholdsOverrideEditor, + process: thresholdsOverrideProcessor, + + settings: { + // ?? + }, + + shouldApply: field => field.type === FieldType.number, + }; + + export const noValue: FieldPropertyEditorItem = { + id: 'noValue', // Match field properties + name: 'No Value', + description: 'What to show when there is no value', + + editor: StringValueEditor, + override: StringOverrideEditor, + process: stringOverrideProcessor, + + settings: { + placeholder: '-', + }, + // ??? any field with no value + shouldApply: () => true, + }; + + export const links: FieldPropertyEditorItem = { + id: 'links', // Match field properties + name: 'DataLinks', + description: 'Manage date links', + editor: DataLinksValueEditor, + override: DataLinksOverrideEditor, + process: dataLinksOverrideProcessor, + settings: { + placeholder: '-', + }, + shouldApply: () => true, + }; +} + +export const getStandardFieldConfigs = () => { + return [ + StandardFieldConfigEditors.decimals, + StandardFieldConfigEditors.max, + StandardFieldConfigEditors.min, + StandardFieldConfigEditors.noValue, + StandardFieldConfigEditors.thresholds, + StandardFieldConfigEditors.title, + StandardFieldConfigEditors.unit, + ]; +}; diff --git a/packages/grafana-ui/src/components/FieldConfigs/string.tsx b/packages/grafana-ui/src/components/FieldConfigs/string.tsx index 1f387295e9f..80dfa9eadb6 100644 --- a/packages/grafana-ui/src/components/FieldConfigs/string.tsx +++ b/packages/grafana-ui/src/components/FieldConfigs/string.tsx @@ -6,6 +6,7 @@ import Forms from '../Forms'; export interface StringFieldConfigSettings { placeholder?: string; maxLength?: number; + expandTemplateVars?: boolean; } export const stringOverrideProcessor = ( @@ -13,6 +14,9 @@ export const stringOverrideProcessor = ( context: FieldOverrideContext, settings: StringFieldConfigSettings ) => { + if (settings.expandTemplateVars && context.replaceVariables) { + return context.replaceVariables(value, context.field!.config.scopedVars); + } return `${value}`; }; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 485709389af..b290333869e 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -54,8 +54,9 @@ export class Gauge extends PureComponent { getFormattedThresholds(): Threshold[] { const { field, theme } = this.props; - const isPercent = field.thresholds?.mode === ThresholdsMode.Percentage; - const steps = field.thresholds!.steps; + const thresholds = field.thresholds ?? Gauge.defaultProps.field?.thresholds!; + const isPercent = thresholds.mode === ThresholdsMode.Percentage; + const steps = thresholds.steps; let min = field.min!; let max = field.max!; if (isPercent) { diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 50e0f07a21b..29ac39b4831 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -134,4 +134,4 @@ export { export { default as Forms } from './Forms'; export { ValuePicker } from './ValuePicker/ValuePicker'; export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI'; -export { standardFieldConfigEditorRegistry } from './FieldConfigs/standardFieldConfigEditorRegistry'; +export { getStandardFieldConfigs } from './FieldConfigs/standardFieldConfigEditors'; diff --git a/public/app/app.ts b/public/app/app.ts index 6b350fbdec6..87a7bd0970a 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -25,7 +25,7 @@ import angular from 'angular'; import config from 'app/core/config'; // @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move import _ from 'lodash'; -import { AppEvents, setLocale, setMarkdownOptions } from '@grafana/data'; +import { AppEvents, setLocale, setMarkdownOptions, standardFieldConfigEditorRegistry } from '@grafana/data'; import appEvents from 'app/core/app_events'; import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar'; import { checkBrowserCompatibility } from 'app/core/utils/browser'; @@ -40,6 +40,7 @@ import { PerformanceBackend } from './core/services/echo/backends/PerformanceBac import 'app/routes/GrafanaCtrl'; import 'app/features/all'; +import { getStandardFieldConfigs } from '@grafana/ui'; // add move to lodash for backward compatabiltiy // @ts-ignore @@ -82,6 +83,7 @@ export class GrafanaApp { setLocale(config.bootData.user.locale); setMarkdownOptions({ sanitize: !config.disableSanitizeHtml }); + standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); app.config( ( diff --git a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx index 54d15a84725..3cf54f72d78 100644 --- a/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx @@ -7,8 +7,9 @@ import { FieldPropertyEditorItem, DynamicConfigValue, VariableSuggestionsScope, + standardFieldConfigEditorRegistry, } from '@grafana/data'; -import { standardFieldConfigEditorRegistry, Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui'; +import { Forms, fieldMatchersUI, ValuePicker } from '@grafana/ui'; import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv'; import { OptionsGroup } from './OptionsGroup'; @@ -151,6 +152,7 @@ export class FieldConfigEditor extends React.PureComponent {
{config.overrides.map((o, i) => { const matcherUi = fieldMatchersUI.get(o.matcher.id); + // TODO: apply matcher to retrieve fields return (
diff --git a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts b/public/app/features/dashboard/components/PanelEditor/utils.test.ts similarity index 78% rename from packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts rename to public/app/features/dashboard/components/PanelEditor/utils.test.ts index 89569bc96e7..a76f77ac16b 100644 --- a/packages/grafana-ui/src/components/FieldConfigs/standardFieldConfigEditorRegistry.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/utils.test.ts @@ -1,5 +1,4 @@ -import { FieldConfig } from '@grafana/data'; -import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; +import { FieldConfig, standardFieldConfigEditorRegistry } from '@grafana/data'; describe('standardFieldConfigEditorRegistry', () => { const dummyConfig: FieldConfig = { diff --git a/public/app/plugins/panel/table2/custom.tsx b/public/app/plugins/panel/table2/custom.tsx index e7f4189c661..2436d04b775 100644 --- a/public/app/plugins/panel/table2/custom.tsx +++ b/public/app/plugins/panel/table2/custom.tsx @@ -20,6 +20,7 @@ const columWidth: FieldPropertyEditorItem = { min: 20, max: 300, }, + shouldApply: () => true, }; export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry(() => { diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index 1b99470f6eb..c53f6cacb7d 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -44,8 +44,6 @@ const localStorageMock = (() => { global.localStorage = localStorageMock; -HTMLCanvasElement.prototype.getContext = jest.fn() as any; - const throwUnhandledRejections = () => { process.on('unhandledRejection', err => { throw err; diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 3489a88ba60..571d408f4ca 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -63,7 +63,12 @@ module.exports = (env = {}) => modules: false, }, ], - '@babel/preset-typescript', + [ + '@babel/preset-typescript', + { + allowNamespaces: true, + }, + ], '@babel/preset-react', ], }, diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index c85c69373e0..0ecf767bc4b 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -56,7 +56,12 @@ module.exports = merge(common, { modules: false, }, ], - '@babel/preset-typescript', + [ + '@babel/preset-typescript', + { + allowNamespaces: true, + }, + ], '@babel/preset-react', ], }, @@ -66,7 +71,7 @@ module.exports = merge(common, { options: { emitError: true, emitWarning: true, - } + }, }, ], },