diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 2db843d3abf..bfb9eb733ee 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -22,7 +22,8 @@ describe('toDataFrame', () => { ], }; let series = toDataFrame(input1); - expect(series.fields[1].name).toBe(input1.target); + expect(series.name).toBe(input1.target); + expect(series.fields[1].name).toBe('Value'); const v0 = series.fields[0].values; const v1 = series.fields[1].values; @@ -182,6 +183,24 @@ describe('SerisData backwards compatibility', () => { expect(roundtrip.target).toBe(timeseries.target); }); + it('can convert TimeSeries to series and back again with tags should render name with tags', () => { + const timeseries = { + target: 'Series A', + tags: { server: 'ServerA', job: 'app' }, + datapoints: [ + [100, 1], + [200, 2], + ], + }; + const series = toDataFrame(timeseries); + expect(isDataFrame(timeseries)).toBeFalsy(); + expect(isDataFrame(series)).toBeTruthy(); + + const roundtrip = toLegacyResponseData(series) as TimeSeries; + expect(isDataFrame(roundtrip)).toBeFalsy(); + expect(roundtrip.target).toBe('{job="app", server="ServerA"}'); + }); + it('can convert empty table to DataFrame then back to legacy', () => { const table = { columns: [], diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index a34a93cb6a2..323fe61794e 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -14,12 +14,14 @@ import { TimeSeriesValue, FieldDTO, DataFrameDTO, + TIME_SERIES_FIELD_NAME, } from '../types/index'; import { isDateTime } from '../datetime/moment_wrapper'; import { ArrayVector } from '../vector/ArrayVector'; import { MutableDataFrame } from './MutableDataFrame'; import { SortedVector } from '../vector/SortedVector'; import { ArrayDataFrame } from './ArrayDataFrame'; +import { getFieldTitle } from '../field/fieldState'; function convertTableToDataFrame(table: TableData): DataFrame { const fields = table.columns.map(c => { @@ -61,6 +63,7 @@ function convertTableToDataFrame(table: TableData): DataFrame { function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { const times: number[] = []; const values: TimeSeriesValue[] = []; + for (const point of timeSeries.datapoints) { values.push(point[0]); times.push(point[1] as number); @@ -74,7 +77,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { values: new ArrayVector(times), }, { - name: timeSeries.target || 'Value', + name: TIME_SERIES_FIELD_NAME, type: FieldType.number, config: { unit: timeSeries.unit, @@ -84,6 +87,10 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { }, ]; + if (timeSeries.title) { + (fields[1].config as FieldConfig).title = timeSeries.title; + } + return { name: timeSeries.target, refId: timeSeries.refId, @@ -111,7 +118,7 @@ function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame { name: graphSeries.label, fields: [ { - name: graphSeries.label || 'Value', + name: graphSeries.label || TIME_SERIES_FIELD_NAME, type: FieldType.number, config: {}, values: x, @@ -312,18 +319,20 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData = const { timeField, timeIndex } = getTimeField(frame); if (timeField) { const valueIndex = timeIndex === 0 ? 1 : 0; + const valueField = fields[valueIndex]; + const timeField = fields[timeIndex!]; // Make sure it is [value,time] for (let i = 0; i < rowCount; i++) { rows.push([ - fields[valueIndex].values.get(i), // value - fields[timeIndex!].values.get(i), // time + valueField.values.get(i), // value + timeField.values.get(i), // time ]); } return { - alias: fields[valueIndex].name || frame.name, - target: fields[valueIndex].name || frame.name, + alias: frame.name, + target: getFieldTitle(valueField, frame), datapoints: rows, unit: fields[0].config ? fields[0].config.unit : undefined, refId: frame.refId, @@ -432,18 +441,6 @@ export function reverseDataFrame(data: DataFrame): DataFrame { }; } -export const getTimeField = (series: DataFrame): { timeField?: Field; timeIndex?: number } => { - for (let i = 0; i < series.fields.length; i++) { - if (series.fields[i].type === FieldType.time) { - return { - timeField: series.fields[i], - timeIndex: i, - }; - } - } - return {}; -}; - /** * Wrapper to get an array from each field value */ @@ -487,3 +484,15 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO { name: data.name, }; } + +export const getTimeField = (series: DataFrame): { timeField?: Field; timeIndex?: number } => { + for (let i = 0; i < series.fields.length; i++) { + if (series.fields[i].type === FieldType.time) { + return { + timeField: series.fields[i], + timeIndex: i, + }; + } + } + return {}; +}; diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 9f284f9505d..77680031cf2 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -37,35 +37,17 @@ export interface ReduceDataOptions { // TODO: use built in variables, same as for data links? export const VAR_SERIES_NAME = '__series.name'; export const VAR_FIELD_NAME = '__field.name'; +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(title: string | undefined, stats: string[], data?: DataFrame[]): string { - // If the title exists, use it as a template variable - if (title) { - return title; - } - if (!data || !data.length) { - return 'No Data'; - } - - let fieldCount = 0; - for (const field of data[0].fields) { - if (field.type === FieldType.number) { - fieldCount++; - } - } - +function getTitleTemplate(stats: string[]): string { const parts: string[] = []; if (stats.length > 1) { parts.push('${' + VAR_CALC + '}'); } - if (data.length > 1) { - parts.push('${' + VAR_SERIES_NAME + '}'); - } - if (fieldCount > 1 || !parts.length) { - parts.push('${' + VAR_FIELD_NAME + '}'); - } + + parts.push('${' + VAR_FIELD_NAME + '}'); return parts.join(' '); } @@ -108,8 +90,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi const data = options.data; let hitLimit = false; const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; - const defaultTitle = getTitleTemplate(fieldConfig.defaults.title, calcs, data); const scopedVars: ScopedVars = {}; + const defaultTitle = getTitleTemplate(calcs); for (let s = 0; s < data.length && !hitLimit; s++) { const series = data[s]; // Name is already set @@ -120,11 +102,14 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi for (let i = 0; i < series.fields.length && !hitLimit; i++) { const field = series.fields[i]; const fieldLinksSupplier = field.getLinks; - // Show all number fields + + // To filter out time field, need an option for this if (field.type !== FieldType.number) { continue; } + const config = field.config; // already set by the prepare task + const title = field.config.title ?? defaultTitle; const display = field.display ?? @@ -134,7 +119,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi timeZone, }); - const title = config.title ? config.title : defaultTitle; // Show all rows if (reduceOptions.values) { const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0; @@ -151,9 +135,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi }; } } + const displayValue = display(field.values.get(j)); displayValue.title = replaceVariables(title, { - ...field.config.scopedVars, // series and field scoped vars + ...field.state?.scopedVars, // series and field scoped vars ...scopedVars, }); @@ -197,9 +182,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi scopedVars[VAR_CALC] = { value: calc, text: calc }; const displayValue = display(results[calc]); displayValue.title = replaceVariables(title, { - ...field.config.scopedVars, // series and field scoped vars + ...field.state?.scopedVars, // series and field scoped vars ...scopedVars, }); + values.push({ name: calc, field: config, diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 5a44485e87d..06e68206e5e 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -19,6 +19,7 @@ import { Registry } from '../utils'; import { mockStandardProperties } from '../utils/tests/mockStandardProperties'; import { FieldMatcherID } from '../transformations'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; +import { getFieldTitle } from './fieldState'; const property1 = { id: 'custom.property1', // Match field properties @@ -111,12 +112,14 @@ describe('applyFieldOverrides', () => { fieldConfigRegistry: new FieldConfigOptionsRegistry(), }); - expect(withOverrides[0].fields[0].config.scopedVars).toMatchInlineSnapshot(` + expect(withOverrides[0].fields[0].state!.scopedVars).toMatchInlineSnapshot(` Object { "__field": Object { "text": "Field", "value": Object { - "name": "message", + "label": undefined, + "labels": "", + "name": "A message", }, }, "__series": Object { @@ -128,12 +131,14 @@ describe('applyFieldOverrides', () => { } `); - expect(withOverrides[1].fields[0].config.scopedVars).toMatchInlineSnapshot(` + expect(withOverrides[1].fields[0].state!.scopedVars).toMatchInlineSnapshot(` Object { "__field": Object { "text": "Field", "value": Object { - "name": "info", + "label": undefined, + "labels": "", + "name": "B info", }, }, "__series": Object { @@ -152,16 +157,19 @@ describe('applyFieldOverrides', () => { min: 0, max: 100, }; + const f1 = { unit: 'ms', dateFormat: '', // should be ignored max: parseFloat('NOPE'), // should be ignored min: null, // should alo be ignored! + title: 'newTitle', }; const f: DataFrame = toDataFrame({ fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }], }); + const processed = applyFieldOverrides({ data: [f], fieldConfig: { @@ -172,11 +180,13 @@ describe('applyFieldOverrides', () => { replaceVariables: v => v, theme: {} as GrafanaTheme, })[0]; - const out = processed.fields[0].config; - expect(out.min).toEqual(0); - expect(out.max).toEqual(100); - expect(out.unit).toEqual('ms'); + const outField = processed.fields[0]; + + expect(outField.config.min).toEqual(0); + expect(outField.config.max).toEqual(100); + expect(outField.config.unit).toEqual('ms'); + expect(getFieldTitle(outField, f)).toEqual('newTitle'); }); it('will apply field overrides', () => { @@ -300,10 +310,8 @@ describe('setDynamicConfigValue', () => { it('applies dynamic config values', () => { const config = { title: 'test', - // custom: { - // property1: 1, - // }, }; + setDynamicConfigValue( config, { diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 866f1552b95..955308ffbe5 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -24,12 +24,15 @@ import set from 'lodash/set'; import unset from 'lodash/unset'; import get from 'lodash/get'; import { getDisplayProcessor } from './displayProcessor'; -import { getTimeField, guessFieldTypeForField } from '../dataframe'; +import { guessFieldTypeForField } from '../dataframe'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { DataLinkBuiltInVars, locationUtil } from '../utils'; import { formattedValueToString } from '../valueFormats'; import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; +import { formatLabels } from '../utils/labels'; +import { getFrameDisplayTitle, getFieldTitle } from './fieldState'; +import { getTimeField } from '../dataframe/processDataFrame'; interface OverrideProps { match: FieldMatcher; @@ -96,25 +99,31 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra } return options.data.map((frame, index) => { - let name = frame.name; - if (!name) { - name = `Series[${index}]`; - } - const scopedVars: ScopedVars = { - __series: { text: 'Series', value: { name } }, + __series: { text: 'Series', value: { name: getFrameDisplayTitle(frame, index) } }, // might be missing }; - const fields: Field[] = frame.fields.map((field, fieldIndex) => { + const fields: Field[] = frame.fields.map(field => { // Config is mutable within this scope - let fieldName = field.name; - if (!fieldName) { - fieldName = `Field[${fieldIndex}]`; - } const fieldScopedVars = { ...scopedVars }; - fieldScopedVars['__field'] = { text: 'Field', value: { name: fieldName } }; + const title = getFieldTitle(field, frame, options.data); - const config: FieldConfig = { ...field.config, scopedVars: fieldScopedVars } || {}; + fieldScopedVars['__field'] = { + text: 'Field', + value: { + name: title, // Generally appropriate (may include the series name if useful) + labels: formatLabels(field.labels!), + label: field.labels, + }, + }; + + field.state = { + ...field.state, + title: title, + scopedVars: fieldScopedVars, + }; + + const config: FieldConfig = { ...field.config }; const context = { field, data: options.data!, @@ -183,6 +192,10 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra ...field, config, type, + state: { + ...field.state, + title: null, + }, }; // and set the display processor using it @@ -204,7 +217,6 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra return { ...frame, fields, - name, }; }); } diff --git a/packages/grafana-data/src/field/fieldState.test.ts b/packages/grafana-data/src/field/fieldState.test.ts new file mode 100644 index 00000000000..17f0164d3fa --- /dev/null +++ b/packages/grafana-data/src/field/fieldState.test.ts @@ -0,0 +1,136 @@ +import { DataFrame, TIME_SERIES_FIELD_NAME, FieldType } from '../types'; +import { getFieldTitle } from './fieldState'; +import { toDataFrame } from '../dataframe'; + +interface TitleScenario { + frames: DataFrame[]; + frameIndex?: number; // assume 0 + fieldIndex?: number; // assume 0 +} + +function checkScenario(scenario: TitleScenario): string { + const frame = scenario.frames[scenario.frameIndex ?? 0]; + const field = frame.fields[scenario.fieldIndex ?? 0]; + return getFieldTitle(field, frame, scenario.frames); +} + +describe('Check field state calculations (title and id)', () => { + it('should use field name if no frame name', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + fields: [{ name: 'Field 1' }], + }), + ], + }); + expect(title).toEqual('Field 1'); + }); + + it('should use only field name if only one series', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + name: 'Series A', + fields: [{ name: 'Field 1' }], + }), + ], + }); + expect(title).toEqual('Field 1'); + }); + + it('should use frame name and field name if more than one frame', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + name: 'Series A', + fields: [{ name: 'Field 1' }], + }), + toDataFrame({ + name: 'Series B', + fields: [{ name: 'Field 1' }], + }), + ], + }); + expect(title).toEqual('Series A Field 1'); + }); + + it('should only use label value if only one label', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + fields: [{ name: 'Value', labels: { server: 'Server A' } }], + }), + ], + }); + expect(title).toEqual('Server A'); + }); + + it('should use label value only if all series have same name', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + name: 'cpu', + fields: [{ name: 'Value', labels: { server: 'Server A' } }], + }), + toDataFrame({ + name: 'cpu', + fields: [{ name: 'Value', labels: { server: 'Server A' } }], + }), + ], + }); + expect(title).toEqual('Server A'); + }); + + it('should use label name and value if more than one label', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + fields: [{ name: 'Value', labels: { server: 'Server A', mode: 'B' } }], + }), + ], + }); + expect(title).toEqual('{mode="B", server="Server A"}'); + }); + + it('should use field name even when it is TIME_SERIES_FIELD_NAME if there are no labels', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + fields: [{ name: TIME_SERIES_FIELD_NAME, labels: {} }], + }), + ], + }); + expect(title).toEqual('Value'); + }); + + it('should use series name when field name is TIME_SERIES_FIELD_NAME and there are no labels ', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + name: 'Series A', + fields: [{ name: TIME_SERIES_FIELD_NAME, labels: {} }], + }), + ], + }); + expect(title).toEqual('Series A'); + }); + + it('should reder loki frames', () => { + const title = checkScenario({ + frames: [ + toDataFrame({ + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time }, + { + name: 'line', + labels: { host: 'ec2-13-53-116-156.eu-north-1.compute.amazonaws.com', region: 'eu-north1' }, + }, + ], + }), + ], + fieldIndex: 1, + }); + expect(title).toEqual('line {host="ec2-13-53-116-156.eu-north-1.compute.amazonaws.com", region="eu-north1"}'); + }); +}); diff --git a/packages/grafana-data/src/field/fieldState.ts b/packages/grafana-data/src/field/fieldState.ts new file mode 100644 index 00000000000..64db3ded13e --- /dev/null +++ b/packages/grafana-data/src/field/fieldState.ts @@ -0,0 +1,153 @@ +import { DataFrame, Field, TIME_SERIES_FIELD_NAME, FieldType } from '../types'; +import { formatLabels } from '../utils/labels'; + +/** + * Get an appropriate display title + */ +export function getFrameDisplayTitle(frame: DataFrame, index?: number) { + if (frame.name) { + return frame.name; + } + + // Single field with tags + const valuesWithLabels = frame.fields.filter(f => f.labels !== undefined); + if (valuesWithLabels.length === 1) { + return formatLabels(valuesWithLabels[0].labels!); + } + + // list all the + if (index === undefined) { + return frame.fields + .filter(f => f.type !== FieldType.time) + .map(f => getFieldTitle(f, frame)) + .join(', '); + } + + if (frame.refId) { + return `Series (${frame.refId})`; + } + + return `Series (${index})`; +} + +export function getFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string { + const existingTitle = field.state?.title; + + if (existingTitle) { + return existingTitle; + } + + const title = calculateFieldTitle(field, frame, allFrames); + field.state = { + ...field.state, + title, + }; + + return title; +} + +/** + * Get an appropriate display title. If the 'title' is set, use that + */ +function calculateFieldTitle(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string { + const hasConfigTitle = field.config?.title && field.config?.title.length; + + let title = hasConfigTitle ? field.config!.title! : field.name; + + if (hasConfigTitle) { + return title; + } + + // This is an ugly exception for time field + // For time series we should normally treat time field with same name + // But in case it has a join source we should handle it as normal field + if (field.type === FieldType.time && !field.labels) { + return title ?? 'Time'; + } + + let parts: string[] = []; + let frameNamesDiffer = false; + + if (allFrames && allFrames.length > 1) { + for (let i = 1; i < allFrames.length; i++) { + const frame = allFrames[i]; + if (frame.name !== allFrames[i - 1].name) { + frameNamesDiffer = true; + break; + } + } + } + + let frameNameAdded = false; + let labelsAdded = false; + + if (frameNamesDiffer && frame?.name) { + parts.push(frame.name); + frameNameAdded = true; + } + + if (field.name && field.name !== TIME_SERIES_FIELD_NAME) { + parts.push(field.name); + } + + if (field.labels && frame) { + let singleLabelName = getSingleLabelName(allFrames ?? [frame]); + + if (!singleLabelName) { + let allLabels = formatLabels(field.labels); + if (allLabels) { + parts.push(allLabels); + labelsAdded = true; + } + } else if (field.labels[singleLabelName]) { + parts.push(field.labels[singleLabelName]); + labelsAdded = true; + } + } + + // if we have not added frame name and no labels, and field name = Value, we should add frame name + if (frame && !frameNameAdded && !labelsAdded && field.name === TIME_SERIES_FIELD_NAME) { + if (frame.name && frame.name.length > 0) { + parts.push(frame.name); + frameNameAdded = true; + } + } + + if (parts.length) { + title = parts.join(' '); + } else if (field.name) { + title = field.name; + } else { + title = TIME_SERIES_FIELD_NAME; + } + + return title; +} + +/** + * Checks all data frames and return name of label if there is only one label name in all frames + */ +function getSingleLabelName(frames: DataFrame[]): string | null { + let singleName: string | null = null; + + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]; + + for (const field of frame.fields) { + if (!field.labels) { + continue; + } + + // yes this should be in! + for (const labelKey in field.labels) { + if (singleName === null) { + singleName = labelKey; + } else if (labelKey !== singleName) { + return null; + } + } + } + } + + return singleName; +} diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 4a92bd90bd7..ad09a744786 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -7,3 +7,4 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; +export { getFieldTitle, getFrameDisplayTitle } from './fieldState'; diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index 17619cc19d5..8a3340a99d6 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -73,7 +73,7 @@ export const stringOverrideProcessor = ( return value; } if (settings && settings.expandTemplateVars && context.replaceVariables) { - return context.replaceVariables(value, context.field!.config.scopedVars); + return context.replaceVariables(value, context.field!.state!.scopedVars); } return `${value}`; }; diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index 0f921f47757..617a5e4e418 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -66,7 +66,7 @@ describe('Stats Calculators', () => { }); it('should support a single stat also', () => { - basicTable.fields[0].calcs = undefined; // clear the cache + basicTable.fields[0].state = undefined; // clear the cache const stats = reduceField({ field: basicTable.fields[0], reducers: ['first'], diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index e15a5f0a329..a0a5571d861 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -1,7 +1,7 @@ // Libraries import isNumber from 'lodash/isNumber'; -import { NullValueMode, Field } from '../types/index'; +import { NullValueMode, Field, FieldState, FieldCalcs } from '../types/index'; import { Registry, RegistryItem } from '../utils/Registry'; export enum ReducerID { @@ -28,10 +28,6 @@ export enum ReducerID { allIsNull = 'allIsNull', } -export interface FieldCalcs { - [key: string]: any; -} - // Internal function type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs; @@ -57,20 +53,23 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs { return {}; } - if (field.calcs) { + if (field.state?.calcs) { // Find the values we need to calculate const missing: string[] = []; for (const s of reducers) { - if (!field.calcs.hasOwnProperty(s)) { + if (!field.state.calcs.hasOwnProperty(s)) { missing.push(s); } } if (missing.length < 1) { return { - ...field.calcs, + ...field.state.calcs, }; } } + if (!field.state) { + field.state = {} as FieldState; + } const queue = fieldReducers.list(reducers); @@ -78,11 +77,11 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs { // This lets the concrete implementations assume at least one row const data = field.values; if (data.length < 1) { - const calcs = { ...field.calcs } as FieldCalcs; + const calcs = { ...field.state.calcs } as FieldCalcs; for (const reducer of queue) { calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null; } - return (field.calcs = calcs); + return (field.state.calcs = calcs); } const { nullValueMode } = field.config; @@ -92,8 +91,8 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs { // Avoid calculating all the standard stats if possible if (queue.length === 1 && queue[0].reduce) { const values = queue[0].reduce(field, ignoreNulls, nullAsZero); - field.calcs = { - ...field.calcs, + field.state.calcs = { + ...field.state.calcs, ...values, }; return values; @@ -111,11 +110,10 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs { } } - field.calcs = { - ...field.calcs, + field.state.calcs = { + ...field.state.calcs, ...values, }; - return values; } diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts index 1b9c0e377ae..30562e65b9d 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -2,6 +2,7 @@ import { Field, DataFrame } from '../../types/dataFrame'; import { FieldMatcherID, FrameMatcherID } from './ids'; import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations'; import { stringToJsRegex } from '../../text/string'; +import { getFieldTitle } from '../../field/fieldState'; // General Field matcher const fieldNameMacher: FieldMatcherInfo = { @@ -18,7 +19,7 @@ const fieldNameMacher: FieldMatcherInfo = { console.error(e); } return (field: Field) => { - return regex.test(field.name); + return regex.test(getFieldTitle(field) ?? ''); }; }, diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 73c0d475adf..004b2e57316 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -43,18 +43,18 @@ describe('calculateField transformer w/ timeseries', () => { expect(rows).toMatchInlineSnapshot(` Array [ Object { - "A {0}": 1, - "B {1}": 2, - "C {1}": 3, - "D {1}": "first", + "A": 1, + "B": 2, + "C": 3, + "D": "first", "The Total": 6, "TheTime": 1000, }, Object { - "A {0}": 100, - "B {1}": 200, - "C {1}": 300, - "D {1}": "second", + "A": 100, + "B": 200, + "C": 300, + "D": "second", "The Total": 600, "TheTime": 2000, }, diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 84da616f0d2..b3b299138ae 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -7,7 +7,7 @@ import { RowVector } from '../../vector/RowVector'; import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector'; import { doStandardCalcs } from '../fieldReducer'; import { seriesToColumnsTransformer } from './seriesToColumns'; -import { getTimeField } from '../../dataframe'; +import { getTimeField } from '../../dataframe/processDataFrame'; import defaults from 'lodash/defaults'; import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators'; @@ -168,7 +168,6 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator { for (let i = 0; i < frame.length; i++) { iter.rowIndex = i; - row.calcs = undefined; // bust the cache (just in case) const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer]; vals.push(val); } diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts index 7f6dc93c237..264997c79d6 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts @@ -43,38 +43,6 @@ describe('Labels as Columns', () => { expect(result[0].fields).toEqual(expected); }); - it('data frames where frame name is same as value field name replace field name with name Value', () => { - const cfg: DataTransformerConfig = { - id: DataTransformerID.labelsToFields, - options: {}, - }; - - const oneValueOneLabelA = toDataFrame({ - name: 'A', - fields: [ - { name: 'time', type: FieldType.time, values: [1000] }, - { name: 'A', type: FieldType.number, values: [1], labels: { location: 'inside' } }, - ], - }); - - const oneValueOneLabelB = toDataFrame({ - name: 'B', - fields: [ - { name: 'time', type: FieldType.time, values: [2000] }, - { name: 'B', type: FieldType.number, values: [-1], labels: { location: 'outside' } }, - ], - }); - - const result = transformDataFrame([cfg], [oneValueOneLabelA, oneValueOneLabelB]); - const expected: Field[] = [ - { name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} }, - { name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} }, - { name: 'Value', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} }, - ]; - - expect(result[0].fields).toEqual(expected); - }); - it('data frame with 2 values and 1 label', () => { const cfg: DataTransformerConfig = { id: DataTransformerID.labelsToFields, diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts index b96df90a623..60e866ea3a0 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts @@ -73,21 +73,12 @@ function getFramesWithOnlyValueFields(data: DataFrame[]): DataFrame[] { for (let i = 0; i < series.fields.length; i++) { const field = series.fields[i]; + if (field.type !== FieldType.number) { continue; } - // When we transform a time series to DataFrame we put series name in field name. - // This casues problems for this transformer that want all time series values in a Value column - // So here we change field names that have same name as DataFrame to just Value - if (field.name === series.name) { - fields.push({ - ...field, - name: 'Value', - }); - } else { - fields.push(field); - } + fields.push(field); } if (!fields.length) { diff --git a/packages/grafana-data/src/transformations/transformers/organize.test.ts b/packages/grafana-data/src/transformations/transformers/organize.test.ts index 215ffd58ae0..f1d6c9278db 100644 --- a/packages/grafana-data/src/transformations/transformers/organize.test.ts +++ b/packages/grafana-data/src/transformations/transformers/organize.test.ts @@ -47,13 +47,23 @@ describe('OrganizeFields Transformer', () => { expect(organized.fields).toEqual([ { config: {}, + labels: undefined, name: 'temperature', + state: { + title: 'temperature', + }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), }, { - config: {}, - name: 'renamed_humidity', + config: { + title: 'renamed_humidity', + }, + labels: undefined, + name: 'humidity', + state: { + title: 'renamed_humidity', + }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), }, @@ -93,14 +103,24 @@ describe('OrganizeFields Transformer', () => { expect(organized.fields).toEqual([ { - config: {}, - name: 'renamed_time', + labels: undefined, + config: { + title: 'renamed_time', + }, + name: 'time', + state: { + title: 'renamed_time', + }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), }, { config: {}, + labels: undefined, name: 'pressure', + state: { + title: 'pressure', + }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), }, diff --git a/packages/grafana-data/src/transformations/transformers/rename.test.ts b/packages/grafana-data/src/transformations/transformers/rename.test.ts index 16d181a8d04..d2c24bcea78 100644 --- a/packages/grafana-data/src/transformations/transformers/rename.test.ts +++ b/packages/grafana-data/src/transformations/transformers/rename.test.ts @@ -40,20 +40,38 @@ describe('Rename Transformer', () => { expect(renamed.fields).toEqual([ { - config: {}, - name: 'Total time', + config: { + title: 'Total time', + }, + labels: undefined, + name: 'time', + state: { + title: 'Total time', + }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), }, { - config: {}, - name: 'how cold is it?', + config: { + title: 'how cold is it?', + }, + labels: undefined, + name: 'temperature', + state: { + title: 'how cold is it?', + }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), }, { - config: {}, - name: 'Moistiness', + config: { + title: 'Moistiness', + }, + name: 'humidity', + labels: undefined, + state: { + title: 'Moistiness', + }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), }, @@ -87,20 +105,36 @@ describe('Rename Transformer', () => { expect(renamed.fields).toEqual([ { - config: {}, - name: 'ttl', + config: { + title: 'ttl', + }, + name: 'time', + labels: undefined, + state: { + title: 'ttl', + }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000]), }, { config: {}, + labels: undefined, name: 'pressure', + state: { + title: 'pressure', + }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), }, { - config: {}, - name: 'hum', + config: { + title: 'hum', + }, + labels: undefined, + name: 'humidity', + state: { + title: 'hum', + }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), }, diff --git a/packages/grafana-data/src/transformations/transformers/rename.ts b/packages/grafana-data/src/transformations/transformers/rename.ts index 9982112d4fc..c38bf71aab3 100644 --- a/packages/grafana-data/src/transformations/transformers/rename.ts +++ b/packages/grafana-data/src/transformations/transformers/rename.ts @@ -1,6 +1,7 @@ import { DataTransformerID } from './ids'; import { DataTransformerInfo } from '../../types/transformations'; -import { DataFrame, Field } from '../..'; +import { DataFrame, Field } from '../../types/dataFrame'; +import { getFieldTitle } from '../../field/fieldState'; export interface RenameFieldsTransformerOptions { renameByName: Record; @@ -28,19 +29,20 @@ export const renameFieldsTransformer: DataTransformerInfo ({ ...frame, - fields: renamer(frame.fields), + fields: renamer(frame), })); }; }, }; -const createRenamer = (renameByName: Record) => (fields: Field[]): Field[] => { +const createRenamer = (renameByName: Record) => (frame: DataFrame): Field[] => { if (!renameByName || Object.keys(renameByName).length === 0) { - return fields; + return frame.fields; } - return fields.map(field => { - const renameTo = renameByName[field.name]; + return frame.fields.map(field => { + const title = getFieldTitle(field, frame); + const renameTo = renameByName[title]; if (typeof renameTo !== 'string' || renameTo.length === 0) { return field; @@ -48,7 +50,14 @@ const createRenamer = (renameByName: Record) => (fields: Field[] return { ...field, - name: renameTo, + config: { + ...field.config, + title: renameTo, + }, + state: { + ...field.state, + title: renameTo, + }, }; }); }; diff --git a/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts b/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts index a7d2580f2a3..bed64112514 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToColumns.test.ts @@ -14,6 +14,7 @@ describe('SeriesToColumns Transformer', () => { beforeAll(() => { mockTransformationsRegistry([seriesToColumnsTransformer]); }); + const everySecondSeries = toDataFrame({ name: 'even', fields: [ @@ -44,38 +45,53 @@ describe('SeriesToColumns Transformer', () => { expect(filtered.fields).toEqual([ { name: 'time', + state: { + title: 'time', + }, type: FieldType.time, values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]), config: {}, - labels: { origin: 'even,odd' }, + labels: undefined, }, { - name: 'temperature {even}', + name: 'temperature', + state: { + title: 'temperature even', + }, type: FieldType.number, values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'humidity {even}', + name: 'humidity', + state: { + title: 'humidity even', + }, type: FieldType.number, values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'temperature {odd}', + name: 'temperature', + state: { + title: 'temperature odd', + }, type: FieldType.number, values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, { - name: 'humidity {odd}', + name: 'humidity', + state: { + title: 'humidity odd', + }, type: FieldType.number, values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, ]); }); @@ -92,38 +108,53 @@ describe('SeriesToColumns Transformer', () => { expect(filtered.fields).toEqual([ { name: 'temperature', + state: { + title: 'temperature', + }, type: FieldType.number, values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]), config: {}, - labels: { origin: 'even,odd' }, + labels: undefined, }, { - name: 'time {even}', + name: 'time', + state: { + title: 'time even', + }, type: FieldType.time, values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'humidity {even}', + name: 'humidity', + state: { + title: 'humidity even', + }, type: FieldType.number, values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'time {odd}', + name: 'time', + state: { + title: 'time odd', + }, type: FieldType.time, values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, { - name: 'humidity {odd}', + name: 'humidity', + state: { + title: 'humidity odd', + }, type: FieldType.number, values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, ]); }); @@ -144,38 +175,53 @@ describe('SeriesToColumns Transformer', () => { expect(filtered.fields).toEqual([ { name: 'time', + state: { + title: 'time', + }, type: FieldType.time, values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]), config: {}, - labels: { origin: 'even,odd' }, + labels: undefined, }, { - name: 'temperature {even}', + name: 'temperature', + state: { + title: 'temperature even', + }, type: FieldType.number, values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'humidity {even}', + name: 'humidity', + state: { + title: 'humidity even', + }, type: FieldType.number, values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]), config: {}, - labels: { origin: 'even' }, + labels: { name: 'even' }, }, { - name: 'temperature {odd}', + name: 'temperature', + state: { + title: 'temperature odd', + }, type: FieldType.number, values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, { - name: 'humidity {odd}', + name: 'humidity', + state: { + title: 'humidity odd', + }, type: FieldType.number, values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]), config: {}, - labels: { origin: 'odd' }, + labels: { name: 'odd' }, }, ]); }); @@ -209,24 +255,33 @@ describe('SeriesToColumns Transformer', () => { const expected: Field[] = [ { name: 'time', + state: { + title: 'time', + }, type: FieldType.time, values: new ArrayVector([1000, 2000, 3000, 4000]), config: {}, - labels: { origin: 'temperature,B' }, + labels: undefined, }, { name: 'temperature', type: FieldType.number, values: new ArrayVector([1, 3, 5, 7]), config: {}, - labels: { origin: 'temperature' }, + state: { + title: 'temperature temperature', + }, + labels: { name: 'temperature' }, }, { - name: 'temperature {B}', + name: 'temperature', + state: { + title: 'temperature B', + }, type: FieldType.number, values: new ArrayVector([2, 4, 6, 8]), config: {}, - labels: { origin: 'B' }, + labels: { name: 'B' }, }, ]; diff --git a/packages/grafana-data/src/transformations/transformers/seriesToColumns.ts b/packages/grafana-data/src/transformations/transformers/seriesToColumns.ts index ca4b8c09748..7bd85984b05 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToColumns.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToColumns.ts @@ -1,70 +1,81 @@ -import { DataFrame, DataTransformerInfo } from '../../types'; +import { DataFrame, DataTransformerInfo, Field } from '../../types'; import { DataTransformerID } from './ids'; import { MutableDataFrame } from '../../dataframe'; -import { filterFieldsByNameTransformer } from './filterByName'; import { ArrayVector } from '../../vector'; +import { getFieldTitle } from '../../field/fieldState'; export interface SeriesToColumnsOptions { byField?: string; } +const DEFAULT_KEY_FIELD = 'Time'; + export const seriesToColumnsTransformer: DataTransformerInfo = { id: DataTransformerID.seriesToColumns, name: 'Series as columns', description: 'Groups series by field and returns values as columns', defaultOptions: { - byField: 'Time', + byField: DEFAULT_KEY_FIELD, }, transformer: options => (data: DataFrame[]) => { - const optionsArray = options.byField ? [options.byField] : []; - // not sure if I should use filterFieldsByNameTransformer to get the key field - const keyDataFrames = filterFieldsByNameTransformer.transformer({ - include: optionsArray, - })(data); - if (!keyDataFrames.length) { - // for now we only parse data frames with 2 fields - return data; - } + const keyFieldMatch = options.byField || DEFAULT_KEY_FIELD; + const allFields: FieldsToProcess[] = []; - // not sure if I should use filterFieldsByNameTransformer to get the other fields - const otherDataFrames = filterFieldsByNameTransformer.transformer({ - exclude: optionsArray, - })(data); - if (!otherDataFrames.length) { - // for now we only parse data frames with 2 fields - return data; - } + for (let frameIndex = 0; frameIndex < data.length; frameIndex++) { + const frame = data[frameIndex]; + const keyField = findKeyField(frame, keyFieldMatch); - const processed = new MutableDataFrame(); - const origins: string[] = []; - for (let frameIndex = 0; frameIndex < keyDataFrames.length; frameIndex++) { - const frame = keyDataFrames[frameIndex]; - const origin = getOrigin(frame, frameIndex); - origins.push(origin); - } + if (!keyField) { + return data; + } - processed.addField({ - ...keyDataFrames[0].fields[0], - values: new ArrayVector([]), - labels: { origin: origins.join(',') }, - }); - - for (let frameIndex = 0; frameIndex < otherDataFrames.length; frameIndex++) { - const frame = otherDataFrames[frameIndex]; for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) { - const field = frame.fields[fieldIndex]; - const origin = getOrigin(frame, frameIndex); - const name = getColumnName(otherDataFrames, frameIndex, fieldIndex, false); - if (processed.fields.find(field => field.name === name)) { + const sourceField = frame.fields[fieldIndex]; + + if (sourceField === keyField) { continue; } - processed.addField({ ...field, name, values: new ArrayVector([]), labels: { origin } }); + + let labels = sourceField.labels ?? {}; + + if (frame.name) { + labels = { ...labels, name: frame.name }; + } + + allFields.push({ + keyField, + sourceField, + newField: { + ...sourceField, + state: null, + values: new ArrayVector([]), + labels, + }, + }); } } + // if no key fields or more than one value field + if (allFields.length <= 1) { + return data; + } + + const resultFrame = new MutableDataFrame(); + + resultFrame.addField({ + ...allFields[0].keyField, + values: new ArrayVector([]), + }); + + for (const item of allFields) { + resultFrame.addField(item.newField); + } + + const keyFieldTitle = getFieldTitle(resultFrame.fields[0], resultFrame); const byKeyField: { [key: string]: { [key: string]: any } } = {}; - // this loop creates a dictionary object that groups the key fields values - /* + + /* + this loop creates a dictionary object that groups the key fields values { "key field first value as string" : { "key field name": key field first value, @@ -77,26 +88,20 @@ export const seriesToColumnsTransformer: DataTransformerInfo { - const frame = frames[frameIndex]; - const field = frame.fields[fieldIndex]; - const frameName = frame.name || `${frameIndex}`; - const fieldName = field.name; - const seriesName = isKeyField ? fieldName : fieldName === frameName ? fieldName : `${fieldName} {${frameName}}`; +function findKeyField(frame: DataFrame, matchTitle: string): Field | null { + for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) { + const field = frame.fields[fieldIndex]; - return seriesName; -}; + if (matchTitle === getFieldTitle(field)) { + return field; + } + } -const getOrigin = (frame: DataFrame, index: number) => { - return frame.name || `${index}`; -}; + return null; +} + +interface FieldsToProcess { + newField: Field; + sourceField: Field; + keyField: Field; +} diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index db16531cc03..619bd0f3c5c 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -111,6 +111,10 @@ export type TimeSeriesPoints = TimeSeriesValue[][]; export interface TimeSeries extends QueryResultBase { target: string; + /** + * If name is manually configured via an alias / legend pattern + */ + title?: string; datapoints: TimeSeriesPoints; unit?: string; tags?: Labels; diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 0b6e6371f3b..a6be4ad3dc8 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -4,7 +4,6 @@ import { QueryResultBase, Labels, NullValueMode } from './data'; import { DisplayProcessor, DisplayValue } from './displayValue'; import { DataLink, LinkModel } from './dataLink'; import { Vector } from './vector'; -import { FieldCalcs } from '../transformations/fieldReducer'; import { FieldColor } from './fieldColor'; import { ScopedVars } from './ScopedVars'; @@ -53,8 +52,6 @@ export interface FieldConfig { // Panel Specific Values custom?: TOptions; - - scopedVars?: ScopedVars; } export interface ValueLinkConfig { @@ -85,9 +82,9 @@ export interface Field> { labels?: Labels; /** - * Cache of reduced values + * Cached values with appropriate dispaly and id values */ - calcs?: FieldCalcs; + state?: FieldState | null; /** * Convert text to the field value @@ -105,6 +102,23 @@ export interface Field> { getLinks?: (config: ValueLinkConfig) => Array>; } +export interface FieldState { + /** + * An appropriate name for the field (does not include frame info) + */ + title?: string | null; + + /** + * Cache of reduced values + */ + calcs?: FieldCalcs; + + /** + * Appropriate values for templating + */ + scopedVars?: ScopedVars; +} + export interface DataFrame extends QueryResultBase { name?: string; fields: Field[]; // All fields of equal length @@ -131,3 +145,7 @@ export interface DataFrameDTO extends QueryResultBase { name?: string; fields: Array; } + +export interface FieldCalcs extends Record {} + +export const TIME_SERIES_FIELD_NAME = 'Value'; diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx index 1bed3d4c4a5..ee5c0f6e717 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types'; -import { FieldMatcherID, fieldMatchers } from '@grafana/data'; +import { FieldMatcherID, fieldMatchers, getFieldTitle } from '@grafana/data'; import { Select } from '../Select/Select'; export class FieldNameMatcherEditor extends React.PureComponent> { @@ -10,7 +10,7 @@ export class FieldNameMatcherEditor extends React.PureComponent = { id: FieldMatcherID.byName, component: FieldNameMatcherEditor, matcher: fieldMatchers.get(FieldMatcherID.byName), - name: 'Filter by name', + name: 'Filter by field', description: 'Set properties for fields matching the name', }; diff --git a/packages/grafana-ui/src/components/Table/utils.ts b/packages/grafana-ui/src/components/Table/utils.ts index 04248620a72..ab02b2641ac 100644 --- a/packages/grafana-ui/src/components/Table/utils.ts +++ b/packages/grafana-ui/src/components/Table/utils.ts @@ -1,5 +1,5 @@ import { TextAlignProperty } from 'csstype'; -import { DataFrame, Field, FieldType } from '@grafana/data'; +import { DataFrame, Field, FieldType, getFieldTitle } from '@grafana/data'; import { Column } from 'react-table'; import { DefaultCell } from './DefaultCell'; import { BarGaugeCell } from './BarGaugeCell'; @@ -48,11 +48,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid } const Cell = getCellComponent(fieldTableOptions.displayMode, field); - columns.push({ Cell, id: fieldIndex.toString(), - Header: field.config.title ?? field.name, + Header: getFieldTitle(field, data), accessor: (row: any, i: number) => { return field.values.get(i); }, diff --git a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx index cfce662033e..0868a304c71 100644 --- a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx @@ -12,6 +12,7 @@ import { BinaryOperationID, SelectableValue, binaryOperators, + getFieldTitle, } from '@grafana/data'; import { StatsPicker } from '../StatsPicker/StatsPicker'; import { Switch } from '../Forms/Legacy/Switch/Switch'; @@ -82,14 +83,18 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< const allNames: string[] = []; const byName: KeyValue = {}; + for (const frame of input) { for (const field of frame.fields) { if (field.type !== FieldType.number) { continue; } - if (!byName[field.name]) { - byName[field.name] = true; - allNames.push(field.name); + + const title = getFieldTitle(field, frame, input); + + if (!byName[title]) { + byName[title] = true; + allNames.push(title); } } } @@ -97,6 +102,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< if (configuredOptions.length) { const options: string[] = []; const selected: string[] = []; + for (const v of allNames) { if (configuredOptions.includes(v)) { selected.push(v); diff --git a/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx index e4c174221b4..cf7d2c51cb1 100644 --- a/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx @@ -6,6 +6,7 @@ import { standardTransformers, TransformerRegistyItem, TransformerUIProps, + getFieldTitle, } from '@grafana/data'; import { HorizontalGroup } from '../Layout/Layout'; import { Input } from '../Input/Input'; @@ -45,6 +46,12 @@ export class FilterByNameTransformerEditor extends React.PureComponent< this.initOptions(); } + componentDidUpdate(oldProps: FilterByNameTransformerEditorProps) { + if (this.props.input !== oldProps.input) { + this.initOptions(); + } + } + private initOptions() { const { input, options } = this.props; const configuredOptions = options.include ? options.include : []; @@ -54,10 +61,11 @@ export class FilterByNameTransformerEditor extends React.PureComponent< for (const frame of input) { for (const field of frame.fields) { - let v = byName[field.name]; + const id = getFieldTitle(field, frame, input); + let v = byName[id]; if (!v) { - v = byName[field.name] = { - name: field.name, + v = byName[id] = { + name: id, count: 0, }; allNames.push(v); @@ -147,7 +155,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent< return (
-
Field name
+
Identifier
1) { + return
Organize fields only works with a single frame. Consider applying a join transformation first.
; + } + return ( @@ -210,10 +216,11 @@ export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] => } return frame.fields.reduce((names, field) => { - names[field.name] = null; + const t = getFieldTitle(field, frame, input); + names[t] = true; return names; }, names); - }, {} as Record) + }, {} as Record) ); }; @@ -221,7 +228,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem { const choices = dataFrames.map((frame, index) => { return { value: index, - label: `${frame.name} (${index})`, + label: `${getFrameDisplayTitle(frame)} (${index})`, }; }); diff --git a/public/app/features/dashboard/state/runRequest.ts b/public/app/features/dashboard/state/runRequest.ts index 9a6810f705f..61070dea47b 100644 --- a/public/app/features/dashboard/state/runRequest.ts +++ b/public/app/features/dashboard/state/runRequest.ts @@ -169,9 +169,9 @@ export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataF for (const result of results) { const dataFrame = guessFieldTypes(toDataFrame(result)); - // clear out any cached calcs + // clear out the cached info for (const field of dataFrame.fields) { - field.calcs = null; + field.state = null; } dataFrames.push(dataFrame); diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 598ddeccd61..cbf34b42cd5 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -1,7 +1,7 @@ import '../datasource'; import { CloudWatchDatasource } from '../datasource'; import * as redux from 'app/store/store'; -import { DataSourceInstanceSettings, dateMath } from '@grafana/data'; +import { DataSourceInstanceSettings, dateMath, getFrameDisplayTitle } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariable } from 'app/features/templating/all'; import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types'; @@ -233,7 +233,7 @@ describe('CloudWatchDatasource', () => { it('should return series list', done => { ctx.ds.query(query).then((result: any) => { - expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); done(); }); @@ -249,7 +249,7 @@ describe('CloudWatchDatasource', () => { it('should be built correctly if theres one search expressions returned in meta for a given query row', done => { response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }]; ctx.ds.query(query).then((result: any) => { - expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}` @@ -264,7 +264,7 @@ describe('CloudWatchDatasource', () => { { Expression: `REMOVE_EMPTY(SEARCH('second expression'))` }, ]; ctx.ds.query(query).then((result: any) => { - expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}` @@ -276,7 +276,7 @@ describe('CloudWatchDatasource', () => { it('should be built correctly if the query is a metric stat query', done => { response.results['A'].meta.gmdMeta = [{ Period: '300' }]; ctx.ds.query(query).then((result: any) => { - expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console'); expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain( `region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}` @@ -517,7 +517,7 @@ describe('CloudWatchDatasource', () => { it('should return series list', done => { ctx.ds.query(query).then((result: any) => { - expect(result.data[0].name).toBe(response.results.A.series[0].name); + expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]); done(); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts index 0d4907b94bc..577f0c1edd2 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts @@ -929,9 +929,10 @@ describe('ElasticResponse', () => { const hist: KeyValue = {}; const histogramResults = new MutableDataFrame(result.data[1]); rows = new DataFrameView(histogramResults); + for (let i = 0; i < rows.length; i++) { const row = rows.get(i); - hist[row.Time] = row.Count; + hist[row.Time] = row.Value; } response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts index 2127f8678a7..2503876f420 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts @@ -1,5 +1,5 @@ import Datasource from '../datasource'; -import { DataFrame, toUtc } from '@grafana/data'; +import { DataFrame, toUtc, getFrameDisplayTitle } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ @@ -175,7 +175,7 @@ describe('AppInsightsDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); const data = results.data[0] as DataFrame; - expect(data.name).toEqual('PrimaryResult'); + expect(getFrameDisplayTitle(data)).toEqual('PrimaryResult'); expect(data.fields[0].values.length).toEqual(1); expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[1].values.get(0)).toEqual(2.2075); @@ -218,7 +218,7 @@ describe('AppInsightsDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); const data = results.data[0] as DataFrame; - expect(data.name).toEqual('paritionA'); + expect(getFrameDisplayTitle(data)).toEqual('paritionA'); expect(data.fields[0].values.length).toEqual(1); expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[1].values.get(0)).toEqual(2.2075); @@ -279,7 +279,7 @@ describe('AppInsightsDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); const data = results.data[0] as DataFrame; - expect(data.name).toEqual('exceptions/server'); + expect(getFrameDisplayTitle(data)).toEqual('exceptions/server'); expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[1].values.get(0)).toEqual(2.2075); }); @@ -322,7 +322,7 @@ describe('AppInsightsDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); const data = results.data[0] as DataFrame; - expect(data.name).toEqual('exceptions/server'); + expect(getFrameDisplayTitle(data)).toEqual('exceptions/server'); expect(data.fields[0].values.length).toEqual(2); expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[1].values.get(0)).toEqual(3); @@ -376,14 +376,14 @@ describe('AppInsightsDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(2); let data = results.data[0] as DataFrame; - expect(data.name).toEqual('exceptions/server{client/city="Miami"}'); + expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="Miami"}'); expect(data.fields[1].values.length).toEqual(2); expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[1].values.get(0)).toEqual(10); expect(data.fields[0].values.get(1)).toEqual(1504112400000); expect(data.fields[1].values.get(1)).toEqual(20); data = results.data[1] as DataFrame; - expect(data.name).toEqual('exceptions/server{client/city="San Antonio"}'); + expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="San Antonio"}'); expect(data.fields[1].values.length).toEqual(2); expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[1].values.get(0)).toEqual(1); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 1029782b9c3..f45bb04a25a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource'; import FakeSchemaData from './__mocks__/schema'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { KustoSchema, AzureLogsVariable } from '../types'; -import { toUtc } from '@grafana/data'; +import { toUtc, getFrameDisplayTitle } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ jest.mock('@grafana/runtime', () => ({ @@ -183,10 +183,11 @@ describe('AzureLogAnalyticsDatasource', () => { it('should return a list of datapoints', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); - expect(results.data[0].name).toEqual('grafana-vm'); + expect(getFrameDisplayTitle(results.data[0])).toEqual('grafana-vm'); expect(results.data[0].fields.length).toBe(2); + expect(results.data[0].name).toBe('grafana-vm'); expect(results.data[0].fields[0].name).toBe('Time'); - expect(results.data[0].fields[1].name).toBe('grafana-vm'); + expect(results.data[0].fields[1].name).toBe('Value'); expect(results.data[0].fields[0].values.toArray().length).toBe(6); expect(results.data[0].fields[0].values.get(0)).toEqual(1587633300000); expect(results.data[0].fields[1].values.get(0)).toEqual(2017.25); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 5c9cdb03b21..c2a65721fc0 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -1,7 +1,7 @@ import AzureMonitorDatasource from '../datasource'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { toUtc, DataFrame } from '@grafana/data'; +import { toUtc, DataFrame, getFrameDisplayTitle } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ jest.mock('@grafana/runtime', () => ({ @@ -137,7 +137,7 @@ describe('AzureMonitorDatasource', () => { return ctx.ds.query(options).then((results: any) => { expect(results.data.length).toBe(1); const data = results.data[0] as DataFrame; - expect(data.name).toEqual('Percentage CPU'); + expect(getFrameDisplayTitle(data)).toEqual('Percentage CPU'); expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[1].values.get(0)).toEqual(2.2075); expect(data.fields[0].values.get(1)).toEqual(1558278720000); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index bbee54bcac3..988a60e6e7b 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -123,6 +123,9 @@ export class GraphiteDatasource extends DataSourceApi ({ @@ -91,8 +91,8 @@ describe('graphiteDatasource', () => { }); expect(result.data.length).toBe(2); - expect(result.data[0].name).toBe('seriesA'); - expect(result.data[1].name).toBe('seriesB'); + expect(getFrameDisplayTitle(result.data[0])).toBe('seriesA'); + expect(getFrameDisplayTitle(result.data[1])).toBe('seriesB'); expect(result.data[0].length).toBe(2); expect(result.data[0].meta.notices.length).toBe(1); expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function'); diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 03345e0fc03..d903bf9c777 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -13,6 +13,8 @@ import { DataSourceInstanceSettings, dateTime, LoadingState, + toDataFrame, + getFieldTitle, } from '@grafana/data'; import { PromOptions, PromQuery } from './types'; import templateSrv from 'app/features/templating/template_srv'; @@ -586,8 +588,9 @@ describe('PrometheusDatasource', () => { }); it('should return series list', async () => { + const frame = toDataFrame(results.data[0]); expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); + expect(getFieldTitle(frame.fields[1])).toBe('test{job="testjob"}'); }); }); @@ -730,8 +733,10 @@ describe('PrometheusDatasource', () => { }); it('should return series list', () => { + const frame = toDataFrame(results.data[0]); expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); + expect(frame.name).toBe('test{job="testjob"}'); + expect(getFieldTitle(frame.fields[1])).toBe('Value'); }); }); @@ -1634,8 +1639,9 @@ describe('PrometheusDatasource for POST', () => { }); it('should return series list', () => { + const frame = toDataFrame(results.data[0]); expect(results.data.length).toBe(1); - expect(results.data[0].target).toBe('test{job="testjob"}'); + expect(getFieldTitle(frame.fields[1])).toBe('test{job="testjob"}'); }); }); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index cc5b4f18133..57b77041209 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -162,6 +162,7 @@ describe('Prometheus Result Transformer', () => { expect(result).toEqual([ { target: '1', + title: '1', query: undefined, datapoints: [ [10, 1445000010000], @@ -172,6 +173,7 @@ describe('Prometheus Result Transformer', () => { }, { target: '2', + title: '2', query: undefined, datapoints: [ [10, 1445000010000], @@ -182,6 +184,7 @@ describe('Prometheus Result Transformer', () => { }, { target: '3', + title: '3', query: undefined, datapoints: [ [10, 1445000010000], @@ -285,6 +288,7 @@ describe('Prometheus Result Transformer', () => { expect(result).toEqual([ { target: 'test{job="testjob"}', + title: 'test{job="testjob"}', query: undefined, datapoints: [ [10, 0], @@ -324,6 +328,7 @@ describe('Prometheus Result Transformer', () => { expect(result).toEqual([ { target: 'test{job="testjob"}', + title: 'test{job="testjob"}', query: undefined, datapoints: [ [null, 0], @@ -335,6 +340,64 @@ describe('Prometheus Result Transformer', () => { ]); }); + it('should use __name__ label as series name', () => { + const response = { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { __name__: 'test', job: 'testjob' }, + values: [ + [1, '10'], + [2, '0'], + ], + }, + ], + }, + }; + + const options = { + format: 'timeseries', + step: 1, + start: 0, + end: 2, + }; + + const result = ctx.resultTransformer.transform({ data: response }, options); + expect(result[0].target).toEqual('test{job="testjob"}'); + }); + + it('should set frame name to undefined if no __name__ label but there are other labels', () => { + const response = { + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { job: 'testjob' }, + values: [ + [1, '10'], + [2, '0'], + ], + }, + ], + }, + }; + + const options = { + format: 'timeseries', + step: 1, + query: 'Some query', + start: 0, + end: 2, + }; + + const result = ctx.resultTransformer.transform({ data: response }, options); + expect(result[0].target).toBe('{job="testjob"}'); + expect(result[0].tags.job).toEqual('testjob'); + }); + it('should align null values with step', () => { const response = { status: 'success', @@ -356,13 +419,20 @@ describe('Prometheus Result Transformer', () => { step: 2, start: 0, end: 8, + refId: 'A', + meta: { custom: { hello: '1' } }, }; const result = ctx.resultTransformer.transform({ data: response }, options); expect(result).toEqual([ { target: 'test{job="testjob"}', + title: 'test{job="testjob"}', + meta: { + custom: { hello: '1' }, + }, query: undefined, + refId: 'A', datapoints: [ [null, 0], [null, 2000], diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index eaeb49808e0..aa675615c18 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; -import { TimeSeries, FieldType } from '@grafana/data'; +import { TimeSeries, FieldType, Labels, formatLabels } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; export class ResultTransformer { @@ -42,9 +42,7 @@ export class ResultTransformer { transformMetricData(metricData: any, options: any, start: number, end: number) { const dps = []; - let metricLabel = null; - - metricLabel = this.createMetricLabel(metricData.metric, options); + const { name, labels, title } = this.createLabelInfo(metricData.metric, options); const stepMs = parseFloat(options.step) * 1000; let baseTimestamp = start * 1000; @@ -76,9 +74,10 @@ export class ResultTransformer { datapoints: dps, query: options.query, refId: options.refId, + target: name, + tags: labels, + title, meta: options.meta, - target: metricLabel, - tags: metricData.metric, }; } @@ -142,23 +141,39 @@ export class ResultTransformer { transformInstantMetricData(md: any, options: any) { const dps = []; - let metricLabel = null; - metricLabel = this.createMetricLabel(md.metric, options); + const { name, labels } = this.createLabelInfo(md.metric, options); dps.push([parseFloat(md.value[1]), md.value[0] * 1000]); - return { target: metricLabel, datapoints: dps, tags: md.metric, refId: options.refId, meta: options.meta }; + return { target: name, datapoints: dps, tags: labels, refId: options.refId, meta: options.meta }; } - createMetricLabel(labelData: { [key: string]: string }, options: any) { - let label = ''; - if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { - label = this.getOriginalMetricName(labelData); - } else { - label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData); + createLabelInfo(labels: { [key: string]: string }, options: any): { name?: string; labels: Labels; title?: string } { + if (options?.legendFormat) { + const title = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labels); + return { name: title, title, labels }; } - if (!label || label === '{}') { - label = options.query; + + let { __name__, ...labelsWithoutName } = labels; + + let title = __name__ || ''; + + const labelPart = formatLabels(labelsWithoutName); + + if (!title && !labelPart) { + title = options.query; } - return label; + + title = `${__name__ ?? ''}${labelPart}`; + + return { name: title, title, labels: labelsWithoutName }; + } + + getOriginalMetricName(labelData: { [key: string]: string }) { + const metricName = labelData.__name__ || ''; + delete labelData.__name__; + const labelPart = Object.entries(labelData) + .map(label => `${label[0]}="${label[1]}"`) + .join(','); + return `${metricName}{${labelPart}}`; } renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) { @@ -171,15 +186,6 @@ export class ResultTransformer { }); } - getOriginalMetricName(labelData: { [key: string]: string }) { - const metricName = labelData.__name__ || ''; - delete labelData.__name__; - const labelPart = _.map(_.toPairs(labelData), label => { - return label[0] + '="' + label[1] + '"'; - }).join(','); - return metricName + '{' + labelPart + '}'; - } - transformToHistogramOverTime(seriesList: TimeSeries[]) { /* t1 = timestamp1, t2 = timestamp2 etc. t1 t2 t3 t1 t2 t3 diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 160a62398cd..ed4bbfe368f 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -8,6 +8,7 @@ import { DataFrame, getTimeField, dateTime, + getFieldTitle, } from '@grafana/data'; import TimeSeries from 'app/core/time_series2'; import config from 'app/core/config'; @@ -31,27 +32,24 @@ export class DataProcessor { for (let i = 0; i < dataList.length; i++) { const series = dataList[i]; const { timeField } = getTimeField(series); + if (!timeField) { continue; } - const seriesName = series.name ? series.name : series.refId; for (let j = 0; j < series.fields.length; j++) { const field = series.fields[j]; + if (field.type !== FieldType.number) { continue; } - - let name = field.config && field.config.title ? field.config.title : field.name; - - if (seriesName && dataList.length > 0 && name !== seriesName) { - name = seriesName + ' ' + name; - } - + const name = getFieldTitle(field, series, dataList); const datapoints = []; + for (let r = 0; r < series.length; r++) { datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]); } + list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range)); } } @@ -60,9 +58,11 @@ export class DataProcessor { if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) { const first = list[0]; first.alias = first.aliasEscaped = 'Count'; + for (let i = 1; i < list.length; i++) { first.datapoints = first.datapoints.concat(list[i].datapoints); } + return [first]; } diff --git a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts index a836fc73867..c4524e14276 100644 --- a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts +++ b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts @@ -17,6 +17,7 @@ import { FieldColor, FieldColorMode, FieldConfigSource, + getFieldTitle, } from '@grafana/data'; import { SeriesOptions, GraphOptions, GraphLegendEditorLegendOptions } from './types'; @@ -122,7 +123,7 @@ export const getGraphSeriesModel = ( }); graphs.push({ - label: field.name, + label: getFieldTitle(field, series, dataFrames), data: points, color: field.config.color?.fixedColor, info: statsDisplayValues, diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 7c547ddd825..a661a5862d0 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -23,6 +23,7 @@ import { PanelEvents, formattedValueToString, locationUtil, + getFieldTitle, } from '@grafana/data'; import { convertOldAngularValueMapping } from '@grafana/ui'; @@ -156,6 +157,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { onFramesReceived(frames: DataFrame[]) { const { panel } = this; + this.dataList = frames; if (frames && frames.length > 1) { this.data = { @@ -204,7 +206,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { processField(fieldInfo: FieldInfo) { const { panel, dashboard } = this; - const name = fieldInfo.field.config.title || fieldInfo.field.name; + const name = getFieldTitle(fieldInfo.field, fieldInfo.frame.frame, this.dataList as DataFrame[]); let calc = panel.valueName; let calcField = fieldInfo.field; let val: any = undefined; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 5d4ad33b7ef..eea40e99c09 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -1,5 +1,5 @@ import { SingleStatCtrl, ShowData } from '../module'; -import { dateTime, ReducerID } from '@grafana/data'; +import { dateTime, ReducerID, getFieldTitle } from '@grafana/data'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { LegacyResponseData } from '@grafana/data'; import { DashboardModel } from 'app/features/dashboard/state'; @@ -90,7 +90,8 @@ describe('SingleStatCtrl', () => { }); it('Should use series avg as default main value', () => { - expect(ctx.data.value).toBe('test.cpu1'); + const title = getFieldTitle(ctx.data.field); + expect(title).toBe('test.cpu1'); }); it('should set formatted value', () => { diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index ee7bd479e3a..67be8764ce6 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { Table, Select } from '@grafana/ui'; -import { FieldMatcherID, PanelProps, DataFrame, SelectableValue } from '@grafana/data'; +import { FieldMatcherID, PanelProps, DataFrame, SelectableValue, getFrameDisplayTitle } from '@grafana/data'; import { Options } from './types'; import { css } from 'emotion'; import { config } from 'app/core/config'; @@ -101,7 +101,7 @@ export class TablePanel extends Component { const currentIndex = this.getCurrentFrameIndex(); const names = data.series.map((frame, index) => { return { - label: `${frame.name ?? 'Series'}`, + label: getFrameDisplayTitle(frame), value: index, }; });