fieldDisplay.ts 9.09 KB
Newer Older
1
import toString from 'lodash/toString';
2
import isEmpty from 'lodash/isEmpty';
3

4
import { getDisplayProcessor } from './displayProcessor';
5 6 7 8
import {
  DataFrame,
  DisplayValue,
  DisplayValueAlignmentFactors,
9
  Field,
10
  FieldConfig,
11
  FieldConfigSource,
12
  FieldType,
13
  InterpolateFunction,
14
  LinkModel,
15
  TimeRange,
16
  TimeZone,
17
} from '../types';
18 19
import { DataFrameView } from '../dataframe/DataFrameView';
import { GrafanaTheme } from '../types/theme';
20
import { reduceField, ReducerID } from '../transformations/fieldReducer';
21 22
import { ScopedVars } from '../types/ScopedVars';
import { getTimeField } from '../dataframe/processDataFrame';
23 24
import { getFieldMatcher } from '../transformations';
import { FieldMatcherID } from '../transformations/matchers/ids';
25

26 27 28 29 30 31 32 33 34 35
/**
 * Options for how to turn DataFrames into an array of display values
 */
export interface ReduceDataOptions {
  /* If true show each row value */
  values?: boolean;
  /** if showing all values limit */
  limit?: number;
  /** When !values, pick one value for the whole field */
  calcs: string[];
36 37
  /** Which fields to show.  By default this is only numeric fields */
  fields?: string;
38
}
39

40
// TODO: use built in variables, same as for data links?
41
export const VAR_SERIES_NAME = '__series.name';
42
export const VAR_FIELD_NAME = '__field.displayName'; // Includes the rendered tags and naming strategy
43
export const VAR_FIELD_LABELS = '__field.labels';
44 45 46
export const VAR_CALC = '__calc';
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates

47
function getTitleTemplate(stats: string[]): string {
48 49
  const parts: string[] = [];
  if (stats.length > 1) {
50
    parts.push('${' + VAR_CALC + '}');
51
  }
52 53

  parts.push('${' + VAR_FIELD_NAME + '}');
54

55 56 57
  return parts.join(' ');
}

58 59 60 61 62 63 64
export interface FieldSparkline {
  y: Field; // Y values
  x?: Field; // if this does not exist, use the index
  timeRange?: TimeRange; // Optionally force an absolute time
  highlightIndex?: number;
}

65
export interface FieldDisplay {
66
  name: string; // The field name (title is in display)
67
  field: FieldConfig;
68
  display: DisplayValue;
69
  sparkline?: FieldSparkline;
70 71 72

  // Expose to the original values for delayed inspection (DataLinks etc)
  view?: DataFrameView;
73 74
  colIndex?: number; // The field column index
  rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction)
75
  getLinks?: () => LinkModel[];
76
  hasLinks: boolean;
77 78 79
}

export interface GetFieldDisplayValuesOptions {
80
  data?: DataFrame[];
81
  reduceOptions: ReduceDataOptions;
82
  fieldConfig: FieldConfigSource;
83 84 85
  replaceVariables: InterpolateFunction;
  sparkline?: boolean; // Calculate the sparkline
  theme: GrafanaTheme;
86
  timeZone?: TimeZone;
87 88 89 90 91
}

export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;

export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
92
  const { replaceVariables, reduceOptions, timeZone } = options;
93
  const calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last];
94 95

  const values: FieldDisplay[] = [];
96 97 98 99 100 101 102 103 104 105
  const fieldMatcher = getFieldMatcher(
    reduceOptions.fields
      ? {
          id: FieldMatcherID.byRegexp,
          options: reduceOptions.fields,
        }
      : {
          id: FieldMatcherID.numeric,
        }
  );
106

