mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FieldOverides: apply field overrides based on configuration (#22047)
* test apply * test apply * Move standard field config editor registry to grafana-data * merge master * Apply field config defaults * Make field and dataFrameIndex optional on on FieldOverrideContext * Apply custom field config overrides * Gauge - make sure thresholds are set * Move series and field scoped vars calculation * Enable template variables interpolation in title fields * Expose standars field configs from grafana ui via function * Add missing option to the config for the min value to be derived from field values * Fix ts issue Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
824f7362d4
commit
2c9b321c48
jest.config.js
packages
grafana-data/src
field
fieldDisplay.test.tsfieldDisplay.tsfieldOverrides.test.tsfieldOverrides.tsindex.tsstandardFieldConfigEditorRegistry.ts
types
utils
grafana-ui/src/components
public
scripts/webpack
@ -1,29 +1,13 @@
|
||||
|
||||
module.exports = {
|
||||
verbose: false,
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
"moduleDirectories": ["node_modules", "public"],
|
||||
"roots": [
|
||||
"<rootDir>/public/app",
|
||||
"<rootDir>/public/test",
|
||||
"<rootDir>/packages",
|
||||
"<rootDir>/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: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/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 } },
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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<FieldPropertyEditorItem>(() => {
|
||||
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,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
@ -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<string, any>, // it's mutable
|
||||
source: Record<string, any>,
|
||||
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)
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './fieldDisplay';
|
||||
export * from './displayProcessor';
|
||||
export * from './scale';
|
||||
export * from './standardFieldConfigEditorRegistry';
|
||||
|
||||
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
|
||||
|
@ -0,0 +1,4 @@
|
||||
import { FieldConfigEditorRegistry, FieldPropertyEditorItem } from '../types/fieldOverrides';
|
||||
import { Registry } from '../utils/Registry';
|
||||
|
||||
export const standardFieldConfigEditorRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>();
|
@ -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<string, any>;
|
||||
|
||||
scopedVars?: ScopedVars;
|
||||
}
|
||||
|
||||
export interface Field<T = any, V = Vector<T>> {
|
||||
|
@ -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<TValue, TSettings> {
|
||||
}
|
||||
|
||||
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<TValue = any, TSettings = any> extends
|
||||
|
||||
// Configuration options for the particular property
|
||||
settings: TSettings;
|
||||
|
||||
// Checks if field should be processed
|
||||
shouldApply: (field: Field) => boolean;
|
||||
}
|
||||
|
||||
export type FieldConfigEditorRegistry = Registry<FieldPropertyEditorItem>;
|
||||
|
@ -33,19 +33,26 @@ interface RegistrySelectInfo {
|
||||
export class Registry<T extends RegistryItem> {
|
||||
private ordered: T[] = [];
|
||||
private byId = new Map<string, T>();
|
||||
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<T extends RegistryItem> {
|
||||
}
|
||||
|
||||
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<T extends RegistryItem> {
|
||||
}
|
||||
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<T extends RegistryItem> {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.initalized) {
|
||||
if (this.initialized) {
|
||||
this.sort();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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<string, StringFieldConfigSettings> = {
|
||||
id: 'title', // Match field properties
|
||||
name: 'Title',
|
||||
description: 'The field title',
|
||||
|
||||
editor: StringValueEditor,
|
||||
override: StringOverrideEditor,
|
||||
process: stringOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
const unit: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
|
||||
id: 'unit', // Match field properties
|
||||
name: 'Unit',
|
||||
description: 'value units',
|
||||
|
||||
editor: UnitValueEditor,
|
||||
override: UnitOverrideEditor,
|
||||
process: stringOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
const min: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
|
||||
id: 'min', // Match field properties
|
||||
name: 'Min',
|
||||
description: 'Minimum expected value',
|
||||
|
||||
editor: NumberValueEditor,
|
||||
override: NumberOverrideEditor,
|
||||
process: numberOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
const max: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
|
||||
id: 'max', // Match field properties
|
||||
name: 'Max',
|
||||
description: 'Maximum expected value',
|
||||
|
||||
editor: NumberValueEditor,
|
||||
override: NumberOverrideEditor,
|
||||
process: numberOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
const decimals: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
|
||||
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<ThresholdsConfig, ThresholdsFieldConfigSettings> = {
|
||||
id: 'thresholds', // Match field properties
|
||||
name: 'Thresholds',
|
||||
description: 'Manage Thresholds',
|
||||
|
||||
editor: ThresholdsValueEditor,
|
||||
override: ThresholdsOverrideEditor,
|
||||
process: thresholdsOverrideProcessor,
|
||||
|
||||
settings: {
|
||||
// ??
|
||||
},
|
||||
};
|
||||
|
||||
const noValue: FieldPropertyEditorItem<string, StringFieldConfigSettings> = {
|
||||
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<DataLink[], StringFieldConfigSettings> = {
|
||||
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<FieldPropertyEditorItem>(
|
||||
() => {
|
||||
return [title, unit, min, max, decimals, thresholds, noValue, links];
|
||||
}
|
||||
);
|
@ -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<string, StringFieldConfigSettings> = {
|
||||
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<string, StringFieldConfigSettings> = {
|
||||
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<number, NumberFieldConfigSettings> = {
|
||||
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<number, NumberFieldConfigSettings> = {
|
||||
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<number, NumberFieldConfigSettings> = {
|
||||
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<ThresholdsConfig, ThresholdsFieldConfigSettings> = {
|
||||
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<string, StringFieldConfigSettings> = {
|
||||
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<DataLink[], StringFieldConfigSettings> = {
|
||||
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,
|
||||
];
|
||||
};
|
@ -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}`;
|
||||
};
|
||||
|
||||
|
@ -54,8 +54,9 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
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) {
|
||||
|
@ -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';
|
||||
|
@ -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(
|
||||
(
|
||||
|
@ -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<Props> {
|
||||
<div>
|
||||
{config.overrides.map((o, i) => {
|
||||
const matcherUi = fieldMatchersUI.get(o.matcher.id);
|
||||
// TODO: apply matcher to retrieve fields
|
||||
return (
|
||||
<div key={`${o.matcher.id}/${i}`} style={{ border: `2px solid red`, marginBottom: '10px' }}>
|
||||
<Forms.Field label={matcherUi.name} description={matcherUi.description}>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { FieldConfig } from '@grafana/data';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
import { FieldConfig, standardFieldConfigEditorRegistry } from '@grafana/data';
|
||||
|
||||
describe('standardFieldConfigEditorRegistry', () => {
|
||||
const dummyConfig: FieldConfig = {
|
@ -20,6 +20,7 @@ const columWidth: FieldPropertyEditorItem<number, NumberFieldConfigSettings> = {
|
||||
min: 20,
|
||||
max: 300,
|
||||
},
|
||||
shouldApply: () => true,
|
||||
};
|
||||
|
||||
export const tableFieldRegistry: FieldConfigEditorRegistry = new Registry<FieldPropertyEditorItem>(() => {
|
||||
|
@ -44,8 +44,6 @@ const localStorageMock = (() => {
|
||||
|
||||
global.localStorage = localStorageMock;
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = jest.fn() as any;
|
||||
|
||||
const throwUnhandledRejections = () => {
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
|
@ -63,7 +63,12 @@ module.exports = (env = {}) =>
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
allowNamespaces: true,
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
},
|
||||
|
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user