import { ApplyFieldOverrideOptions, DataFrame, DataLink, DynamicConfigValue, Field, FieldColorModeId, FieldConfig, FieldConfigPropertyItem, FieldOverrideContext, FieldType, InterpolateFunction, LinkModel, NumericRange, ScopedVars, TimeZone, ValueLinkConfig, } from '../types'; import { fieldMatchers, reduceField, ReducerID } from '../transformations'; import { FieldMatcher } from '../types/transformations'; import { isNumber, set, unset, get } from 'lodash'; import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor'; import { guessFieldTypeForField } from '../dataframe'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { DataLinkBuiltInVars, locationUtil } from '../utils'; import { formattedValueToString } from '../valueFormats'; import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; import { getFieldDisplayName, getFrameDisplayName } from './fieldState'; import { getTimeField } from '../dataframe/processDataFrame'; import { mapInternalLinkToExplore } from '../utils/dataLinks'; import { getTemplateProxyForField } from './templateProxies'; interface OverrideProps { match: FieldMatcher; properties: DynamicConfigValue[]; } export function findNumericFieldMinMax(data: DataFrame[]): NumericRange { let min: number | null = null; let max: number | null = null; const reducers = [ReducerID.min, ReducerID.max]; for (const frame of data) { for (const field of frame.fields) { if (field.type === FieldType.number) { const stats = reduceField({ field, reducers }); const statsMin = stats[ReducerID.min]; const statsMax = stats[ReducerID.max]; if (min === null || statsMin < min) { min = statsMin; } if (max === null || statsMax > max) { max = statsMax; } } } } return { min, max, delta: (max ?? 0) - (min ?? 0) }; } /** * Return a copy of the DataFrame with all rules applied */ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFrame[] { if (!options.data) { return []; } const source = options.fieldConfig; if (!source) { return options.data; } const fieldConfigRegistry = options.fieldConfigRegistry ?? standardFieldConfigEditorRegistry; let seriesIndex = 0; let globalRange: NumericRange | undefined = undefined; // Prepare the Matchers const override: OverrideProps[] = []; if (source.overrides) { for (const rule of source.overrides) { const info = fieldMatchers.get(rule.matcher.id); if (info) { override.push({ match: info.get(rule.matcher.options), properties: rule.properties, }); } } } return options.data.map((frame, index) => { // Need to define this new frame here as it's passed to the getLinkSupplier function inside the fields loop const newFrame: DataFrame = { ...frame }; const scopedVars: ScopedVars = { __series: { text: 'Series', value: { name: getFrameDisplayName(frame, index) } }, // might be missing }; const fields: Field[] = frame.fields.map((field) => { // Config is mutable within this scope const fieldScopedVars = { ...scopedVars }; const displayName = getFieldDisplayName(field, frame, options.data); fieldScopedVars['__field'] = { text: 'Field', value: getTemplateProxyForField(field, frame, options.data), }; field.state = { ...field.state, scopedVars: fieldScopedVars, displayName, }; const config: FieldConfig = { ...field.config }; const context = { field, data: options.data!, dataFrameIndex: index, replaceVariables: options.replaceVariables, fieldConfigRegistry: fieldConfigRegistry, }; // 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, frame, options.data!)) { for (const prop of rule.properties) { // config.scopedVars is set already here setDynamicConfigValue(config, prop, context); } } } // Try harder to set a real value that is not 'other' let type = field.type; if (!type || type === FieldType.other) { const t = guessFieldTypeForField(field); if (t) { type = t; } } // Some units have an implied range if (config.unit === 'percent') { if (!isNumber(config.min)) { config.min = 0; } if (!isNumber(config.max)) { config.max = 100; } } else if (config.unit === 'percentunit') { if (!isNumber(config.min)) { config.min = 0; } if (!isNumber(config.max)) { config.max = 1; } } // Set the Min/Max value automatically let range: NumericRange | undefined = undefined; if (field.type === FieldType.number) { if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) { globalRange = findNumericFieldMinMax(options.data!); } const min = config.min ?? globalRange!.min; const max = config.max ?? globalRange!.max; range = { min, max, delta: max! - min! }; } // Some color modes needs series index to assign field color so we count // up series index here but ignore time fields if (field.type !== FieldType.time) { seriesIndex++; } // Overwrite the configs const newField: Field = { ...field, config, type, state: { ...field.state, displayName: null, seriesIndex, range, }, }; // and set the display processor using it newField.display = getDisplayProcessor({ field: newField, theme: options.theme, timeZone: options.timeZone, }); // Attach data links supplier newField.getLinks = getLinksSupplier( newFrame, newField, fieldScopedVars, context.replaceVariables, options.timeZone ); return newField; }); newFrame.fields = fields; return newFrame; }); } export interface FieldOverrideEnv extends FieldOverrideContext { fieldConfigRegistry: FieldConfigOptionsRegistry; } export function setDynamicConfigValue(config: FieldConfig, value: DynamicConfigValue, context: FieldOverrideEnv) { const reg = context.fieldConfigRegistry; const item = reg.getIfExists(value.id); if (!item) { return; } const val = item.process(value.value, context, item.settings); const remove = val === undefined || val === null; if (remove) { if (item.isCustom && config.custom) { unset(config.custom, item.path); } else { unset(config, item.path); } } else { if (item.isCustom) { if (!config.custom) { config.custom = {}; } set(config.custom, item.path, val); } else { set(config, item.path, val); } } } // config -> from DS // defaults -> from Panel config export function setFieldConfigDefaults(config: FieldConfig, defaults: FieldConfig, context: FieldOverrideEnv) { for (const fieldConfigProperty of context.fieldConfigRegistry.list()) { if (fieldConfigProperty.isCustom && !config.custom) { config.custom = {}; } processFieldConfigValue( fieldConfigProperty.isCustom ? config.custom : config, fieldConfigProperty.isCustom ? defaults.custom : defaults, fieldConfigProperty, context ); } validateFieldConfig(config); } const processFieldConfigValue = ( destination: Record, // it's mutable source: Record, fieldConfigProperty: FieldConfigPropertyItem, context: FieldOverrideEnv ) => { const currentConfig = get(destination, fieldConfigProperty.path); if (currentConfig === null || currentConfig === undefined) { const item = context.fieldConfigRegistry.getIfExists(fieldConfigProperty.id); if (!item) { return; } if (item && item.shouldApply(context.field!)) { const val = item.process(get(source, item.path), context, item.settings); if (val !== undefined && val !== null) { set(destination, item.path, 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) */ export function validateFieldConfig(config: FieldConfig) { const { thresholds } = config; if (!config.color) { if (thresholds) { config.color = { mode: FieldColorModeId.Thresholds, }; } // No Color settings } else if (!config.color.mode) { // Without a mode, skip color altogether delete config.color; } // Verify that max > min (swap if necessary) if (config.hasOwnProperty('min') && config.hasOwnProperty('max') && config.min! > config.max!) { const tmp = config.max; config.max = config.min; config.min = tmp; } } export const getLinksSupplier = ( frame: DataFrame, field: Field, fieldScopedVars: ScopedVars, replaceVariables: InterpolateFunction, timeZone?: TimeZone ) => (config: ValueLinkConfig): Array> => { if (!field.config.links || field.config.links.length === 0) { return []; } const timeRangeUrl = locationUtil.getTimeRangeUrlParams(); const { timeField } = getTimeField(frame); return field.config.links.map((link: DataLink) => { const variablesQuery = locationUtil.getVariablesUrlParams(); let dataFrameVars = {}; let valueVars = {}; // We are not displaying reduction result if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) { const fieldsProxy = getFieldDisplayValuesProxy({ frame, rowIndex: config.valueRowIndex, timeZone: timeZone, }); valueVars = { raw: field.values.get(config.valueRowIndex), numeric: fieldsProxy[field.name].numeric, text: fieldsProxy[field.name].text, time: timeField ? timeField.values.get(config.valueRowIndex) : undefined, }; dataFrameVars = { __data: { value: { name: frame.name, refId: frame.refId, fields: fieldsProxy, }, text: 'Data', }, }; } else { if (config.calculatedValue) { valueVars = { raw: config.calculatedValue.numeric, numeric: config.calculatedValue.numeric, text: formattedValueToString(config.calculatedValue), }; } } const variables = { ...fieldScopedVars, __value: { text: 'Value', value: valueVars, }, ...dataFrameVars, [DataLinkBuiltInVars.keepTime]: { text: timeRangeUrl, value: timeRangeUrl, }, [DataLinkBuiltInVars.includeVars]: { text: variablesQuery, value: variablesQuery, }, }; if (link.internal) { // For internal links at the moment only destination is Explore. return mapInternalLinkToExplore({ link, internalLink: link.internal, scopedVars: variables, field, range: {} as any, replaceVariables, }); } else { let href = locationUtil.assureBaseUrl(link.url.replace(/\n/g, '')); href = replaceVariables(href, variables); href = locationUtil.processUrl(href); const info: LinkModel = { href, title: replaceVariables(link.title || '', variables), target: link.targetBlank ? '_blank' : undefined, origin: field, }; return info; } }); }; /** * Return a copy of the DataFrame with raw data */ export function applyRawFieldOverrides(data: DataFrame[]): DataFrame[] { if (!data || data.length === 0) { return []; } const newData = [...data]; const processor = getRawDisplayProcessor(); for (let frameIndex = 0; frameIndex < newData.length; frameIndex++) { const newFrame = { ...newData[frameIndex] }; const newFields = [...newFrame.fields]; for (let fieldIndex = 0; fieldIndex < newFields.length; fieldIndex++) { newFields[fieldIndex] = { ...newFields[fieldIndex], display: processor, }; } newData[frameIndex] = { ...newFrame, fields: newFields, }; } return newData; }