fieldToConfigMapping.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import { isArray } from 'lodash';
  2. import {
  3. anyToNumber,
  4. DataFrame,
  5. FieldColorModeId,
  6. FieldConfig,
  7. getFieldDisplayName,
  8. MappingType,
  9. ReducerID,
  10. ThresholdsMode,
  11. ValueMapping,
  12. ValueMap,
  13. Field,
  14. FieldType,
  15. } from '@grafana/data';
  16. export interface FieldToConfigMapping {
  17. fieldName: string;
  18. reducerId?: ReducerID;
  19. handlerKey: string | null;
  20. }
  21. /**
  22. * Transforms a frame with fields to a map of field configs
  23. *
  24. * Input
  25. * | Unit | Min | Max |
  26. * --------------------------------
  27. * | Temperature | 0 | 30 |
  28. * | Pressure | 0 | 100 |
  29. *
  30. * Outputs
  31. * {
  32. { min: 0, max: 100 },
  33. * }
  34. */
  35. export function getFieldConfigFromFrame(
  36. frame: DataFrame,
  37. rowIndex: number,
  38. evaluatedMappings: EvaluatedMappingResult
  39. ): FieldConfig {
  40. const config: FieldConfig = {};
  41. const context: FieldToConfigContext = {};
  42. for (const field of frame.fields) {
  43. const fieldName = getFieldDisplayName(field, frame);
  44. const mapping = evaluatedMappings.index[fieldName];
  45. const handler = mapping.handler;
  46. if (!handler) {
  47. continue;
  48. }
  49. const configValue = field.values.get(rowIndex);
  50. if (configValue === null || configValue === undefined) {
  51. continue;
  52. }
  53. const newValue = handler.processor(configValue, config, context);
  54. if (newValue != null) {
  55. (config as any)[handler.targetProperty ?? handler.key] = newValue;
  56. }
  57. }
  58. if (context.mappingValues) {
  59. config.mappings = combineValueMappings(context);
  60. }
  61. return config;
  62. }
  63. interface FieldToConfigContext {
  64. mappingValues?: any[];
  65. mappingColors?: string[];
  66. mappingTexts?: string[];
  67. }
  68. type FieldToConfigMapHandlerProcessor = (value: any, config: FieldConfig, context: FieldToConfigContext) => any;
  69. export interface FieldToConfigMapHandler {
  70. key: string;
  71. targetProperty?: string;
  72. name?: string;
  73. processor: FieldToConfigMapHandlerProcessor;
  74. defaultReducer?: ReducerID;
  75. }
  76. export enum FieldConfigHandlerKey {
  77. Name = 'field.name',
  78. Value = 'field.value',
  79. Label = 'field.label',
  80. Ignore = '__ignore',
  81. }
  82. export const configMapHandlers: FieldToConfigMapHandler[] = [
  83. {
  84. key: FieldConfigHandlerKey.Name,
  85. name: 'Field name',
  86. processor: () => {},
  87. },
  88. {
  89. key: FieldConfigHandlerKey.Value,
  90. name: 'Field value',
  91. processor: () => {},
  92. },
  93. {
  94. key: FieldConfigHandlerKey.Label,
  95. name: 'Field label',
  96. processor: () => {},
  97. },
  98. {
  99. key: FieldConfigHandlerKey.Ignore,
  100. name: 'Ignore',
  101. processor: () => {},
  102. },
  103. {
  104. key: 'max',
  105. processor: toNumericOrUndefined,
  106. },
  107. {
  108. key: 'min',
  109. processor: toNumericOrUndefined,
  110. },
  111. {
  112. key: 'unit',
  113. processor: (value) => value.toString(),
  114. },
  115. {
  116. key: 'decimals',
  117. processor: toNumericOrUndefined,
  118. },
  119. {
  120. key: 'displayName',
  121. name: 'Display name',
  122. processor: (value: any) => value.toString(),
  123. },
  124. {
  125. key: 'color',
  126. processor: (value) => ({ fixedColor: value, mode: FieldColorModeId.Fixed }),
  127. },
  128. {
  129. key: 'threshold1',
  130. targetProperty: 'thresholds',
  131. processor: (value, config) => {
  132. const numeric = anyToNumber(value);
  133. if (isNaN(numeric)) {
  134. return;
  135. }
  136. if (!config.thresholds) {
  137. config.thresholds = {
  138. mode: ThresholdsMode.Absolute,
  139. steps: [{ value: -Infinity, color: 'green' }],
  140. };
  141. }
  142. config.thresholds.steps.push({
  143. value: numeric,
  144. color: 'red',
  145. });
  146. return config.thresholds;
  147. },
  148. },
  149. {
  150. key: 'mappings.value',
  151. name: 'Value mappings / Value',
  152. targetProperty: 'mappings',
  153. defaultReducer: ReducerID.allValues,
  154. processor: (value, config, context) => {
  155. if (!isArray(value)) {
  156. return;
  157. }
  158. context.mappingValues = value;
  159. return config.mappings;
  160. },
  161. },
  162. {
  163. key: 'mappings.color',
  164. name: 'Value mappings / Color',
  165. targetProperty: 'mappings',
  166. defaultReducer: ReducerID.allValues,
  167. processor: (value, config, context) => {
  168. if (!isArray(value)) {
  169. return;
  170. }
  171. context.mappingColors = value;
  172. return config.mappings;
  173. },
  174. },
  175. {
  176. key: 'mappings.text',
  177. name: 'Value mappings / Display text',
  178. targetProperty: 'mappings',
  179. defaultReducer: ReducerID.allValues,
  180. processor: (value, config, context) => {
  181. if (!isArray(value)) {
  182. return;
  183. }
  184. context.mappingTexts = value;
  185. return config.mappings;
  186. },
  187. },
  188. ];
  189. function combineValueMappings(context: FieldToConfigContext): ValueMapping[] {
  190. const valueMap: ValueMap = {
  191. type: MappingType.ValueToText,
  192. options: {},
  193. };
  194. if (!context.mappingValues) {
  195. return [];
  196. }
  197. for (let i = 0; i < context.mappingValues.length; i++) {
  198. const value = context.mappingValues[i];
  199. if (value != null) {
  200. valueMap.options[value.toString()] = {
  201. color: context.mappingColors && context.mappingColors[i],
  202. text: context.mappingTexts && context.mappingTexts[i],
  203. index: i,
  204. };
  205. }
  206. }
  207. return [valueMap];
  208. }
  209. let configMapHandlersIndex: Record<string, FieldToConfigMapHandler> | null = null;
  210. export function getConfigMapHandlersIndex() {
  211. if (configMapHandlersIndex === null) {
  212. configMapHandlersIndex = {};
  213. for (const def of configMapHandlers) {
  214. configMapHandlersIndex[def.key] = def;
  215. }
  216. }
  217. return configMapHandlersIndex;
  218. }
  219. function toNumericOrUndefined(value: any) {
  220. const numeric = anyToNumber(value);
  221. if (isNaN(numeric)) {
  222. return;
  223. }
  224. return numeric;
  225. }
  226. export function getConfigHandlerKeyForField(fieldName: string, mappings: FieldToConfigMapping[]) {
  227. for (const map of mappings) {
  228. if (fieldName === map.fieldName) {
  229. return map.handlerKey;
  230. }
  231. }
  232. return fieldName.toLowerCase();
  233. }
  234. export function lookUpConfigHandler(key: string | null): FieldToConfigMapHandler | null {
  235. if (!key) {
  236. return null;
  237. }
  238. return getConfigMapHandlersIndex()[key];
  239. }
  240. export interface EvaluatedMapping {
  241. automatic: boolean;
  242. handler: FieldToConfigMapHandler | null;
  243. reducerId: ReducerID;
  244. }
  245. export interface EvaluatedMappingResult {
  246. index: Record<string, EvaluatedMapping>;
  247. nameField?: Field;
  248. valueField?: Field;
  249. }
  250. export function evaluteFieldMappings(
  251. frame: DataFrame,
  252. mappings: FieldToConfigMapping[],
  253. withNameAndValue?: boolean
  254. ): EvaluatedMappingResult {
  255. const result: EvaluatedMappingResult = {
  256. index: {},
  257. };
  258. // Look up name and value field in mappings
  259. let nameFieldMappping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Name);
  260. let valueFieldMapping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Value);
  261. for (const field of frame.fields) {
  262. const fieldName = getFieldDisplayName(field, frame);
  263. const mapping = mappings.find((x) => x.fieldName === fieldName);
  264. const key = mapping ? mapping.handlerKey : fieldName.toLowerCase();
  265. let handler = lookUpConfigHandler(key);
  266. // Name and value handlers are a special as their auto logic is based on first matching criteria
  267. if (withNameAndValue) {
  268. // If we have a handler it means manually specified field
  269. if (handler) {
  270. if (handler.key === FieldConfigHandlerKey.Name) {
  271. result.nameField = field;
  272. }
  273. if (handler.key === FieldConfigHandlerKey.Value) {
  274. result.valueField = field;
  275. }
  276. } else if (!mapping) {
  277. // We have no name field and no mapping for it, pick first string
  278. if (!result.nameField && !nameFieldMappping && field.type === FieldType.string) {
  279. result.nameField = field;
  280. handler = lookUpConfigHandler(FieldConfigHandlerKey.Name);
  281. }
  282. if (!result.valueField && !valueFieldMapping && field.type === FieldType.number) {
  283. result.valueField = field;
  284. handler = lookUpConfigHandler(FieldConfigHandlerKey.Value);
  285. }
  286. }
  287. }
  288. // If no handle and when in name and value mode (Rows to fields) default to labels
  289. if (!handler && withNameAndValue) {
  290. handler = lookUpConfigHandler(FieldConfigHandlerKey.Label);
  291. }
  292. result.index[fieldName] = {
  293. automatic: !mapping,
  294. handler: handler,
  295. reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull,
  296. };
  297. }
  298. return result;
  299. }