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 10:42:03 +02:00
committed by GitHub
co-authored by bergquist Kyle Brandt Marcus Efraimsson Andrej Ocenas Hugo Häggmark Tobias Skarhed Arve Knudsen Leonard Gram Ivana Huckova Alexander Zobnin Torkel Ödegaard Marcus Andersson Dominik Prokop Alex Khomenko Richard Hartmann Daniel Lee Joe Elliott Diana Payton Mario Trangoni
parent 184941eab4
commit 5dca59f720
47 changed files with 912 additions and 368 deletions
@@ -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: [],
@@ -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 {};
};
+13 -27
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,
@@ -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,
{
@@ -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,
};
});
}
@@ -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"}');
});
});
@@ -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;
}
+1
View File
@@ -7,3 +7,4 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
export { getFieldTitle, getFrameDisplayTitle } from './fieldState';
@@ -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}`;
};
@@ -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'],
@@ -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;
}
@@ -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) ?? '');
};
},
@@ -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,
},
@@ -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);
}
@@ -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,
@@ -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) {
@@ -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]),
},
@@ -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]),
},
@@ -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,
},
};
});
};
@@ -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' },
},
];
@@ -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;
}
+4
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;
+23 -5
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';
@@ -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',
};
@@ -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);
},
@@ -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);
@@ -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}
@@ -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.",
};
+5
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';
-5
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';
@@ -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})`,
};
});
@@ -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);
@@ -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();
});
@@ -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) => {
@@ -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);
@@ -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);
@@ -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);
@@ -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;
}
@@ -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');
@@ -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"}');
});
});
@@ -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],
@@ -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
@@ -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];
}
@@ -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,
@@ -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;
@@ -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', () => {
@@ -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,
};
});