From dd1486eef6100ff1b20b8b0fdb3081b9021b5dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 5 Mar 2021 10:42:37 +0100 Subject: [PATCH] FieldDisplay: Smarter naming of stat values when visualising row values (all values) in stat panels (#31704) * StatPanels: Improving naming * Finally making progress * Removed default templaet * Removed unused function --- .../src/field/fieldDisplay.test.ts | 94 ++++++ .../grafana-data/src/field/fieldDisplay.ts | 278 ++++++++++-------- 2 files changed, 245 insertions(+), 127 deletions(-) diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index e521232a36e..fc8325e10e9 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -203,6 +203,100 @@ describe('FieldDisplay', () => { expect(result[3].display.text).toEqual(mappedValue); }); }); + + describe('auto option', () => { + it('No string fields, single value', () => { + const options = createDisplayOptions({ + reduceOptions: { + values: true, + calcs: [], + }, + data: [ + toDataFrame({ + name: 'Series Name', + fields: [{ name: 'A', values: [10] }], + }), + ], + }); + + const result = getFieldDisplayValues(options); + expect(result[0].display.title).toEqual('A'); + expect(result[0].display.text).toEqual('10'); + }); + + it('Single other string field', () => { + const options = createDisplayOptions({ + reduceOptions: { + values: true, + calcs: [], + }, + data: [ + toDataFrame({ + fields: [ + { name: 'Name', values: ['A', 'B'] }, + { name: 'Value', values: [10, 20] }, + ], + }), + ], + }); + + const result = getFieldDisplayValues(options); + expect(result[0].display.title).toEqual('A'); + expect(result[0].display.text).toEqual('10'); + expect(result[1].display.title).toEqual('B'); + expect(result[1].display.text).toEqual('20'); + }); + + it('Single string field multiple value fields', () => { + const options = createDisplayOptions({ + reduceOptions: { + values: true, + calcs: [], + }, + data: [ + toDataFrame({ + fields: [ + { name: 'Name', values: ['A', 'B'] }, + { name: 'SensorA', values: [10, 20] }, + { name: 'SensorB', values: [10, 20] }, + ], + }), + ], + }); + + const result = getFieldDisplayValues(options); + expect(result[0].display.title).toEqual('A SensorA'); + expect(result[0].display.text).toEqual('10'); + expect(result[1].display.title).toEqual('B SensorA'); + expect(result[1].display.text).toEqual('20'); + expect(result[2].display.title).toEqual('A SensorB'); + expect(result[3].display.title).toEqual('B SensorB'); + }); + + it('Multiple other string fields', () => { + const options = createDisplayOptions({ + reduceOptions: { + values: true, + calcs: [], + }, + data: [ + toDataFrame({ + fields: [ + { name: 'Country', values: ['Sweden', 'Norway'] }, + { name: 'City', values: ['Stockholm', 'Oslo'] }, + { name: 'Value', values: [10, 20] }, + ], + }), + ], + }); + + const result = getFieldDisplayValues(options); + expect(result[0].display.title).toEqual('Sweden Stockholm'); + expect(result[0].display.text).toEqual('10'); + expect(result[1].display.title).toEqual('Norway Oslo'); + expect(result[1].display.text).toEqual('20'); + }); + }); }); function createEmptyDisplayOptions(extend = {}): GetFieldDisplayValuesOptions { diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index d0f2000a11c..551c551cc43 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -22,6 +22,7 @@ import { ScopedVars } from '../types/ScopedVars'; import { getTimeField } from '../dataframe/processDataFrame'; import { getFieldMatcher } from '../transformations'; import { FieldMatcherID } from '../transformations/matchers/ids'; +import { getFieldDisplayName } from './fieldState'; /** * Options for how to turn DataFrames into an array of display values @@ -44,17 +45,6 @@ export const VAR_FIELD_LABELS = '__field.labels'; export const VAR_CALC = '__calc'; export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates -function getTitleTemplate(stats: string[]): string { - const parts: string[] = []; - if (stats.length > 1) { - parts.push('${' + VAR_CALC + '}'); - } - - parts.push('${' + VAR_FIELD_NAME + '}'); - - return parts.join(' '); -} - export interface FieldSparkline { y: Field; // Y values x?: Field; // if this does not exist, use the index @@ -104,136 +94,144 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi } ); - if (options.data) { - // Field overrides are applied already - const data = options.data; - let hitLimit = false; - const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; - const scopedVars: ScopedVars = {}; - const defaultDisplayName = getTitleTemplate(calcs); + const data = options.data ?? []; + const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; + const scopedVars: ScopedVars = {}; - for (let s = 0; s < data.length && !hitLimit; s++) { - const series = data[s]; // Name is already set + let hitLimit = false; - const { timeField } = getTimeField(series); - const view = new DataFrameView(series); + for (let s = 0; s < data.length && !hitLimit; s++) { + const dataFrame = data[s]; // Name is already set - for (let i = 0; i < series.fields.length && !hitLimit; i++) { - const field = series.fields[i]; - const fieldLinksSupplier = field.getLinks; + const { timeField } = getTimeField(dataFrame); + const view = new DataFrameView(dataFrame); - // To filter out time field, need an option for this - if (!fieldMatcher(field, series, data)) { - continue; - } + for (let i = 0; i < dataFrame.fields.length && !hitLimit; i++) { + const field = dataFrame.fields[i]; + const fieldLinksSupplier = field.getLinks; - let config = field.config; // already set by the prepare task + // To filter out time field, need an option for this + if (!fieldMatcher(field, dataFrame, data)) { + continue; + } - if (field.state?.range) { - // Us the global min/max values - config = { - ...config, - ...field.state?.range, - }; - } + let config = field.config; // already set by the prepare task - const displayName = field.config.displayName ?? defaultDisplayName; + if (field.state?.range) { + // Us the global min/max values + config = { + ...config, + ...field.state?.range, + }; + } - const display = - field.display ?? - getDisplayProcessor({ - field, - theme: options.theme, - timeZone, - }); + // const displayName = getFieldDisplayName(field, dataFrame, data); + const displayName = field.config.displayName ?? ''; - // Show all rows - if (reduceOptions.values) { - const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0; + const display = + field.display ?? + getDisplayProcessor({ + field, + theme: options.theme, + timeZone, + }); - for (let j = 0; j < field.values.length; j++) { - // Add all the row variables - if (usesCellValues) { - 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), - }; - } - } + // Show all rows + if (reduceOptions.values) { + const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0; - const displayValue = display(field.values.get(j)); - displayValue.title = replaceVariables(displayName, { - ...field.state?.scopedVars, // series and field scoped vars - ...scopedVars, - }); - - values.push({ - name: '', - field: config, - display: displayValue, - view, - colIndex: i, - rowIndex: j, - getLinks: fieldLinksSupplier - ? () => - fieldLinksSupplier({ - valueRowIndex: j, - }) - : () => [], - hasLinks: hasLinks(field), - }); - - if (values.length >= limit) { - hitLimit = true; - break; - } - } - } else { - const results = reduceField({ - field, - reducers: calcs, // The stats to calculate - }); - - for (const calc of calcs) { - scopedVars[VAR_CALC] = { value: calc, text: calc }; - const displayValue = display(results[calc]); - displayValue.title = replaceVariables(displayName, { - ...field.state?.scopedVars, // series and field scoped vars - ...scopedVars, - }); - - let sparkline: FieldSparkline | undefined = undefined; - if (options.sparkline) { - sparkline = { - y: series.fields[i], - x: timeField, + for (let j = 0; j < field.values.length; j++) { + // Add all the row variables + if (usesCellValues) { + for (let k = 0; k < dataFrame.fields.length; k++) { + const f = dataFrame.fields[k]; + const v = f.values.get(j); + scopedVars[VAR_CELL_PREFIX + k] = { + value: v, + text: toString(v), }; - if (calc === ReducerID.last) { - sparkline.highlightIndex = sparkline.y.values.length - 1; - } else if (calc === ReducerID.first) { - sparkline.highlightIndex = 0; - } } - - values.push({ - name: calc, - field: config, - display: displayValue, - sparkline, - view, - colIndex: i, - getLinks: fieldLinksSupplier - ? () => - fieldLinksSupplier({ - calculatedValue: displayValue, - }) - : () => [], - hasLinks: hasLinks(field), - }); } + + const displayValue = display(field.values.get(j)); + + if (displayName !== '') { + displayValue.title = replaceVariables(displayName, { + ...field.state?.scopedVars, // series and field scoped vars + ...scopedVars, + }); + } else { + displayValue.title = getSmartDisplayNameForRow(dataFrame, field, j); + } + + values.push({ + name: '', + field: config, + display: displayValue, + view, + colIndex: i, + rowIndex: j, + getLinks: fieldLinksSupplier + ? () => + fieldLinksSupplier({ + valueRowIndex: j, + }) + : () => [], + hasLinks: hasLinks(field), + }); + + if (values.length >= limit) { + hitLimit = true; + break; + } + } + } else { + const results = reduceField({ + field, + reducers: calcs, // The stats to calculate + }); + + for (const calc of calcs) { + scopedVars[VAR_CALC] = { value: calc, text: calc }; + const displayValue = display(results[calc]); + + if (displayName !== '') { + displayValue.title = replaceVariables(displayName, { + ...field.state?.scopedVars, // series and field scoped vars + ...scopedVars, + }); + } else { + displayValue.title = getFieldDisplayName(field, dataFrame, data); + } + + let sparkline: FieldSparkline | undefined = undefined; + if (options.sparkline) { + sparkline = { + y: dataFrame.fields[i], + x: timeField, + }; + if (calc === ReducerID.last) { + sparkline.highlightIndex = sparkline.y.values.length - 1; + } else if (calc === ReducerID.first) { + sparkline.highlightIndex = 0; + } + } + + values.push({ + name: calc, + field: config, + display: displayValue, + sparkline, + view, + colIndex: i, + getLinks: fieldLinksSupplier + ? () => + fieldLinksSupplier({ + calculatedValue: displayValue, + }) + : () => [], + hasLinks: hasLinks(field), + }); } } } @@ -246,6 +244,32 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi return values; }; +function getSmartDisplayNameForRow(frame: DataFrame, field: Field, rowIndex: number): string { + let parts: string[] = []; + let otherNumericFields = 0; + + for (const otherField of frame.fields) { + if (otherField === field) { + continue; + } + + if (otherField.type === FieldType.string) { + const value = otherField.values.get(rowIndex) ?? ''; + if (value.length > 0) { + parts.push(value); + } + } else if (otherField.type === FieldType.number) { + otherNumericFields++; + } + } + + if (otherNumericFields || parts.length === 0) { + parts.push(getFieldDisplayName(field)); + } + + return parts.join(' '); +} + export function hasLinks(field: Field): boolean { return field.config?.links?.length ? field.config.links.length > 0 : false; }