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);
expect(series.fields[1].name).toBe(input1.target);
expect(series.name).toBe(input1.target);
expect(series.fields[1].name).toBe('Value');
const v0 = series.fields[0].values;
const v1 = series.fields[1].values;
@ -182,6 +183,24 @@ describe('SerisData backwards compatibility', () => {
expect(roundtrip.target).toBe(timeseries.target);
});
it('can convert TimeSeries to series and back again with tags should render name with tags', () => {
const timeseries = {
target: 'Series A',
tags: { server: 'ServerA', job: 'app' },
datapoints: [
[100, 1],
[200, 2],
],
};
const series = toDataFrame(timeseries);
expect(isDataFrame(timeseries)).toBeFalsy();
expect(isDataFrame(series)).toBeTruthy();
const roundtrip = toLegacyResponseData(series) as TimeSeries;
expect(isDataFrame(roundtrip)).toBeFalsy();
expect(roundtrip.target).toBe('{job="app", server="ServerA"}');
});
it('can convert empty table to DataFrame then back to legacy', () => {
const table = {
columns: [],

View File

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

View File

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

View File

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

View File

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

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 { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldTitle, getFrameDisplayTitle } from './fieldState';

View File

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

View File

@ -66,7 +66,7 @@ describe('Stats Calculators', () => {
});
it('should support a single stat also', () => {
basicTable.fields[0].calcs = undefined; // clear the cache
basicTable.fields[0].state = undefined; // clear the cache
const stats = reduceField({
field: basicTable.fields[0],
reducers: ['first'],

View File

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

View File

@ -2,6 +2,7 @@ import { Field, DataFrame } from '../../types/dataFrame';
import { FieldMatcherID, FrameMatcherID } from './ids';
import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations';
import { stringToJsRegex } from '../../text/string';
import { getFieldTitle } from '../../field/fieldState';
// General Field matcher
const fieldNameMacher: FieldMatcherInfo<string> = {
@ -18,7 +19,7 @@ const fieldNameMacher: FieldMatcherInfo<string> = {
console.error(e);
}
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(`
Array [
Object {
"A {0}": 1,
"B {1}": 2,
"C {1}": 3,
"D {1}": "first",
"A": 1,
"B": 2,
"C": 3,
"D": "first",
"The Total": 6,
"TheTime": 1000,
},
Object {
"A {0}": 100,
"B {1}": 200,
"C {1}": 300,
"D {1}": "second",
"A": 100,
"B": 200,
"C": 300,
"D": "second",
"The Total": 600,
"TheTime": 2000,
},

View File

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

View File

@ -43,38 +43,6 @@ describe('Labels as Columns', () => {
expect(result[0].fields).toEqual(expected);
});
it('data frames where frame name is same as value field name replace field name with name Value', () => {
const cfg: DataTransformerConfig<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', () => {
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
id: DataTransformerID.labelsToFields,

View File

@ -73,21 +73,12 @@ function getFramesWithOnlyValueFields(data: DataFrame[]): DataFrame[] {
for (let i = 0; i < series.fields.length; i++) {
const field = series.fields[i];
if (field.type !== FieldType.number) {
continue;
}
// When we transform a time series to DataFrame we put series name in field name.
// This casues problems for this transformer that want all time series values in a Value column
// So here we change field names that have same name as DataFrame to just Value
if (field.name === series.name) {
fields.push({
...field,
name: 'Value',
});
} else {
fields.push(field);
}
fields.push(field);
}
if (!fields.length) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import { QueryResultBase, Labels, NullValueMode } from './data';
import { DisplayProcessor, DisplayValue } from './displayValue';
import { DataLink, LinkModel } from './dataLink';
import { Vector } from './vector';
import { FieldCalcs } from '../transformations/fieldReducer';
import { FieldColor } from './fieldColor';
import { ScopedVars } from './ScopedVars';
@ -53,8 +52,6 @@ export interface FieldConfig<TOptions extends object = any> {
// Panel Specific Values
custom?: TOptions;
scopedVars?: ScopedVars;
}
export interface ValueLinkConfig {
@ -85,9 +82,9 @@ export interface Field<T = any, V = Vector<T>> {
labels?: Labels;
/**
* Cache of reduced values
* Cached values with appropriate dispaly and id values
*/
calcs?: FieldCalcs;
state?: FieldState | null;
/**
* Convert text to the field value
@ -105,6 +102,23 @@ export interface Field<T = any, V = Vector<T>> {
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 {
name?: string;
fields: Field[]; // All fields of equal length
@ -131,3 +145,7 @@ export interface DataFrameDTO extends QueryResultBase {
name?: string;
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 { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
import { FieldMatcherID, fieldMatchers, getFieldTitle } from '@grafana/data';
import { Select } from '../Select/Select';
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
@ -10,7 +10,7 @@ export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<s
for (const frame of data) {
for (const field of frame.fields) {
names.add(field.name);
names.add(getFieldTitle(field, frame, data));
}
}
if (options) {
@ -32,6 +32,6 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
id: FieldMatcherID.byName,
component: FieldNameMatcherEditor,
matcher: fieldMatchers.get(FieldMatcherID.byName),
name: 'Filter by name',
name: 'Filter by field',
description: 'Set properties for fields matching the name',
};

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import {
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
getFieldTitle,
} from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
import { Input } from '../Input/Input';
@ -71,6 +72,11 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
[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 (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
@ -210,10 +216,11 @@ export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] =>
}
return frame.fields.reduce((names, field) => {
names[field.name] = null;
const t = getFieldTitle(field, frame, input);
names[t] = true;
return names;
}, names);
}, {} as Record<string, null>)
}, {} as Record<string, boolean>)
);
};
@ -221,7 +228,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<Organiz
id: DataTransformerID.organize,
editor: OrganizeFieldsTransformerEditor,
transformation: standardTransformers.organizeFieldsTransformer,
name: 'Change order, hide and rename',
name: 'Organize fields',
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 './themes';
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
export { DOMUtil };
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,
toCSV,
transformDataFrame,
getFrameDisplayTitle,
} from '@grafana/data';
import { Button, Field, Icon, Select, Table } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
@ -105,7 +106,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
const choices = dataFrames.map((frame, index) => {
return {
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) {
const dataFrame = guessFieldTypes(toDataFrame(result));
// clear out any cached calcs
// clear out the cached info
for (const field of dataFrame.fields) {
field.calcs = null;
field.state = null;
}
dataFrames.push(dataFrame);

View File

@ -1,7 +1,7 @@
import '../datasource';
import { CloudWatchDatasource } from '../datasource';
import * as redux from 'app/store/store';
import { DataSourceInstanceSettings, dateMath } from '@grafana/data';
import { DataSourceInstanceSettings, dateMath, getFrameDisplayTitle } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types';
@ -233,7 +233,7 @@ describe('CloudWatchDatasource', () => {
it('should return series list', done => {
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].name).toBe(response.results.A.series[0].name);
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});
@ -249,7 +249,7 @@ describe('CloudWatchDatasource', () => {
it('should be built correctly if theres one search expressions returned in meta for a given query row', done => {
response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].name).toBe(response.results.A.series[0].name);
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
@ -264,7 +264,7 @@ describe('CloudWatchDatasource', () => {
{ Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
];
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].name).toBe(response.results.A.series[0].name);
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
@ -276,7 +276,7 @@ describe('CloudWatchDatasource', () => {
it('should be built correctly if the query is a metric stat query', done => {
response.results['A'].meta.gmdMeta = [{ Period: '300' }];
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].name).toBe(response.results.A.series[0].name);
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
@ -517,7 +517,7 @@ describe('CloudWatchDatasource', () => {
it('should return series list', done => {
ctx.ds.query(query).then((result: any) => {
expect(result.data[0].name).toBe(response.results.A.series[0].name);
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
done();
});

View File

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

View File

@ -1,5 +1,5 @@
import Datasource from '../datasource';
import { DataFrame, toUtc } from '@grafana/data';
import { DataFrame, toUtc, getFrameDisplayTitle } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
@ -175,7 +175,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame;
expect(data.name).toEqual('PrimaryResult');
expect(getFrameDisplayTitle(data)).toEqual('PrimaryResult');
expect(data.fields[0].values.length).toEqual(1);
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075);
@ -218,7 +218,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame;
expect(data.name).toEqual('paritionA');
expect(getFrameDisplayTitle(data)).toEqual('paritionA');
expect(data.fields[0].values.length).toEqual(1);
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075);
@ -279,7 +279,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame;
expect(data.name).toEqual('exceptions/server');
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server');
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
expect(data.fields[1].values.get(0)).toEqual(2.2075);
});
@ -322,7 +322,7 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(1);
const data = results.data[0] as DataFrame;
expect(data.name).toEqual('exceptions/server');
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server');
expect(data.fields[0].values.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(3);
@ -376,14 +376,14 @@ describe('AppInsightsDatasource', () => {
return ctx.ds.query(options).then((results: any) => {
expect(results.data.length).toBe(2);
let data = results.data[0] as DataFrame;
expect(data.name).toEqual('exceptions/server{client/city="Miami"}');
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="Miami"}');
expect(data.fields[1].values.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(10);
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
expect(data.fields[1].values.get(1)).toEqual(20);
data = results.data[1] as DataFrame;
expect(data.name).toEqual('exceptions/server{client/city="San Antonio"}');
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="San Antonio"}');
expect(data.fields[1].values.length).toEqual(2);
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
expect(data.fields[1].values.get(0)).toEqual(1);

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { GraphiteDatasource } from '../datasource';
import _ from 'lodash';
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__
jest.mock('@grafana/runtime', () => ({
@ -91,8 +91,8 @@ describe('graphiteDatasource', () => {
});
expect(result.data.length).toBe(2);
expect(result.data[0].name).toBe('seriesA');
expect(result.data[1].name).toBe('seriesB');
expect(getFrameDisplayTitle(result.data[0])).toBe('seriesA');
expect(getFrameDisplayTitle(result.data[1])).toBe('seriesB');
expect(result.data[0].length).toBe(2);
expect(result.data[0].meta.notices.length).toBe(1);
expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');

View File

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

View File

@ -162,6 +162,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([
{
target: '1',
title: '1',
query: undefined,
datapoints: [
[10, 1445000010000],
@ -172,6 +173,7 @@ describe('Prometheus Result Transformer', () => {
},
{
target: '2',
title: '2',
query: undefined,
datapoints: [
[10, 1445000010000],
@ -182,6 +184,7 @@ describe('Prometheus Result Transformer', () => {
},
{
target: '3',
title: '3',
query: undefined,
datapoints: [
[10, 1445000010000],
@ -285,6 +288,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([
{
target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
query: undefined,
datapoints: [
[10, 0],
@ -324,6 +328,7 @@ describe('Prometheus Result Transformer', () => {
expect(result).toEqual([
{
target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
query: undefined,
datapoints: [
[null, 0],
@ -335,6 +340,64 @@ describe('Prometheus Result Transformer', () => {
]);
});
it('should use __name__ label as series name', () => {
const response = {
status: 'success',
data: {
resultType: 'matrix',
result: [
{
metric: { __name__: 'test', job: 'testjob' },
values: [
[1, '10'],
[2, '0'],
],
},
],
},
};
const options = {
format: 'timeseries',
step: 1,
start: 0,
end: 2,
};
const result = ctx.resultTransformer.transform({ data: response }, options);
expect(result[0].target).toEqual('test{job="testjob"}');
});
it('should set frame name to undefined if no __name__ label but there are other labels', () => {
const response = {
status: 'success',
data: {
resultType: 'matrix',
result: [
{
metric: { job: 'testjob' },
values: [
[1, '10'],
[2, '0'],
],
},
],
},
};
const options = {
format: 'timeseries',
step: 1,
query: 'Some query',
start: 0,
end: 2,
};
const result = ctx.resultTransformer.transform({ data: response }, options);
expect(result[0].target).toBe('{job="testjob"}');
expect(result[0].tags.job).toEqual('testjob');
});
it('should align null values with step', () => {
const response = {
status: 'success',
@ -356,13 +419,20 @@ describe('Prometheus Result Transformer', () => {
step: 2,
start: 0,
end: 8,
refId: 'A',
meta: { custom: { hello: '1' } },
};
const result = ctx.resultTransformer.transform({ data: response }, options);
expect(result).toEqual([
{
target: 'test{job="testjob"}',
title: 'test{job="testjob"}',
meta: {
custom: { hello: '1' },
},
query: undefined,
refId: 'A',
datapoints: [
[null, 0],
[null, 2000],

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { TimeSeries, FieldType } from '@grafana/data';
import { TimeSeries, FieldType, Labels, formatLabels } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
export class ResultTransformer {
@ -42,9 +42,7 @@ export class ResultTransformer {
transformMetricData(metricData: any, options: any, start: number, end: number) {
const dps = [];
let metricLabel = null;
metricLabel = this.createMetricLabel(metricData.metric, options);
const { name, labels, title } = this.createLabelInfo(metricData.metric, options);
const stepMs = parseFloat(options.step) * 1000;
let baseTimestamp = start * 1000;
@ -76,9 +74,10 @@ export class ResultTransformer {
datapoints: dps,
query: options.query,
refId: options.refId,
target: name,
tags: labels,
title,
meta: options.meta,
target: metricLabel,
tags: metricData.metric,
};
}
@ -142,23 +141,39 @@ export class ResultTransformer {
transformInstantMetricData(md: any, options: any) {
const dps = [];
let metricLabel = null;
metricLabel = this.createMetricLabel(md.metric, options);
const { name, labels } = this.createLabelInfo(md.metric, options);
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
return { target: metricLabel, datapoints: dps, tags: md.metric, refId: options.refId, meta: options.meta };
return { target: name, datapoints: dps, tags: labels, refId: options.refId, meta: options.meta };
}
createMetricLabel(labelData: { [key: string]: string }, options: any) {
let label = '';
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
label = this.getOriginalMetricName(labelData);
} else {
label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData);
createLabelInfo(labels: { [key: string]: string }, options: any): { name?: string; labels: Labels; title?: string } {
if (options?.legendFormat) {
const title = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labels);
return { name: title, title, labels };
}
if (!label || label === '{}') {
label = options.query;
let { __name__, ...labelsWithoutName } = labels;
let title = __name__ || '';
const labelPart = formatLabels(labelsWithoutName);
if (!title && !labelPart) {
title = options.query;
}
return label;
title = `${__name__ ?? ''}${labelPart}`;
return { name: title, title, labels: labelsWithoutName };
}
getOriginalMetricName(labelData: { [key: string]: string }) {
const metricName = labelData.__name__ || '';
delete labelData.__name__;
const labelPart = Object.entries(labelData)
.map(label => `${label[0]}="${label[1]}"`)
.join(',');
return `${metricName}{${labelPart}}`;
}
renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {
@ -171,15 +186,6 @@ export class ResultTransformer {
});
}
getOriginalMetricName(labelData: { [key: string]: string }) {
const metricName = labelData.__name__ || '';
delete labelData.__name__;
const labelPart = _.map(_.toPairs(labelData), label => {
return label[0] + '="' + label[1] + '"';
}).join(',');
return metricName + '{' + labelPart + '}';
}
transformToHistogramOverTime(seriesList: TimeSeries[]) {
/* t1 = timestamp1, t2 = timestamp2 etc.
t1 t2 t3 t1 t2 t3

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { SingleStatCtrl, ShowData } from '../module';
import { dateTime, ReducerID } from '@grafana/data';
import { dateTime, ReducerID, getFieldTitle } from '@grafana/data';
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
import { LegacyResponseData } from '@grafana/data';
import { DashboardModel } from 'app/features/dashboard/state';
@ -90,7 +90,8 @@ describe('SingleStatCtrl', () => {
});
it('Should use series avg as default main value', () => {
expect(ctx.data.value).toBe('test.cpu1');
const title = getFieldTitle(ctx.data.field);
expect(title).toBe('test.cpu1');
});
it('should set formatted value', () => {

View File

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