Field: getFieldTitle as field / series display identity and use it in all field name matchers & field / series name displays (#24024)

* common title handling

* show labels

* update comment

* Update changelog for v7.0.0-beta1 (#24007)

Co-Authored-By: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-Authored-By: Andrej Ocenas <mr.ocenas@gmail.com>
Co-Authored-By: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>

* verify-repo-update: Fix Dockerfile.deb (#24030)

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* CircleCI: Upgrade build pipeline tool (#24021)

* CircleCI: Upgrade build pipeline tool

* Devenv: ignore enterprise (#24037)

* Add header icon to Add data source page (#24033)

* latest.json: Update testing version (#24038)

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix login page redirected from password reset (#24032)

* Storybook: Rewrite stories to CSF (#23989)

* ColorPicker to CSF format

* Convert stories to CSF

* Do not export ClipboardButton

* Update ConfirmButton

* Remove unused imports

* Fix feedback

* changelog enterprise 7.0.0-beta1 (#24039)

* CircleCI: Bump grafana/build-container revision (#24043)

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Changelog: Updates changelog with more feature details (#24040)

* Changelog: Updates changelog with more feature details

* spell fix

* spell fix

* Updates

* Readme update

* Updates

* Select: fixes so component loses focus on selecting value or pressing outside of input. (#24008)

* changed the value container to a class component to get it to work with focus (maybe something with context?).

* added e2e tests to verify that the select focus is working as it should.

* fixed according to feedback.

* updated snapshot.

* Devenv: add remote renderer to grafana (#24050)

* NewPanelEditor: minor UI twekas (#24042)

* Forward ref for tabs, use html props

* Inspect:  add inspect label to drawer title

* Add tooltips to sidebar pane tabs, copy changes

* Remove unused import

* Place tooltips over tabs

* Inspector: dont show transformations select if there is only one data frame

* Review

* Changelog: Add a breaking change (#24051)

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* CircleCI: Unpin grafana/docs-base (#24054)

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Search: close overlay on Esc press (#24003)

* Search: Close on Esc

* Search: Increase bottom padding for the last item in section

* Search: Move closing search to keybindingsSrv

* Search: Fix folder view

* Search: Do not move folders if already in folder

* Docs: Adds deprecation notice to changelog and docs for scripted dashboards (#24060)

* Update CHANGELOG.md (#24047)

Fix typo

Co-authored-by: Daniel Lee <dan.limerick@gmail.com>

* Documentation: Alternative Team Sync Wording (#23960)

* Alternative wording for team sync docs

Signed-off-by: Joe Elliott <number101010@gmail.com>

* Update docs/sources/auth/team-sync.md

Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Fix misspell issues (#23905)

* Fix misspell issues

See,
$ golangci-lint run --timeout 10m --disable-all -E misspell ./...

Signed-off-by: Mario Trangoni <mjtrangoni@gmail.com>

* Fix codespell issues

See,
$ codespell -S './.git*' -L 'uint,thru,pres,unknwon,serie,referer,uptodate,durationm'

Signed-off-by: Mario Trangoni <mjtrangoni@gmail.com>

* ci please?

* non-empty commit - ci?

* Trigger build

Co-authored-by: bergquist <carl.bergquist@gmail.com>
Co-authored-by: Kyle Brandt <kyle@grafana.com>

* fix compile error

* better series display

* better display

* now with prometheus and loki

* a few more tests

* Improvements and tests

* thinking

* More advanced and smart default title generation

* Another fix

* Progress but dam this will be hard

* Reverting the time series Value field name change

* revert revert going in circles

* add a field state object

* Use state title when converting back to legacy format

* Improved the join (series to columsn) transformer

* Got tests running again

* Rewrite of seriesToColums that simplifies and fixing tests

* Fixed the tricky problem of multiple time field when not used in join

* Prometheus: Restoring prometheus formatting

* Graphite: Disable Grafana's series naming

* fixed imports

* Fixed tests and made rename transform change title instead

* Fixing more tests

* fix more tests

* fixed import issue

* Fixed more circular dependencies

* Renamed to getFieldTitle

* More rename

* Review feedback

* Fix for showing field title in calculate field transformer

* fieldOverride: Make it clear that state title after applying defaults & overrides

* Fixed ts issue

* Update packages/grafana-ui/src/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Leonard Gram <leo@xlson.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Richard Hartmann <RichiH@users.noreply.github.com>
Co-authored-by: Daniel Lee <dan.limerick@gmail.com>
Co-authored-by: Joe Elliott <joe.elliott@grafana.com>
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Mario Trangoni <mario@mariotrangoni.de>
Co-authored-by: bergquist <carl.bergquist@gmail.com>
Co-authored-by: Kyle Brandt <kyle@grafana.com>
This commit is contained in:
Ryan McKinley 2020-05-07 01:42:03 -07:00 committed by GitHub
parent 184941eab4
commit 5dca59f720
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 912 additions and 368 deletions

View File

@ -22,7 +22,8 @@ describe('toDataFrame', () => {
], ],
}; };
let series = toDataFrame(input1); 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 v0 = series.fields[0].values;
const v1 = series.fields[1].values; const v1 = series.fields[1].values;
@ -182,6 +183,24 @@ describe('SerisData backwards compatibility', () => {
expect(roundtrip.target).toBe(timeseries.target); 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', () => { it('can convert empty table to DataFrame then back to legacy', () => {
const table = { const table = {
columns: [], columns: [],

View File

@ -14,12 +14,14 @@ import {
TimeSeriesValue, TimeSeriesValue,
FieldDTO, FieldDTO,
DataFrameDTO, DataFrameDTO,
TIME_SERIES_FIELD_NAME,
} from '../types/index'; } from '../types/index';
import { isDateTime } from '../datetime/moment_wrapper'; import { isDateTime } from '../datetime/moment_wrapper';
import { ArrayVector } from '../vector/ArrayVector'; import { ArrayVector } from '../vector/ArrayVector';
import { MutableDataFrame } from './MutableDataFrame'; import { MutableDataFrame } from './MutableDataFrame';
import { SortedVector } from '../vector/SortedVector'; import { SortedVector } from '../vector/SortedVector';
import { ArrayDataFrame } from './ArrayDataFrame'; import { ArrayDataFrame } from './ArrayDataFrame';
import { getFieldTitle } from '../field/fieldState';
function convertTableToDataFrame(table: TableData): DataFrame { function convertTableToDataFrame(table: TableData): DataFrame {
const fields = table.columns.map(c => { const fields = table.columns.map(c => {
@ -61,6 +63,7 @@ function convertTableToDataFrame(table: TableData): DataFrame {
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
const times: number[] = []; const times: number[] = [];
const values: TimeSeriesValue[] = []; const values: TimeSeriesValue[] = [];
for (const point of timeSeries.datapoints) { for (const point of timeSeries.datapoints) {
values.push(point[0]); values.push(point[0]);
times.push(point[1] as number); times.push(point[1] as number);
@ -74,7 +77,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
values: new ArrayVector<number>(times), values: new ArrayVector<number>(times),
}, },
{ {
name: timeSeries.target || 'Value', name: TIME_SERIES_FIELD_NAME,
type: FieldType.number, type: FieldType.number,
config: { config: {
unit: timeSeries.unit, unit: timeSeries.unit,
@ -84,6 +87,10 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
}, },
]; ];
if (timeSeries.title) {
(fields[1].config as FieldConfig).title = timeSeries.title;
}
return { return {
name: timeSeries.target, name: timeSeries.target,
refId: timeSeries.refId, refId: timeSeries.refId,
@ -111,7 +118,7 @@ function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
name: graphSeries.label, name: graphSeries.label,
fields: [ fields: [
{ {
name: graphSeries.label || 'Value', name: graphSeries.label || TIME_SERIES_FIELD_NAME,
type: FieldType.number, type: FieldType.number,
config: {}, config: {},
values: x, values: x,
@ -312,18 +319,20 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
const { timeField, timeIndex } = getTimeField(frame); const { timeField, timeIndex } = getTimeField(frame);
if (timeField) { if (timeField) {
const valueIndex = timeIndex === 0 ? 1 : 0; const valueIndex = timeIndex === 0 ? 1 : 0;
const valueField = fields[valueIndex];
const timeField = fields[timeIndex!];
// Make sure it is [value,time] // Make sure it is [value,time]
for (let i = 0; i < rowCount; i++) { for (let i = 0; i < rowCount; i++) {
rows.push([ rows.push([
fields[valueIndex].values.get(i), // value valueField.values.get(i), // value
fields[timeIndex!].values.get(i), // time timeField.values.get(i), // time
]); ]);
} }
return { return {
alias: fields[valueIndex].name || frame.name, alias: frame.name,
target: fields[valueIndex].name || frame.name, target: getFieldTitle(valueField, frame),
datapoints: rows, datapoints: rows,
unit: fields[0].config ? fields[0].config.unit : undefined, unit: fields[0].config ? fields[0].config.unit : undefined,
refId: frame.refId, 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 * Wrapper to get an array from each field value
*/ */
@ -487,3 +484,15 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
name: data.name, 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 {};
};

View File

@ -37,35 +37,17 @@ export interface ReduceDataOptions {
// TODO: use built in variables, same as for data links? // TODO: use built in variables, same as for data links?
export const VAR_SERIES_NAME = '__series.name'; export const VAR_SERIES_NAME = '__series.name';
export const VAR_FIELD_NAME = '__field.name'; export const VAR_FIELD_NAME = '__field.name';
export const VAR_FIELD_LABELS = '__field.labels';
export const VAR_CALC = '__calc'; export const VAR_CALC = '__calc';
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
function getTitleTemplate(title: string | undefined, stats: string[], data?: DataFrame[]): string { function getTitleTemplate(stats: string[]): 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++;
}
}
const parts: string[] = []; const parts: string[] = [];
if (stats.length > 1) { if (stats.length > 1) {
parts.push('${' + VAR_CALC + '}'); 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(' '); return parts.join(' ');
} }
@ -108,8 +90,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
const data = options.data; const data = options.data;
let hitLimit = false; let hitLimit = false;
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
const defaultTitle = getTitleTemplate(fieldConfig.defaults.title, calcs, data);
const scopedVars: ScopedVars = {}; const scopedVars: ScopedVars = {};
const defaultTitle = getTitleTemplate(calcs);
for (let s = 0; s < data.length && !hitLimit; s++) { for (let s = 0; s < data.length && !hitLimit; s++) {
const series = data[s]; // Name is already set 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++) { for (let i = 0; i < series.fields.length && !hitLimit; i++) {
const field = series.fields[i]; const field = series.fields[i];
const fieldLinksSupplier = field.getLinks; const fieldLinksSupplier = field.getLinks;
// Show all number fields
// To filter out time field, need an option for this
if (field.type !== FieldType.number) { if (field.type !== FieldType.number) {
continue; continue;
} }
const config = field.config; // already set by the prepare task const config = field.config; // already set by the prepare task
const title = field.config.title ?? defaultTitle;
const display = const display =
field.display ?? field.display ??
@ -134,7 +119,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
timeZone, timeZone,
}); });
const title = config.title ? config.title : defaultTitle;
// Show all rows // Show all rows
if (reduceOptions.values) { if (reduceOptions.values) {
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0; 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)); const displayValue = display(field.values.get(j));
displayValue.title = replaceVariables(title, { displayValue.title = replaceVariables(title, {
...field.config.scopedVars, // series and field scoped vars ...field.state?.scopedVars, // series and field scoped vars
...scopedVars, ...scopedVars,
}); });
@ -197,9 +182,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
scopedVars[VAR_CALC] = { value: calc, text: calc }; scopedVars[VAR_CALC] = { value: calc, text: calc };
const displayValue = display(results[calc]); const displayValue = display(results[calc]);
displayValue.title = replaceVariables(title, { displayValue.title = replaceVariables(title, {
...field.config.scopedVars, // series and field scoped vars ...field.state?.scopedVars, // series and field scoped vars
...scopedVars, ...scopedVars,
}); });
values.push({ values.push({
name: calc, name: calc,
field: config, field: config,

View File

@ -19,6 +19,7 @@ import { Registry } from '../utils';
import { mockStandardProperties } from '../utils/tests/mockStandardProperties'; import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
import { FieldMatcherID } from '../transformations'; import { FieldMatcherID } from '../transformations';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getFieldTitle } from './fieldState';
const property1 = { const property1 = {
id: 'custom.property1', // Match field properties id: 'custom.property1', // Match field properties
@ -111,12 +112,14 @@ describe('applyFieldOverrides', () => {
fieldConfigRegistry: new FieldConfigOptionsRegistry(), fieldConfigRegistry: new FieldConfigOptionsRegistry(),
}); });
expect(withOverrides[0].fields[0].config.scopedVars).toMatchInlineSnapshot(` expect(withOverrides[0].fields[0].state!.scopedVars).toMatchInlineSnapshot(`
Object { Object {
"__field": Object { "__field": Object {
"text": "Field", "text": "Field",
"value": Object { "value": Object {
"name": "message", "label": undefined,
"labels": "",
"name": "A message",
}, },
}, },
"__series": Object { "__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 { Object {
"__field": Object { "__field": Object {
"text": "Field", "text": "Field",
"value": Object { "value": Object {
"name": "info", "label": undefined,
"labels": "",
"name": "B info",
}, },
}, },
"__series": Object { "__series": Object {
@ -152,16 +157,19 @@ describe('applyFieldOverrides', () => {
min: 0, min: 0,
max: 100, max: 100,
}; };
const f1 = { const f1 = {
unit: 'ms', unit: 'ms',
dateFormat: '', // should be ignored dateFormat: '', // should be ignored
max: parseFloat('NOPE'), // should be ignored max: parseFloat('NOPE'), // should be ignored
min: null, // should alo be ignored! min: null, // should alo be ignored!
title: 'newTitle',
}; };
const f: DataFrame = toDataFrame({ const f: DataFrame = toDataFrame({
fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }], fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }],
}); });
const processed = applyFieldOverrides({ const processed = applyFieldOverrides({
data: [f], data: [f],
fieldConfig: { fieldConfig: {
@ -172,11 +180,13 @@ describe('applyFieldOverrides', () => {
replaceVariables: v => v, replaceVariables: v => v,
theme: {} as GrafanaTheme, theme: {} as GrafanaTheme,
})[0]; })[0];
const out = processed.fields[0].config;
expect(out.min).toEqual(0); const outField = processed.fields[0];
expect(out.max).toEqual(100);
expect(out.unit).toEqual('ms'); 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', () => { it('will apply field overrides', () => {
@ -300,10 +310,8 @@ describe('setDynamicConfigValue', () => {
it('applies dynamic config values', () => { it('applies dynamic config values', () => {
const config = { const config = {
title: 'test', title: 'test',
// custom: {
// property1: 1,
// },
}; };
setDynamicConfigValue( setDynamicConfigValue(
config, config,
{ {

View File

@ -24,12 +24,15 @@ import set from 'lodash/set';
import unset from 'lodash/unset'; import unset from 'lodash/unset';
import get from 'lodash/get'; import get from 'lodash/get';
import { getDisplayProcessor } from './displayProcessor'; import { getDisplayProcessor } from './displayProcessor';
import { getTimeField, guessFieldTypeForField } from '../dataframe'; import { guessFieldTypeForField } from '../dataframe';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { DataLinkBuiltInVars, locationUtil } from '../utils'; import { DataLinkBuiltInVars, locationUtil } from '../utils';
import { formattedValueToString } from '../valueFormats'; import { formattedValueToString } from '../valueFormats';
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
import { formatLabels } from '../utils/labels';
import { getFrameDisplayTitle, getFieldTitle } from './fieldState';
import { getTimeField } from '../dataframe/processDataFrame';
interface OverrideProps { interface OverrideProps {
match: FieldMatcher; match: FieldMatcher;
@ -96,25 +99,31 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
} }
return options.data.map((frame, index) => { return options.data.map((frame, index) => {
let name = frame.name;
if (!name) {
name = `Series[${index}]`;
}
const scopedVars: ScopedVars = { 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 // Config is mutable within this scope
let fieldName = field.name;
if (!fieldName) {
fieldName = `Field[${fieldIndex}]`;
}
const fieldScopedVars = { ...scopedVars }; 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 = { const context = {
field, field,
data: options.data!, data: options.data!,
@ -183,6 +192,10 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
...field, ...field,
config, config,
type, type,
state: {
...field.state,
title: null,
},
}; };
// and set the display processor using it // and set the display processor using it
@ -204,7 +217,6 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
return { return {
...frame, ...frame,
fields, fields,
name,
}; };
}); });
} }

View File

@ -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"}');
});
});

View File

@ -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;
}

View File

@ -7,3 +7,4 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldTitle, getFrameDisplayTitle } from './fieldState';

View File

@ -73,7 +73,7 @@ export const stringOverrideProcessor = (
return value; return value;
} }
if (settings && settings.expandTemplateVars && context.replaceVariables) { if (settings && settings.expandTemplateVars && context.replaceVariables) {
return context.replaceVariables(value, context.field!.config.scopedVars); return context.replaceVariables(value, context.field!.state!.scopedVars);
} }
return `${value}`; return `${value}`;
}; };

View File

@ -66,7 +66,7 @@ describe('Stats Calculators', () => {
}); });
it('should support a single stat also', () => { 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({ const stats = reduceField({
field: basicTable.fields[0], field: basicTable.fields[0],
reducers: ['first'], reducers: ['first'],

View File

@ -1,7 +1,7 @@
// Libraries // Libraries
import isNumber from 'lodash/isNumber'; 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'; import { Registry, RegistryItem } from '../utils/Registry';
export enum ReducerID { export enum ReducerID {
@ -28,10 +28,6 @@ export enum ReducerID {
allIsNull = 'allIsNull', allIsNull = 'allIsNull',
} }
export interface FieldCalcs {
[key: string]: any;
}
// Internal function // Internal function
type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs; type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
@ -57,20 +53,23 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
return {}; return {};
} }
if (field.calcs) { if (field.state?.calcs) {
// Find the values we need to calculate // Find the values we need to calculate
const missing: string[] = []; const missing: string[] = [];
for (const s of reducers) { for (const s of reducers) {
if (!field.calcs.hasOwnProperty(s)) { if (!field.state.calcs.hasOwnProperty(s)) {
missing.push(s); missing.push(s);
} }
} }
if (missing.length < 1) { if (missing.length < 1) {
return { return {
...field.calcs, ...field.state.calcs,
}; };
} }
} }
if (!field.state) {
field.state = {} as FieldState;
}
const queue = fieldReducers.list(reducers); 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 // This lets the concrete implementations assume at least one row
const data = field.values; const data = field.values;
if (data.length < 1) { if (data.length < 1) {
const calcs = { ...field.calcs } as FieldCalcs; const calcs = { ...field.state.calcs } as FieldCalcs;
for (const reducer of queue) { for (const reducer of queue) {
calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null; calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
} }
return (field.calcs = calcs); return (field.state.calcs = calcs);
} }
const { nullValueMode } = field.config; const { nullValueMode } = field.config;
@ -92,8 +91,8 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
// Avoid calculating all the standard stats if possible // Avoid calculating all the standard stats if possible
if (queue.length === 1 && queue[0].reduce) { if (queue.length === 1 && queue[0].reduce) {
const values = queue[0].reduce(field, ignoreNulls, nullAsZero); const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
field.calcs = { field.state.calcs = {
...field.calcs, ...field.state.calcs,
...values, ...values,
}; };
return values; return values;
@ -111,11 +110,10 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
} }
} }
field.calcs = { field.state.calcs = {
...field.calcs, ...field.state.calcs,
...values, ...values,
}; };
return values; return values;
} }

View File

@ -2,6 +2,7 @@ import { Field, DataFrame } from '../../types/dataFrame';
import { FieldMatcherID, FrameMatcherID } from './ids'; import { FieldMatcherID, FrameMatcherID } from './ids';
import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations'; import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations';
import { stringToJsRegex } from '../../text/string'; import { stringToJsRegex } from '../../text/string';
import { getFieldTitle } from '../../field/fieldState';
// General Field matcher // General Field matcher
const fieldNameMacher: FieldMatcherInfo<string> = { const fieldNameMacher: FieldMatcherInfo<string> = {
@ -18,7 +19,7 @@ const fieldNameMacher: FieldMatcherInfo<string> = {
console.error(e); console.error(e);
} }
return (field: Field) => { return (field: Field) => {
return regex.test(field.name); return regex.test(getFieldTitle(field) ?? '');
}; };
}, },

View File

@ -43,18 +43,18 @@ describe('calculateField transformer w/ timeseries', () => {
expect(rows).toMatchInlineSnapshot(` expect(rows).toMatchInlineSnapshot(`
Array [ Array [
Object { Object {
"A {0}": 1, "A": 1,
"B {1}": 2, "B": 2,
"C {1}": 3, "C": 3,
"D {1}": "first", "D": "first",
"The Total": 6, "The Total": 6,
"TheTime": 1000, "TheTime": 1000,
}, },
Object { Object {
"A {0}": 100, "A": 100,
"B {1}": 200, "B": 200,
"C {1}": 300, "C": 300,
"D {1}": "second", "D": "second",
"The Total": 600, "The Total": 600,
"TheTime": 2000, "TheTime": 2000,
}, },

View File

@ -7,7 +7,7 @@ import { RowVector } from '../../vector/RowVector';
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector'; import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
import { doStandardCalcs } from '../fieldReducer'; import { doStandardCalcs } from '../fieldReducer';
import { seriesToColumnsTransformer } from './seriesToColumns'; import { seriesToColumnsTransformer } from './seriesToColumns';
import { getTimeField } from '../../dataframe'; import { getTimeField } from '../../dataframe/processDataFrame';
import defaults from 'lodash/defaults'; import defaults from 'lodash/defaults';
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators'; import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
@ -168,7 +168,6 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
for (let i = 0; i < frame.length; i++) { for (let i = 0; i < frame.length; i++) {
iter.rowIndex = i; iter.rowIndex = i;
row.calcs = undefined; // bust the cache (just in case)
const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer]; const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer];
vals.push(val); vals.push(val);
} }

View File

@ -43,38 +43,6 @@ describe('Labels as Columns', () => {
expect(result[0].fields).toEqual(expected); 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<LabelsToFieldsOptions> = {
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', () => { it('data frame with 2 values and 1 label', () => {
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = { const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
id: DataTransformerID.labelsToFields, id: DataTransformerID.labelsToFields,

View File

@ -73,22 +73,13 @@ function getFramesWithOnlyValueFields(data: DataFrame[]): DataFrame[] {
for (let i = 0; i < series.fields.length; i++) { for (let i = 0; i < series.fields.length; i++) {
const field = series.fields[i]; const field = series.fields[i];
if (field.type !== FieldType.number) { if (field.type !== FieldType.number) {
continue; 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) { if (!fields.length) {
continue; continue;

View File

@ -47,13 +47,23 @@ describe('OrganizeFields Transformer', () => {
expect(organized.fields).toEqual([ expect(organized.fields).toEqual([
{ {
config: {}, config: {},
labels: undefined,
name: 'temperature', name: 'temperature',
state: {
title: 'temperature',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
}, },
{ {
config: {}, config: {
name: 'renamed_humidity', title: 'renamed_humidity',
},
labels: undefined,
name: 'humidity',
state: {
title: 'renamed_humidity',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
}, },
@ -93,14 +103,24 @@ describe('OrganizeFields Transformer', () => {
expect(organized.fields).toEqual([ expect(organized.fields).toEqual([
{ {
config: {}, labels: undefined,
name: 'renamed_time', config: {
title: 'renamed_time',
},
name: 'time',
state: {
title: 'renamed_time',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]), values: new ArrayVector([3000, 4000, 5000, 6000]),
}, },
{ {
config: {}, config: {},
labels: undefined,
name: 'pressure', name: 'pressure',
state: {
title: 'pressure',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
}, },

View File

@ -40,20 +40,38 @@ describe('Rename Transformer', () => {
expect(renamed.fields).toEqual([ expect(renamed.fields).toEqual([
{ {
config: {}, config: {
name: 'Total time', title: 'Total time',
},
labels: undefined,
name: 'time',
state: {
title: 'Total time',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]), values: new ArrayVector([3000, 4000, 5000, 6000]),
}, },
{ {
config: {}, config: {
name: 'how cold is it?', title: 'how cold is it?',
},
labels: undefined,
name: 'temperature',
state: {
title: 'how cold is it?',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
}, },
{ {
config: {}, config: {
name: 'Moistiness', title: 'Moistiness',
},
name: 'humidity',
labels: undefined,
state: {
title: 'Moistiness',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
}, },
@ -87,20 +105,36 @@ describe('Rename Transformer', () => {
expect(renamed.fields).toEqual([ expect(renamed.fields).toEqual([
{ {
config: {}, config: {
name: 'ttl', title: 'ttl',
},
name: 'time',
labels: undefined,
state: {
title: 'ttl',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]), values: new ArrayVector([3000, 4000, 5000, 6000]),
}, },
{ {
config: {}, config: {},
labels: undefined,
name: 'pressure', name: 'pressure',
state: {
title: 'pressure',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]), values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
}, },
{ {
config: {}, config: {
name: 'hum', title: 'hum',
},
labels: undefined,
name: 'humidity',
state: {
title: 'hum',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]), values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
}, },

View File

@ -1,6 +1,7 @@
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations'; import { DataTransformerInfo } from '../../types/transformations';
import { DataFrame, Field } from '../..'; import { DataFrame, Field } from '../../types/dataFrame';
import { getFieldTitle } from '../../field/fieldState';
export interface RenameFieldsTransformerOptions { export interface RenameFieldsTransformerOptions {
renameByName: Record<string, string>; renameByName: Record<string, string>;
@ -28,19 +29,20 @@ export const renameFieldsTransformer: DataTransformerInfo<RenameFieldsTransforme
return data.map(frame => ({ return data.map(frame => ({
...frame, ...frame,
fields: renamer(frame.fields), fields: renamer(frame),
})); }));
}; };
}, },
}; };
const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]): Field[] => { const createRenamer = (renameByName: Record<string, string>) => (frame: DataFrame): Field[] => {
if (!renameByName || Object.keys(renameByName).length === 0) { if (!renameByName || Object.keys(renameByName).length === 0) {
return fields; return frame.fields;
} }
return fields.map(field => { return frame.fields.map(field => {
const renameTo = renameByName[field.name]; const title = getFieldTitle(field, frame);
const renameTo = renameByName[title];
if (typeof renameTo !== 'string' || renameTo.length === 0) { if (typeof renameTo !== 'string' || renameTo.length === 0) {
return field; return field;
@ -48,7 +50,14 @@ const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]
return { return {
...field, ...field,
name: renameTo, config: {
...field.config,
title: renameTo,
},
state: {
...field.state,
title: renameTo,
},
}; };
}); });
}; };

View File

@ -14,6 +14,7 @@ describe('SeriesToColumns Transformer', () => {
beforeAll(() => { beforeAll(() => {
mockTransformationsRegistry([seriesToColumnsTransformer]); mockTransformationsRegistry([seriesToColumnsTransformer]);
}); });
const everySecondSeries = toDataFrame({ const everySecondSeries = toDataFrame({
name: 'even', name: 'even',
fields: [ fields: [
@ -44,38 +45,53 @@ describe('SeriesToColumns Transformer', () => {
expect(filtered.fields).toEqual([ expect(filtered.fields).toEqual([
{ {
name: 'time', name: 'time',
state: {
title: 'time',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]), values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
config: {}, config: {},
labels: { origin: 'even,odd' }, labels: undefined,
}, },
{ {
name: 'temperature {even}', name: 'temperature',
state: {
title: 'temperature even',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]), values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'humidity {even}', name: 'humidity',
state: {
title: 'humidity even',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]), values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'temperature {odd}', name: 'temperature',
state: {
title: 'temperature odd',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]), values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
{ {
name: 'humidity {odd}', name: 'humidity',
state: {
title: 'humidity odd',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]), values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
]); ]);
}); });
@ -92,38 +108,53 @@ describe('SeriesToColumns Transformer', () => {
expect(filtered.fields).toEqual([ expect(filtered.fields).toEqual([
{ {
name: 'temperature', name: 'temperature',
state: {
title: 'temperature',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]), values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]),
config: {}, config: {},
labels: { origin: 'even,odd' }, labels: undefined,
}, },
{ {
name: 'time {even}', name: 'time',
state: {
title: 'time even',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]), values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'humidity {even}', name: 'humidity',
state: {
title: 'humidity even',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]), values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'time {odd}', name: 'time',
state: {
title: 'time odd',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]), values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
{ {
name: 'humidity {odd}', name: 'humidity',
state: {
title: 'humidity odd',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]), values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
]); ]);
}); });
@ -144,38 +175,53 @@ describe('SeriesToColumns Transformer', () => {
expect(filtered.fields).toEqual([ expect(filtered.fields).toEqual([
{ {
name: 'time', name: 'time',
state: {
title: 'time',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]), values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
config: {}, config: {},
labels: { origin: 'even,odd' }, labels: undefined,
}, },
{ {
name: 'temperature {even}', name: 'temperature',
state: {
title: 'temperature even',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]), values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'humidity {even}', name: 'humidity',
state: {
title: 'humidity even',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]), values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
config: {}, config: {},
labels: { origin: 'even' }, labels: { name: 'even' },
}, },
{ {
name: 'temperature {odd}', name: 'temperature',
state: {
title: 'temperature odd',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]), values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
{ {
name: 'humidity {odd}', name: 'humidity',
state: {
title: 'humidity odd',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]), values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
config: {}, config: {},
labels: { origin: 'odd' }, labels: { name: 'odd' },
}, },
]); ]);
}); });
@ -209,24 +255,33 @@ describe('SeriesToColumns Transformer', () => {
const expected: Field[] = [ const expected: Field[] = [
{ {
name: 'time', name: 'time',
state: {
title: 'time',
},
type: FieldType.time, type: FieldType.time,
values: new ArrayVector([1000, 2000, 3000, 4000]), values: new ArrayVector([1000, 2000, 3000, 4000]),
config: {}, config: {},
labels: { origin: 'temperature,B' }, labels: undefined,
}, },
{ {
name: 'temperature', name: 'temperature',
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([1, 3, 5, 7]), values: new ArrayVector([1, 3, 5, 7]),
config: {}, config: {},
labels: { origin: 'temperature' }, state: {
title: 'temperature temperature',
},
labels: { name: 'temperature' },
}, },
{ {
name: 'temperature {B}', name: 'temperature',
state: {
title: 'temperature B',
},
type: FieldType.number, type: FieldType.number,
values: new ArrayVector([2, 4, 6, 8]), values: new ArrayVector([2, 4, 6, 8]),
config: {}, config: {},
labels: { origin: 'B' }, labels: { name: 'B' },
}, },
]; ];

View File

@ -1,70 +1,81 @@
import { DataFrame, DataTransformerInfo } from '../../types'; import { DataFrame, DataTransformerInfo, Field } from '../../types';
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { MutableDataFrame } from '../../dataframe'; import { MutableDataFrame } from '../../dataframe';
import { filterFieldsByNameTransformer } from './filterByName';
import { ArrayVector } from '../../vector'; import { ArrayVector } from '../../vector';
import { getFieldTitle } from '../../field/fieldState';
export interface SeriesToColumnsOptions { export interface SeriesToColumnsOptions {
byField?: string; byField?: string;
} }
const DEFAULT_KEY_FIELD = 'Time';
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = { export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
id: DataTransformerID.seriesToColumns, id: DataTransformerID.seriesToColumns,
name: 'Series as columns', name: 'Series as columns',
description: 'Groups series by field and returns values as columns', description: 'Groups series by field and returns values as columns',
defaultOptions: { defaultOptions: {
byField: 'Time', byField: DEFAULT_KEY_FIELD,
}, },
transformer: options => (data: DataFrame[]) => { transformer: options => (data: DataFrame[]) => {
const optionsArray = options.byField ? [options.byField] : []; const keyFieldMatch = options.byField || DEFAULT_KEY_FIELD;
// not sure if I should use filterFieldsByNameTransformer to get the key field const allFields: FieldsToProcess[] = [];
const keyDataFrames = filterFieldsByNameTransformer.transformer({
include: optionsArray, for (let frameIndex = 0; frameIndex < data.length; frameIndex++) {
})(data); const frame = data[frameIndex];
if (!keyDataFrames.length) { const keyField = findKeyField(frame, keyFieldMatch);
// for now we only parse data frames with 2 fields
if (!keyField) {
return data; return data;
} }
// 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;
}
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);
}
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++) { for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex]; const sourceField = frame.fields[fieldIndex];
const origin = getOrigin(frame, frameIndex);
const name = getColumnName(otherDataFrames, frameIndex, fieldIndex, false); if (sourceField === keyField) {
if (processed.fields.find(field => field.name === name)) {
continue; 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 } } = {}; 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 first value as string" : {
"key field name": key field first value, "key field name": key field first value,
@ -78,25 +89,19 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
} }
} }
*/ */
for (let seriesIndex = 0; seriesIndex < keyDataFrames.length; seriesIndex++) {
const keyDataFrame = keyDataFrames[seriesIndex]; for (let fieldIndex = 0; fieldIndex < allFields.length; fieldIndex++) {
const keyField = keyDataFrame.fields[0]; const { sourceField, keyField, newField } = allFields[fieldIndex];
const keyColumnName = getColumnName(keyDataFrames, seriesIndex, 0, true); const newFieldTitle = getFieldTitle(newField, resultFrame);
const keyValues = keyField.values;
for (let valueIndex = 0; valueIndex < keyValues.length; valueIndex++) { for (let valueIndex = 0; valueIndex < sourceField.values.length; valueIndex++) {
const keyValue = keyValues.get(valueIndex); const value = sourceField.values.get(valueIndex);
const keyValueAsString = keyValue.toString(); const keyValue = keyField.values.get(valueIndex);
if (!byKeyField[keyValueAsString]) {
byKeyField[keyValueAsString] = { [keyColumnName]: keyValue }; if (!byKeyField[keyValue]) {
} byKeyField[keyValue] = { [newFieldTitle]: value, [keyFieldTitle]: keyValue };
const otherDataFrame = otherDataFrames[seriesIndex]; } else {
for (let otherIndex = 0; otherIndex < otherDataFrame.fields.length; otherIndex++) { byKeyField[keyValue][newFieldTitle] = value;
const otherColumnName = getColumnName(otherDataFrames, seriesIndex, otherIndex, false);
const otherField = otherDataFrame.fields[otherIndex];
const otherValue = otherField.values.get(valueIndex);
if (!byKeyField[keyValueAsString][otherColumnName]) {
byKeyField[keyValueAsString] = { ...byKeyField[keyValueAsString], [otherColumnName]: otherValue };
}
} }
} }
} }
@ -104,27 +109,33 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
const keyValueStrings = Object.keys(byKeyField); const keyValueStrings = Object.keys(byKeyField);
for (let rowIndex = 0; rowIndex < keyValueStrings.length; rowIndex++) { for (let rowIndex = 0; rowIndex < keyValueStrings.length; rowIndex++) {
const keyValueAsString = keyValueStrings[rowIndex]; const keyValueAsString = keyValueStrings[rowIndex];
for (let fieldIndex = 0; fieldIndex < processed.fields.length; fieldIndex++) {
const field = processed.fields[fieldIndex]; for (let fieldIndex = 0; fieldIndex < resultFrame.fields.length; fieldIndex++) {
const value = byKeyField[keyValueAsString][field.name] ?? null; const field = resultFrame.fields[fieldIndex];
const otherColumnName = getFieldTitle(field, resultFrame);
const value = byKeyField[keyValueAsString][otherColumnName] ?? null;
field.values.add(value); field.values.add(value);
} }
} }
return [processed]; return [resultFrame];
}, },
}; };
const getColumnName = (frames: DataFrame[], frameIndex: number, fieldIndex: number, isKeyField = false) => { function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
const frame = frames[frameIndex]; for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
const field = frame.fields[fieldIndex]; const field = frame.fields[fieldIndex];
const frameName = frame.name || `${frameIndex}`;
const fieldName = field.name;
const seriesName = isKeyField ? fieldName : fieldName === frameName ? fieldName : `${fieldName} {${frameName}}`;
return seriesName; if (matchTitle === getFieldTitle(field)) {
}; return field;
}
}
const getOrigin = (frame: DataFrame, index: number) => { return null;
return frame.name || `${index}`; }
};
interface FieldsToProcess {
newField: Field;
sourceField: Field;
keyField: Field;
}

View File

@ -111,6 +111,10 @@ export type TimeSeriesPoints = TimeSeriesValue[][];
export interface TimeSeries extends QueryResultBase { export interface TimeSeries extends QueryResultBase {
target: string; target: string;
/**
* If name is manually configured via an alias / legend pattern
*/
title?: string;
datapoints: TimeSeriesPoints; datapoints: TimeSeriesPoints;
unit?: string; unit?: string;
tags?: Labels; tags?: Labels;

View File

@ -4,7 +4,6 @@ import { QueryResultBase, Labels, NullValueMode } from './data';
import { DisplayProcessor, DisplayValue } from './displayValue'; import { DisplayProcessor, DisplayValue } from './displayValue';
import { DataLink, LinkModel } from './dataLink'; import { DataLink, LinkModel } from './dataLink';
import { Vector } from './vector'; import { Vector } from './vector';
import { FieldCalcs } from '../transformations/fieldReducer';
import { FieldColor } from './fieldColor'; import { FieldColor } from './fieldColor';
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
@ -53,8 +52,6 @@ export interface FieldConfig<TOptions extends object = any> {
// Panel Specific Values // Panel Specific Values
custom?: TOptions; custom?: TOptions;
scopedVars?: ScopedVars;
} }
export interface ValueLinkConfig { export interface ValueLinkConfig {
@ -85,9 +82,9 @@ export interface Field<T = any, V = Vector<T>> {
labels?: Labels; 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 * Convert text to the field value
@ -105,6 +102,23 @@ export interface Field<T = any, V = Vector<T>> {
getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>; getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>;
} }
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 { export interface DataFrame extends QueryResultBase {
name?: string; name?: string;
fields: Field[]; // All fields of equal length fields: Field[]; // All fields of equal length
@ -131,3 +145,7 @@ export interface DataFrameDTO extends QueryResultBase {
name?: string; name?: string;
fields: Array<FieldDTO | Field>; fields: Array<FieldDTO | Field>;
} }
export interface FieldCalcs extends Record<string, any> {}
export const TIME_SERIES_FIELD_NAME = 'Value';

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types'; import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
import { FieldMatcherID, fieldMatchers } from '@grafana/data'; import { FieldMatcherID, fieldMatchers, getFieldTitle } from '@grafana/data';
import { Select } from '../Select/Select'; import { Select } from '../Select/Select';
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> { export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
@ -10,7 +10,7 @@ export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<s
for (const frame of data) { for (const frame of data) {
for (const field of frame.fields) { for (const field of frame.fields) {
names.add(field.name); names.add(getFieldTitle(field, frame, data));
} }
} }
if (options) { if (options) {
@ -32,6 +32,6 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
id: FieldMatcherID.byName, id: FieldMatcherID.byName,
component: FieldNameMatcherEditor, component: FieldNameMatcherEditor,
matcher: fieldMatchers.get(FieldMatcherID.byName), matcher: fieldMatchers.get(FieldMatcherID.byName),
name: 'Filter by name', name: 'Filter by field',
description: 'Set properties for fields matching the name', description: 'Set properties for fields matching the name',
}; };

View File

@ -1,5 +1,5 @@
import { TextAlignProperty } from 'csstype'; 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 { Column } from 'react-table';
import { DefaultCell } from './DefaultCell'; import { DefaultCell } from './DefaultCell';
import { BarGaugeCell } from './BarGaugeCell'; import { BarGaugeCell } from './BarGaugeCell';
@ -48,11 +48,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
} }
const Cell = getCellComponent(fieldTableOptions.displayMode, field); const Cell = getCellComponent(fieldTableOptions.displayMode, field);
columns.push({ columns.push({
Cell, Cell,
id: fieldIndex.toString(), id: fieldIndex.toString(),
Header: field.config.title ?? field.name, Header: getFieldTitle(field, data),
accessor: (row: any, i: number) => { accessor: (row: any, i: number) => {
return field.values.get(i); return field.values.get(i);
}, },

View File

@ -14,6 +14,7 @@ import {
binaryOperators, binaryOperators,
CalculateFieldMode, CalculateFieldMode,
getResultFieldNameForCalculateFieldTransformerOptions, getResultFieldNameForCalculateFieldTransformerOptions,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { StatsPicker } from '../StatsPicker/StatsPicker'; import { StatsPicker } from '../StatsPicker/StatsPicker';
import { Switch } from '../Forms/Legacy/Switch/Switch'; import { Switch } from '../Forms/Legacy/Switch/Switch';
@ -80,14 +81,18 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
const allNames: string[] = []; const allNames: string[] = [];
const byName: KeyValue<boolean> = {}; const byName: KeyValue<boolean> = {};
for (const frame of input) { for (const frame of input) {
for (const field of frame.fields) { for (const field of frame.fields) {
if (field.type !== FieldType.number) { if (field.type !== FieldType.number) {
continue; continue;
} }
if (!byName[field.name]) {
byName[field.name] = true; const title = getFieldTitle(field, frame, input);
allNames.push(field.name);
if (!byName[title]) {
byName[title] = true;
allNames.push(title);
} }
} }
} }
@ -95,6 +100,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
if (configuredOptions.length) { if (configuredOptions.length) {
const options: string[] = []; const options: string[] = [];
const selected: string[] = []; const selected: string[] = [];
for (const v of allNames) { for (const v of allNames) {
if (configuredOptions.includes(v)) { if (configuredOptions.includes(v)) {
selected.push(v); selected.push(v);

View File

@ -6,6 +6,7 @@ import {
standardTransformers, standardTransformers,
TransformerRegistyItem, TransformerRegistyItem,
TransformerUIProps, TransformerUIProps,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { HorizontalGroup } from '../Layout/Layout'; import { HorizontalGroup } from '../Layout/Layout';
import { Input } from '../Input/Input'; import { Input } from '../Input/Input';
@ -45,6 +46,12 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
this.initOptions(); this.initOptions();
} }
componentDidUpdate(oldProps: FilterByNameTransformerEditorProps) {
if (this.props.input !== oldProps.input) {
this.initOptions();
}
}
private initOptions() { private initOptions() {
const { input, options } = this.props; const { input, options } = this.props;
const configuredOptions = options.include ? options.include : []; const configuredOptions = options.include ? options.include : [];
@ -54,10 +61,11 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
for (const frame of input) { for (const frame of input) {
for (const field of frame.fields) { for (const field of frame.fields) {
let v = byName[field.name]; const id = getFieldTitle(field, frame, input);
let v = byName[id];
if (!v) { if (!v) {
v = byName[field.name] = { v = byName[id] = {
name: field.name, name: id,
count: 0, count: 0,
}; };
allNames.push(v); allNames.push(v);
@ -147,7 +155,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
return ( return (
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form gf-form--grow"> <div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div> <div className="gf-form-label width-8">Identifier</div>
<HorizontalGroup spacing="xs" align="flex-start" wrap> <HorizontalGroup spacing="xs" align="flex-start" wrap>
<Field <Field
invalid={!isRegexValid} invalid={!isRegexValid}

View File

@ -10,6 +10,7 @@ import {
standardTransformers, standardTransformers,
TransformerRegistyItem, TransformerRegistyItem,
TransformerUIProps, TransformerUIProps,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes'; import { stylesFactory, useTheme } from '../../themes';
import { Input } from '../Input/Input'; import { Input } from '../Input/Input';
@ -71,6 +72,11 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
[onChange, fieldNames, renameByName] [onChange, fieldNames, renameByName]
); );
// Show warning that we only apply the first frame
if (input.length > 1) {
return <div>Organize fields only works with a single frame. Consider applying a join transformation first.</div>;
}
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-fields-transformer" direction="vertical"> <Droppable droppableId="sortable-fields-transformer" direction="vertical">
@ -210,10 +216,11 @@ export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] =>
} }
return frame.fields.reduce((names, field) => { return frame.fields.reduce((names, field) => {
names[field.name] = null; const t = getFieldTitle(field, frame, input);
names[t] = true;
return names; return names;
}, names); }, names);
}, {} as Record<string, null>) }, {} as Record<string, boolean>)
); );
}; };
@ -221,7 +228,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<Organiz
id: DataTransformerID.organize, id: DataTransformerID.organize,
editor: OrganizeFieldsTransformerEditor, editor: OrganizeFieldsTransformerEditor,
transformation: standardTransformers.organizeFieldsTransformer, transformation: standardTransformers.organizeFieldsTransformer,
name: 'Change order, hide and rename', name: 'Organize fields',
description: description:
"Allows the user to re-order, hide, or rename columns. Useful when data source doesn't allow overrides for visualizing data.", "Allows the user to re-order, hide, or rename fields / columns. Useful when data source doesn't allow overrides for visualizing data.",
}; };

View File

@ -8,3 +8,8 @@ export * from './types';
export * from './utils'; export * from './utils';
export * from './themes'; export * from './themes';
export * from './slate-plugins'; export * from './slate-plugins';
// Exposes standard editors for registries of optionsUi config and panel options UI
export { getStandardFieldConfigs, getStandardOptionEditors } from './utils//standardEditors';
// Exposes standard transformers for registry of Transformations
export { getStandardTransformers } from './utils/standardTransformers';

View File

@ -9,8 +9,3 @@ export { default as ansicolor } from './ansicolor';
import * as DOMUtil from './dom'; // includes Element.closest polyfil import * as DOMUtil from './dom'; // includes Element.closest polyfil
export { DOMUtil }; export { DOMUtil };
export { renderOrCallToRender } from './renderOrCallToRender'; export { renderOrCallToRender } from './renderOrCallToRender';
// Exposes standard editors for registries of optionsUi config and panel options UI
export { getStandardFieldConfigs, getStandardOptionEditors } from './standardEditors';
// Exposes standard transformers for registry of Transformations
export { getStandardTransformers } from './standardTransformers';

View File

@ -6,6 +6,7 @@ import {
SelectableValue, SelectableValue,
toCSV, toCSV,
transformDataFrame, transformDataFrame,
getFrameDisplayTitle,
} from '@grafana/data'; } from '@grafana/data';
import { Button, Field, Icon, Select, Table } from '@grafana/ui'; import { Button, Field, Icon, Select, Table } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -105,7 +106,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
const choices = dataFrames.map((frame, index) => { const choices = dataFrames.map((frame, index) => {
return { return {
value: index, value: index,
label: `${frame.name} (${index})`, label: `${getFrameDisplayTitle(frame)} (${index})`,
}; };
}); });

View File

@ -169,9 +169,9 @@ export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataF
for (const result of results) { for (const result of results) {
const dataFrame = guessFieldTypes(toDataFrame(result)); const dataFrame = guessFieldTypes(toDataFrame(result));
// clear out any cached calcs // clear out the cached info
for (const field of dataFrame.fields) { for (const field of dataFrame.fields) {
field.calcs = null; field.state = null;
} }
dataFrames.push(dataFrame); dataFrames.push(dataFrame);

View File

@ -1,7 +1,7 @@
import '../datasource'; import '../datasource';
import { CloudWatchDatasource } from '../datasource'; import { CloudWatchDatasource } from '../datasource';
import * as redux from 'app/store/store'; 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 { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all'; import { CustomVariable } from 'app/features/templating/all';
import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types'; import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types';
@ -233,7 +233,7 @@ describe('CloudWatchDatasource', () => {
it('should return series list', done => { it('should return series list', done => {
ctx.ds.query(query).then((result: any) => { 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]); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done(); 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 => { 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' }]; response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
ctx.ds.query(query).then((result: any) => { 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(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( 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\'))"}]}` `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'))` }, { Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
]; ];
ctx.ds.query(query).then((result: any) => { 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(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( 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\'))"}]}` `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 => { it('should be built correctly if the query is a metric stat query', done => {
response.results['A'].meta.gmdMeta = [{ Period: '300' }]; response.results['A'].meta.gmdMeta = [{ Period: '300' }];
ctx.ds.query(query).then((result: any) => { 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(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( 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\"}]]}` `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 => { it('should return series list', done => {
ctx.ds.query(query).then((result: any) => { 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]); expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done(); done();
}); });

View File

@ -929,9 +929,10 @@ describe('ElasticResponse', () => {
const hist: KeyValue<number> = {}; const hist: KeyValue<number> = {};
const histogramResults = new MutableDataFrame(result.data[1]); const histogramResults = new MutableDataFrame(result.data[1]);
rows = new DataFrameView(histogramResults); rows = new DataFrameView(histogramResults);
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
const row = rows.get(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) => { response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => {

View File

@ -1,5 +1,5 @@
import Datasource from '../datasource'; 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 { TemplateSrv } from 'app/features/templating/template_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ 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) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame; 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.length).toEqual(1);
expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075); expect(data.fields[1].values.get(0)).toEqual(2.2075);
@ -218,7 +218,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame; 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.length).toEqual(1);
expect(data.fields[0].values.get(0)).toEqual(1558278660000); expect(data.fields[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075); expect(data.fields[1].values.get(0)).toEqual(2.2075);
@ -279,7 +279,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame; 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[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075); expect(data.fields[1].values.get(0)).toEqual(2.2075);
}); });
@ -322,7 +322,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame; 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.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(3); expect(data.fields[1].values.get(0)).toEqual(3);
@ -376,14 +376,14 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(2); expect(results.data.length).toBe(2);
let data = results.data[0] as DataFrame; 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[1].values.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(10); expect(data.fields[1].values.get(0)).toEqual(10);
expect(data.fields[0].values.get(1)).toEqual(1504112400000); expect(data.fields[0].values.get(1)).toEqual(1504112400000);
expect(data.fields[1].values.get(1)).toEqual(20); expect(data.fields[1].values.get(1)).toEqual(20);
data = results.data[1] as DataFrame; 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[1].values.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000); expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(1); expect(data.fields[1].values.get(0)).toEqual(1);

View File

@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource';
import FakeSchemaData from './__mocks__/schema'; import FakeSchemaData from './__mocks__/schema';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { KustoSchema, AzureLogsVariable } from '../types'; 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__ import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
@ -183,10 +183,11 @@ describe('AzureLogAnalyticsDatasource', () => {
it('should return a list of datapoints', () => { it('should return a list of datapoints', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); 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].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[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.toArray().length).toBe(6);
expect(results.data[0].fields[0].values.get(0)).toEqual(1587633300000); expect(results.data[0].fields[0].values.get(0)).toEqual(1587633300000);
expect(results.data[0].fields[1].values.get(0)).toEqual(2017.25); expect(results.data[0].fields[1].values.get(0)).toEqual(2017.25);

View File

@ -1,7 +1,7 @@
import AzureMonitorDatasource from '../datasource'; import AzureMonitorDatasource from '../datasource';
import { TemplateSrv } from 'app/features/templating/template_srv'; 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__ import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
@ -137,7 +137,7 @@ describe('AzureMonitorDatasource', () => {
return ctx.ds.query(options).then((results: any) => { return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame; 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[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075); expect(data.fields[1].values.get(0)).toEqual(2.2075);
expect(data.fields[0].values.get(1)).toEqual(1558278720000); expect(data.fields[0].values.get(1)).toEqual(1558278720000);

View File

@ -123,6 +123,9 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
for (let i = 0; i < series.length; i++) { for (let i = 0; i < series.length; i++) {
const s = series[i]; const s = series[i];
// Disables Grafana own series naming
s.title = s.target;
for (let y = 0; y < s.datapoints.length; y++) { for (let y = 0; y < s.datapoints.length; y++) {
s.datapoints[y][1] *= 1000; s.datapoints[y][1] *= 1000;
} }

View File

@ -2,7 +2,7 @@ import { GraphiteDatasource } from '../datasource';
import _ from 'lodash'; import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { dateTime } from '@grafana/data'; import { dateTime, getFrameDisplayTitle } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
jest.mock('@grafana/runtime', () => ({ jest.mock('@grafana/runtime', () => ({
@ -91,8 +91,8 @@ describe('graphiteDatasource', () => {
}); });
expect(result.data.length).toBe(2); expect(result.data.length).toBe(2);
expect(result.data[0].name).toBe('seriesA'); expect(getFrameDisplayTitle(result.data[0])).toBe('seriesA');
expect(result.data[1].name).toBe('seriesB'); expect(getFrameDisplayTitle(result.data[1])).toBe('seriesB');
expect(result.data[0].length).toBe(2); expect(result.data[0].length).toBe(2);
expect(result.data[0].meta.notices.length).toBe(1); 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'); expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');

View File

@ -13,6 +13,8 @@ import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
dateTime, dateTime,
LoadingState, LoadingState,
toDataFrame,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { PromOptions, PromQuery } from './types'; import { PromOptions, PromQuery } from './types';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
@ -586,8 +588,9 @@ describe('PrometheusDatasource', () => {
}); });
it('should return series list', async () => { it('should return series list', async () => {
const frame = toDataFrame(results.data[0]);
expect(results.data.length).toBe(1); 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', () => { it('should return series list', () => {
const frame = toDataFrame(results.data[0]);
expect(results.data.length).toBe(1); 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', () => { it('should return series list', () => {
const frame = toDataFrame(results.data[0]);
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
expect(results.data[0].target).toBe('test{job="testjob"}'); expect(getFieldTitle(frame.fields[1])).toBe('test{job="testjob"}');
}); });
}); });

View File

@ -162,6 +162,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([ expect(result).toEqual([
{ {
target: '1', target: '1',
title: '1',
query: undefined, query: undefined,
datapoints: [ datapoints: [
[10, 1445000010000], [10, 1445000010000],
@ -172,6 +173,7 @@ describe('Prometheus Result Transformer', () => {
}, },
{ {
target: '2', target: '2',
title: '2',
query: undefined, query: undefined,
datapoints: [ datapoints: [
[10, 1445000010000], [10, 1445000010000],
@ -182,6 +184,7 @@ describe('Prometheus Result Transformer', () => {
}, },
{ {
target: '3', target: '3',
title: '3',
query: undefined, query: undefined,
datapoints: [ datapoints: [
[10, 1445000010000], [10, 1445000010000],
@ -285,6 +288,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([ expect(result).toEqual([
{ {
target: 'test{job="testjob"}', target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
query: undefined, query: undefined,
datapoints: [ datapoints: [
[10, 0], [10, 0],
@ -324,6 +328,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([ expect(result).toEqual([
{ {
target: 'test{job="testjob"}', target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
query: undefined, query: undefined,
datapoints: [ datapoints: [
[null, 0], [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', () => { it('should align null values with step', () => {
const response = { const response = {
status: 'success', status: 'success',
@ -356,13 +419,20 @@ describe('Prometheus Result Transformer', () => {
step: 2, step: 2,
start: 0, start: 0,
end: 8, end: 8,
refId: 'A',
meta: { custom: { hello: '1' } },
}; };
const result = ctx.resultTransformer.transform({ data: response }, options); const result = ctx.resultTransformer.transform({ data: response }, options);
expect(result).toEqual([ expect(result).toEqual([
{ {
target: 'test{job="testjob"}', target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
meta: {
custom: { hello: '1' },
},
query: undefined, query: undefined,
refId: 'A',
datapoints: [ datapoints: [
[null, 0], [null, 0],
[null, 2000], [null, 2000],

View File

@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import TableModel from 'app/core/table_model'; 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'; import { TemplateSrv } from 'app/features/templating/template_srv';
export class ResultTransformer { export class ResultTransformer {
@ -42,9 +42,7 @@ export class ResultTransformer {
transformMetricData(metricData: any, options: any, start: number, end: number) { transformMetricData(metricData: any, options: any, start: number, end: number) {
const dps = []; const dps = [];
let metricLabel = null; const { name, labels, title } = this.createLabelInfo(metricData.metric, options);
metricLabel = this.createMetricLabel(metricData.metric, options);
const stepMs = parseFloat(options.step) * 1000; const stepMs = parseFloat(options.step) * 1000;
let baseTimestamp = start * 1000; let baseTimestamp = start * 1000;
@ -76,9 +74,10 @@ export class ResultTransformer {
datapoints: dps, datapoints: dps,
query: options.query, query: options.query,
refId: options.refId, refId: options.refId,
target: name,
tags: labels,
title,
meta: options.meta, meta: options.meta,
target: metricLabel,
tags: metricData.metric,
}; };
} }
@ -142,23 +141,39 @@ export class ResultTransformer {
transformInstantMetricData(md: any, options: any) { transformInstantMetricData(md: any, options: any) {
const dps = []; const dps = [];
let metricLabel = null; const { name, labels } = this.createLabelInfo(md.metric, options);
metricLabel = this.createMetricLabel(md.metric, options);
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]); 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) { createLabelInfo(labels: { [key: string]: string }, options: any): { name?: string; labels: Labels; title?: string } {
let label = ''; if (options?.legendFormat) {
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { const title = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labels);
label = this.getOriginalMetricName(labelData); return { name: title, title, labels };
} else {
label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData);
} }
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 }) { 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[]) { transformToHistogramOverTime(seriesList: TimeSeries[]) {
/* t1 = timestamp1, t2 = timestamp2 etc. /* t1 = timestamp1, t2 = timestamp2 etc.
t1 t2 t3 t1 t2 t3 t1 t2 t3 t1 t2 t3

View File

@ -8,6 +8,7 @@ import {
DataFrame, DataFrame,
getTimeField, getTimeField,
dateTime, dateTime,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import config from 'app/core/config'; import config from 'app/core/config';
@ -31,27 +32,24 @@ export class DataProcessor {
for (let i = 0; i < dataList.length; i++) { for (let i = 0; i < dataList.length; i++) {
const series = dataList[i]; const series = dataList[i];
const { timeField } = getTimeField(series); const { timeField } = getTimeField(series);
if (!timeField) { if (!timeField) {
continue; continue;
} }
const seriesName = series.name ? series.name : series.refId;
for (let j = 0; j < series.fields.length; j++) { for (let j = 0; j < series.fields.length; j++) {
const field = series.fields[j]; const field = series.fields[j];
if (field.type !== FieldType.number) { if (field.type !== FieldType.number) {
continue; continue;
} }
const name = getFieldTitle(field, series, dataList);
let name = field.config && field.config.title ? field.config.title : field.name;
if (seriesName && dataList.length > 0 && name !== seriesName) {
name = seriesName + ' ' + name;
}
const datapoints = []; const datapoints = [];
for (let r = 0; r < series.length; r++) { for (let r = 0; r < series.length; r++) {
datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]); datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]);
} }
list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range)); 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) { if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) {
const first = list[0]; const first = list[0];
first.alias = first.aliasEscaped = 'Count'; first.alias = first.aliasEscaped = 'Count';
for (let i = 1; i < list.length; i++) { for (let i = 1; i < list.length; i++) {
first.datapoints = first.datapoints.concat(list[i].datapoints); first.datapoints = first.datapoints.concat(list[i].datapoints);
} }
return [first]; return [first];
} }

View File

@ -17,6 +17,7 @@ import {
FieldColor, FieldColor,
FieldColorMode, FieldColorMode,
FieldConfigSource, FieldConfigSource,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { SeriesOptions, GraphOptions, GraphLegendEditorLegendOptions } from './types'; import { SeriesOptions, GraphOptions, GraphLegendEditorLegendOptions } from './types';
@ -122,7 +123,7 @@ export const getGraphSeriesModel = (
}); });
graphs.push({ graphs.push({
label: field.name, label: getFieldTitle(field, series, dataFrames),
data: points, data: points,
color: field.config.color?.fixedColor, color: field.config.color?.fixedColor,
info: statsDisplayValues, info: statsDisplayValues,

View File

@ -23,6 +23,7 @@ import {
PanelEvents, PanelEvents,
formattedValueToString, formattedValueToString,
locationUtil, locationUtil,
getFieldTitle,
} from '@grafana/data'; } from '@grafana/data';
import { convertOldAngularValueMapping } from '@grafana/ui'; import { convertOldAngularValueMapping } from '@grafana/ui';
@ -156,6 +157,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
onFramesReceived(frames: DataFrame[]) { onFramesReceived(frames: DataFrame[]) {
const { panel } = this; const { panel } = this;
this.dataList = frames;
if (frames && frames.length > 1) { if (frames && frames.length > 1) {
this.data = { this.data = {
@ -204,7 +206,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
processField(fieldInfo: FieldInfo) { processField(fieldInfo: FieldInfo) {
const { panel, dashboard } = this; 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 calc = panel.valueName;
let calcField = fieldInfo.field; let calcField = fieldInfo.field;
let val: any = undefined; let val: any = undefined;

View File

@ -1,5 +1,5 @@
import { SingleStatCtrl, ShowData } from '../module'; 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 { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { LegacyResponseData } from '@grafana/data'; import { LegacyResponseData } from '@grafana/data';
import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardModel } from 'app/features/dashboard/state';
@ -90,7 +90,8 @@ describe('SingleStatCtrl', () => {
}); });
it('Should use series avg as default main value', () => { 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', () => { it('should set formatted value', () => {

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Table, Select } from '@grafana/ui'; 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 { Options } from './types';
import { css } from 'emotion'; import { css } from 'emotion';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
@ -101,7 +101,7 @@ export class TablePanel extends Component<Props> {
const currentIndex = this.getCurrentFrameIndex(); const currentIndex = this.getCurrentFrameIndex();
const names = data.series.map((frame, index) => { const names = data.series.map((frame, index) => {
return { return {
label: `${frame.name ?? 'Series'}`, label: getFrameDisplayTitle(frame),
value: index, value: index,
}; };
}); });