import { isArray } from 'lodash'; import { anyToNumber, DataFrame, FieldColorModeId, FieldConfig, getFieldDisplayName, MappingType, ReducerID, ThresholdsMode, ValueMapping, ValueMap, Field, FieldType, } from '@grafana/data'; export interface FieldToConfigMapping { fieldName: string; reducerId?: ReducerID; handlerKey: string | null; } /** * Transforms a frame with fields to a map of field configs * * Input * | Unit | Min | Max | * -------------------------------- * | Temperature | 0 | 30 | * | Pressure | 0 | 100 | * * Outputs * { { min: 0, max: 100 }, * } */ export function getFieldConfigFromFrame( frame: DataFrame, rowIndex: number, evaluatedMappings: EvaluatedMappingResult ): FieldConfig { const config: FieldConfig = {}; const context: FieldToConfigContext = {}; for (const field of frame.fields) { const fieldName = getFieldDisplayName(field, frame); const mapping = evaluatedMappings.index[fieldName]; const handler = mapping.handler; if (!handler) { continue; } const configValue = field.values.get(rowIndex); if (configValue === null || configValue === undefined) { continue; } const newValue = handler.processor(configValue, config, context); if (newValue != null) { (config as any)[handler.targetProperty ?? handler.key] = newValue; } } if (context.mappingValues) { config.mappings = combineValueMappings(context); } return config; } interface FieldToConfigContext { mappingValues?: any[]; mappingColors?: string[]; mappingTexts?: string[]; } type FieldToConfigMapHandlerProcessor = (value: any, config: FieldConfig, context: FieldToConfigContext) => any; export interface FieldToConfigMapHandler { key: string; targetProperty?: string; name?: string; processor: FieldToConfigMapHandlerProcessor; defaultReducer?: ReducerID; } export enum FieldConfigHandlerKey { Name = 'field.name', Value = 'field.value', Label = 'field.label', Ignore = '__ignore', } export const configMapHandlers: FieldToConfigMapHandler[] = [ { key: FieldConfigHandlerKey.Name, name: 'Field name', processor: () => {}, }, { key: FieldConfigHandlerKey.Value, name: 'Field value', processor: () => {}, }, { key: FieldConfigHandlerKey.Label, name: 'Field label', processor: () => {}, }, { key: FieldConfigHandlerKey.Ignore, name: 'Ignore', processor: () => {}, }, { key: 'max', processor: toNumericOrUndefined, }, { key: 'min', processor: toNumericOrUndefined, }, { key: 'unit', processor: (value) => value.toString(), }, { key: 'decimals', processor: toNumericOrUndefined, }, { key: 'displayName', name: 'Display name', processor: (value: any) => value.toString(), }, { key: 'color', processor: (value) => ({ fixedColor: value, mode: FieldColorModeId.Fixed }), }, { key: 'threshold1', targetProperty: 'thresholds', processor: (value, config) => { const numeric = anyToNumber(value); if (isNaN(numeric)) { return; } if (!config.thresholds) { config.thresholds = { mode: ThresholdsMode.Absolute, steps: [{ value: -Infinity, color: 'green' }], }; } config.thresholds.steps.push({ value: numeric, color: 'red', }); return config.thresholds; }, }, { key: 'mappings.value', name: 'Value mappings / Value', targetProperty: 'mappings', defaultReducer: ReducerID.allValues, processor: (value, config, context) => { if (!isArray(value)) { return; } context.mappingValues = value; return config.mappings; }, }, { key: 'mappings.color', name: 'Value mappings / Color', targetProperty: 'mappings', defaultReducer: ReducerID.allValues, processor: (value, config, context) => { if (!isArray(value)) { return; } context.mappingColors = value; return config.mappings; }, }, { key: 'mappings.text', name: 'Value mappings / Display text', targetProperty: 'mappings', defaultReducer: ReducerID.allValues, processor: (value, config, context) => { if (!isArray(value)) { return; } context.mappingTexts = value; return config.mappings; }, }, ]; function combineValueMappings(context: FieldToConfigContext): ValueMapping[] { const valueMap: ValueMap = { type: MappingType.ValueToText, options: {}, }; if (!context.mappingValues) { return []; } for (let i = 0; i < context.mappingValues.length; i++) { const value = context.mappingValues[i]; if (value != null) { valueMap.options[value.toString()] = { color: context.mappingColors && context.mappingColors[i], text: context.mappingTexts && context.mappingTexts[i], index: i, }; } } return [valueMap]; } let configMapHandlersIndex: Record | null = null; export function getConfigMapHandlersIndex() { if (configMapHandlersIndex === null) { configMapHandlersIndex = {}; for (const def of configMapHandlers) { configMapHandlersIndex[def.key] = def; } } return configMapHandlersIndex; } function toNumericOrUndefined(value: any) { const numeric = anyToNumber(value); if (isNaN(numeric)) { return; } return numeric; } export function getConfigHandlerKeyForField(fieldName: string, mappings: FieldToConfigMapping[]) { for (const map of mappings) { if (fieldName === map.fieldName) { return map.handlerKey; } } return fieldName.toLowerCase(); } export function lookUpConfigHandler(key: string | null): FieldToConfigMapHandler | null { if (!key) { return null; } return getConfigMapHandlersIndex()[key]; } export interface EvaluatedMapping { automatic: boolean; handler: FieldToConfigMapHandler | null; reducerId: ReducerID; } export interface EvaluatedMappingResult { index: Record; nameField?: Field; valueField?: Field; } export function evaluteFieldMappings( frame: DataFrame, mappings: FieldToConfigMapping[], withNameAndValue?: boolean ): EvaluatedMappingResult { const result: EvaluatedMappingResult = { index: {}, }; // Look up name and value field in mappings let nameFieldMappping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Name); let valueFieldMapping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Value); for (const field of frame.fields) { const fieldName = getFieldDisplayName(field, frame); const mapping = mappings.find((x) => x.fieldName === fieldName); const key = mapping ? mapping.handlerKey : fieldName.toLowerCase(); let handler = lookUpConfigHandler(key); // Name and value handlers are a special as their auto logic is based on first matching criteria if (withNameAndValue) { // If we have a handler it means manually specified field if (handler) { if (handler.key === FieldConfigHandlerKey.Name) { result.nameField = field; } if (handler.key === FieldConfigHandlerKey.Value) { result.valueField = field; } } else if (!mapping) { // We have no name field and no mapping for it, pick first string if (!result.nameField && !nameFieldMappping && field.type === FieldType.string) { result.nameField = field; handler = lookUpConfigHandler(FieldConfigHandlerKey.Name); } if (!result.valueField && !valueFieldMapping && field.type === FieldType.number) { result.valueField = field; handler = lookUpConfigHandler(FieldConfigHandlerKey.Value); } } } // If no handle and when in name and value mode (Rows to fields) default to labels if (!handler && withNameAndValue) { handler = lookUpConfigHandler(FieldConfigHandlerKey.Label); } result.index[fieldName] = { automatic: !mapping, handler: handler, reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull, }; } return result; }