fieldDisplay.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {
  2. ReducerID,
  3. reduceField,
  4. FieldType,
  5. DataFrame,
  6. FieldConfig,
  7. DisplayValue,
  8. GraphSeriesValue,
  9. DataFrameView,
  10. getTimeField,
  11. } from '@grafana/data';
  12. import toNumber from 'lodash/toNumber';
  13. import toString from 'lodash/toString';
  14. import { GrafanaTheme, InterpolateFunction, ScopedVars } from '../types/index';
  15. import { getDisplayProcessor } from './displayProcessor';
  16. import { getFlotPairs } from './flotPairs';
  17. import { DataLinkBuiltInVars } from '../utils/dataLinks';
  18. export interface FieldDisplayOptions {
  19. values?: boolean; // If true show each row value
  20. limit?: number; // if showing all values limit
  21. calcs: string[]; // when !values, pick one value for the whole field
  22. defaults: FieldConfig; // Use these values unless otherwise stated
  23. override: FieldConfig; // Set these values regardless of the source
  24. }
  25. // TODO: use built in variables, same as for data links?
  26. export const VAR_SERIES_NAME = '__series_name';
  27. export const VAR_FIELD_NAME = '__field_name';
  28. export const VAR_CALC = '__calc';
  29. export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
  30. function getTitleTemplate(title: string | undefined, stats: string[], data?: DataFrame[]): string {
  31. // If the title exists, use it as a template variable
  32. if (title) {
  33. return title;
  34. }
  35. if (!data || !data.length) {
  36. return 'No Data';
  37. }
  38. let fieldCount = 0;
  39. for (const field of data[0].fields) {
  40. if (field.type === FieldType.number) {
  41. fieldCount++;
  42. }
  43. }
  44. const parts: string[] = [];
  45. if (stats.length > 1) {
  46. parts.push('$' + VAR_CALC);
  47. }
  48. if (data.length > 1) {
  49. parts.push('$' + VAR_SERIES_NAME);
  50. }
  51. if (fieldCount > 1 || !parts.length) {
  52. parts.push('$' + VAR_FIELD_NAME);
  53. }
  54. return parts.join(' ');
  55. }
  56. export interface FieldDisplay {
  57. name: string; // The field name (title is in display)
  58. field: FieldConfig;
  59. display: DisplayValue;
  60. sparkline?: GraphSeriesValue[][];
  61. // Expose to the original values for delayed inspection (DataLinks etc)
  62. view?: DataFrameView;
  63. column?: number; // The field column index
  64. row?: number; // only filled in when the value is from a row (ie, not a reduction)
  65. }
  66. export interface GetFieldDisplayValuesOptions {
  67. data?: DataFrame[];
  68. fieldOptions: FieldDisplayOptions;
  69. replaceVariables: InterpolateFunction;
  70. sparkline?: boolean; // Calculate the sparkline
  71. theme: GrafanaTheme;
  72. }
  73. export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
  74. export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
  75. const { data, replaceVariables, fieldOptions } = options;
  76. const { defaults, override } = fieldOptions;
  77. const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
  78. const values: FieldDisplay[] = [];
  79. if (data) {
  80. let hitLimit = false;
  81. const limit = fieldOptions.limit ? fieldOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
  82. const defaultTitle = getTitleTemplate(fieldOptions.defaults.title, calcs, data);
  83. const scopedVars: ScopedVars = {};
  84. for (let s = 0; s < data.length && !hitLimit; s++) {
  85. let series = data[s];
  86. if (!series.name) {
  87. series = {
  88. ...series,
  89. name: series.refId ? series.refId : `Series[${s}]`,
  90. };
  91. }
  92. scopedVars[DataLinkBuiltInVars.seriesName] = { text: 'Series', value: series.name };
  93. const { timeField } = getTimeField(series);
  94. const view = new DataFrameView(series);
  95. for (let i = 0; i < series.fields.length && !hitLimit; i++) {
  96. const field = series.fields[i];
  97. // Show all number fields
  98. if (field.type !== FieldType.number) {
  99. continue;
  100. }
  101. const config = getFieldProperties(defaults, field.config || {}, override);
  102. let name = field.name;
  103. if (!name) {
  104. name = `Field[${s}]`;
  105. }
  106. scopedVars[VAR_FIELD_NAME] = { text: 'Field', value: name };
  107. const display = getDisplayProcessor({
  108. field: config,
  109. theme: options.theme,
  110. });
  111. const title = config.title ? config.title : defaultTitle;
  112. // Show all rows
  113. if (fieldOptions.values) {
  114. const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
  115. for (let j = 0; j < field.values.length; j++) {
  116. // Add all the row variables
  117. if (usesCellValues) {
  118. for (let k = 0; k < series.fields.length; k++) {
  119. const f = series.fields[k];
  120. const v = f.values.get(j);
  121. scopedVars[VAR_CELL_PREFIX + k] = {
  122. value: v,
  123. text: toString(v),
  124. };
  125. }
  126. }
  127. const displayValue = display(field.values.get(j));
  128. displayValue.title = replaceVariables(title, scopedVars);
  129. values.push({
  130. name,
  131. field: config,
  132. display: displayValue,
  133. view,
  134. column: i,
  135. row: j,
  136. });
  137. if (values.length >= limit) {
  138. hitLimit = true;
  139. break;
  140. }
  141. }
  142. } else {
  143. const results = reduceField({
  144. field,
  145. reducers: calcs, // The stats to calculate
  146. });
  147. let sparkline: GraphSeriesValue[][] | undefined = undefined;
  148. // Single sparkline for every reducer
  149. if (options.sparkline && timeField) {
  150. sparkline = getFlotPairs({
  151. xField: timeField,
  152. yField: series.fields[i],
  153. });
  154. }
  155. for (const calc of calcs) {
  156. scopedVars[VAR_CALC] = { value: calc, text: calc };
  157. const displayValue = display(results[calc]);
  158. displayValue.title = replaceVariables(title, scopedVars);
  159. values.push({
  160. name,
  161. field: config,
  162. display: displayValue,
  163. sparkline,
  164. view,
  165. column: i,
  166. });
  167. }
  168. }
  169. }
  170. }
  171. }
  172. if (values.length === 0) {
  173. values.push({
  174. name: 'No data',
  175. field: {
  176. ...defaults,
  177. },
  178. display: {
  179. numeric: 0,
  180. text: 'No data',
  181. },
  182. });
  183. } else if (values.length === 1 && !fieldOptions.defaults.title) {
  184. // Don't show title for single item
  185. values[0].display.title = undefined;
  186. }
  187. return values;
  188. };
  189. const numericFieldProps: any = {
  190. decimals: true,
  191. min: true,
  192. max: true,
  193. };
  194. /**
  195. * Returns a version of the field with the overries applied. Any property with
  196. * value: null | undefined | empty string are skipped.
  197. *
  198. * For numeric values, only valid numbers will be applied
  199. * for units, 'none' will be skipped
  200. */
  201. export function applyFieldProperties(field: FieldConfig, props?: FieldConfig): FieldConfig {
  202. if (!props) {
  203. return field;
  204. }
  205. const keys = Object.keys(props);
  206. if (!keys.length) {
  207. return field;
  208. }
  209. const copy = { ...field } as any; // make a copy that we will manipulate directly
  210. for (const key of keys) {
  211. const val = (props as any)[key];
  212. if (val === null || val === undefined) {
  213. continue;
  214. }
  215. if (numericFieldProps[key]) {
  216. const num = toNumber(val);
  217. if (!isNaN(num)) {
  218. copy[key] = num;
  219. }
  220. } else if (val) {
  221. // skips empty string
  222. if (key === 'unit' && val === 'none') {
  223. continue;
  224. }
  225. copy[key] = val;
  226. }
  227. }
  228. return copy as FieldConfig;
  229. }
  230. export function getFieldProperties(...props: FieldConfig[]): FieldConfig {
  231. let field = props[0] as FieldConfig;
  232. for (let i = 1; i < props.length; i++) {
  233. field = applyFieldProperties(field, props[i]);
  234. }
  235. // First value is always -Infinity
  236. if (field.thresholds && field.thresholds.length) {
  237. field.thresholds[0].value = -Infinity;
  238. }
  239. // Verify that max > min
  240. if (field.hasOwnProperty('min') && field.hasOwnProperty('max') && field.min! > field.max!) {
  241. return {
  242. ...field,
  243. min: field.max,
  244. max: field.min,
  245. };
  246. }
  247. return field;
  248. }