107
  if (options.data) {
108 109
    // Field overrides are applied already
    const data = options.data;
110
    let hitLimit = false;
111
    const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
112
    const scopedVars: ScopedVars = {};
113
    const defaultDisplayName = getTitleTemplate(calcs);
114 115

    for (let s = 0; s < data.length && !hitLimit; s++) {
116
      const series = data[s]; // Name is already set
117

118
      const { timeField } = getTimeField(series);
119
      const view = new DataFrameView(series);
120 121

      for (let i = 0; i < series.fields.length && !hitLimit; i++) {
122
        const field = series.fields[i];
123
        const fieldLinksSupplier = field.getLinks;
124 125

        // To filter out time field, need an option for this
126
        if (!fieldMatcher(field, series, data)) {
127 128
          continue;
        }
129

130 131 132 133 134
        let config = field.config; // already set by the prepare task
        if (field.state?.range) {
          // Us the global min/max values
          config = { ...config, ...field.state?.range };
        }
135
        const displayName = field.config.displayName ?? defaultDisplayName;
136

137 138 139
        const display =
          field.display ??
          getDisplayProcessor({
140
            field,
141
            theme: options.theme,
142
            timeZone,
143
          });
144

145
        // Show all rows
146
        if (reduceOptions.values) {
147
          const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
148

149
          for (let j = 0; j < field.values.length; j++) {
150 151
            // Add all the row variables
            if (usesCellValues) {
152 153 154 155 156 157
              for (let k = 0; k < series.fields.length; k++) {
                const f = series.fields[k];
                const v = f.values.get(j);
                scopedVars[VAR_CELL_PREFIX + k] = {
                  value: v,
                  text: toString(v),
158 159 160
                };
              }
            }
161

162
            const displayValue = display(field.values.get(j));
163
            displayValue.title = replaceVariables(displayName, {
164
              ...field.state?.scopedVars, // series and field scoped vars
165 166 167
              ...scopedVars,
            });

168
            values.push({
169
              name: '',
170
              field: config,
171
              display: displayValue,
172
              view,
173 174
              colIndex: i,
              rowIndex: j,
175 176 177 178 179 180
              getLinks: fieldLinksSupplier
                ? () =>
                    fieldLinksSupplier({
                      valueRowIndex: j,
                    })
                : () => [],
181
              hasLinks: hasLinks(field),
182 183 184 185 186 187 188 189 190
            });

            if (values.length >= limit) {
              hitLimit = true;
              break;
            }
          }
        } else {
          const results = reduceField({
191
            field,
192 193 194 195 196 197
            reducers: calcs, // The stats to calculate
          });

          for (const calc of calcs) {
            scopedVars[VAR_CALC] = { value: calc, text: calc };
            const displayValue = display(results[calc]);
198
            displayValue.title = replaceVariables(displayName, {
199
              ...field.state?.scopedVars, // series and field scoped vars
200 201
              ...scopedVars,
            });
202

203 204 205 206 207 208 209 210 211 212 213 214 215
            let sparkline: FieldSparkline | undefined = undefined;
            if (options.sparkline) {
              sparkline = {
                y: series.fields[i],
                x: timeField,
              };
              if (calc === ReducerID.last) {
                sparkline.highlightIndex = sparkline.y.values.length - 1;
              } else if (calc === ReducerID.first) {
                sparkline.highlightIndex = 0;
              }
            }

216
            values.push({
217
              name: calc,
218
              field: config,
219
              display: displayValue,
220 221
              sparkline,
              view,
222
              colIndex: i,
223 224 225 226 227 228
              getLinks: fieldLinksSupplier
                ? () =>
                    fieldLinksSupplier({
                      calculatedValue: displayValue,
                    })
                : () => [],
229
              hasLinks: hasLinks(field),
230 231 232 233 234 235 236 237
            });
          }
        }
      }
    }
  }

  if (values.length === 0) {
238
    values.push(createNoValuesFieldDisplay(options));
239 240 241 242 243
  }

  return values;
};

244 245 246 247
export function hasLinks(field: Field): boolean {
  return field.config?.links?.length ? field.config.links.length > 0 : false;
}

248 249 250 251 252 253
export function getDisplayValueAlignmentFactors(values: FieldDisplay[]): DisplayValueAlignmentFactors {
  const info: DisplayValueAlignmentFactors = {
    title: '',
    text: '',
  };

254 255 256
  let prefixLength = 0;
  let suffixLength = 0;

257 258
  for (let i = 0; i < values.length; i++) {
    const v = values[i].display;
259

260 261 262 263 264 265 266
    if (v.text && v.text.length > info.text.length) {
      info.text = v.text;
    }

    if (v.title && v.title.length > info.title.length) {
      info.title = v.title;
    }
267 268 269 270 271 272 273 274 275 276

    if (v.prefix && v.prefix.length > prefixLength) {
      info.prefix = v.prefix;
      prefixLength = v.prefix.length;
    }

    if (v.suffix && v.suffix.length > suffixLength) {
      info.suffix = v.suffix;
      suffixLength = v.suffix.length;
    }
277 278 279
  }
  return info;
}
280 281 282

function createNoValuesFieldDisplay(options: GetFieldDisplayValuesOptions): FieldDisplay {
  const displayName = 'No data';
283
  const { fieldConfig, timeZone } = options;
284
  const { defaults } = fieldConfig;
285 286

  const displayProcessor = getDisplayProcessor({
287 288 289 290
    field: {
      type: FieldType.other,
      config: defaults,
    },
291
    theme: options.theme,
292
    timeZone,
293 294 295 296 297 298 299 300 301
  });

  const display = displayProcessor(null);
  const text = getDisplayText(display, displayName);

  return {
    name: displayName,
    field: {
      ...defaults,
302 303
      max: defaults.max ?? 0,
      min: defaults.min ?? 0,
304 305 306 307
    },
    display: {
      text,
      numeric: 0,
308
      color: display.color,
309
    },
310
    hasLinks: false,
311 312 313 314 315 316 317 318 319
  };
}

function getDisplayText(display: DisplayValue, fallback: string): string {
  if (!display || isEmpty(display.text)) {
    return fallback;
  }
  return display.text;
}