mirror of https://github.com/grafana/grafana.git
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:
parent
184941eab4
commit
5dca59f720
|
|
@ -22,7 +22,8 @@ describe('toDataFrame', () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
let series = toDataFrame(input1);
|
let series = toDataFrame(input1);
|
||||||
expect(series.fields[1].name).toBe(input1.target);
|
expect(series.name).toBe(input1.target);
|
||||||
|
expect(series.fields[1].name).toBe('Value');
|
||||||
|
|
||||||
const v0 = series.fields[0].values;
|
const v0 = series.fields[0].values;
|
||||||
const v1 = series.fields[1].values;
|
const v1 = series.fields[1].values;
|
||||||
|
|
@ -182,6 +183,24 @@ describe('SerisData backwards compatibility', () => {
|
||||||
expect(roundtrip.target).toBe(timeseries.target);
|
expect(roundtrip.target).toBe(timeseries.target);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can convert TimeSeries to series and back again with tags should render name with tags', () => {
|
||||||
|
const timeseries = {
|
||||||
|
target: 'Series A',
|
||||||
|
tags: { server: 'ServerA', job: 'app' },
|
||||||
|
datapoints: [
|
||||||
|
[100, 1],
|
||||||
|
[200, 2],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const series = toDataFrame(timeseries);
|
||||||
|
expect(isDataFrame(timeseries)).toBeFalsy();
|
||||||
|
expect(isDataFrame(series)).toBeTruthy();
|
||||||
|
|
||||||
|
const roundtrip = toLegacyResponseData(series) as TimeSeries;
|
||||||
|
expect(isDataFrame(roundtrip)).toBeFalsy();
|
||||||
|
expect(roundtrip.target).toBe('{job="app", server="ServerA"}');
|
||||||
|
});
|
||||||
|
|
||||||
it('can convert empty table to DataFrame then back to legacy', () => {
|
it('can convert empty table to DataFrame then back to legacy', () => {
|
||||||
const table = {
|
const table = {
|
||||||
columns: [],
|
columns: [],
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ import {
|
||||||
TimeSeriesValue,
|
TimeSeriesValue,
|
||||||
FieldDTO,
|
FieldDTO,
|
||||||
DataFrameDTO,
|
DataFrameDTO,
|
||||||
|
TIME_SERIES_FIELD_NAME,
|
||||||
} from '../types/index';
|
} from '../types/index';
|
||||||
import { isDateTime } from '../datetime/moment_wrapper';
|
import { isDateTime } from '../datetime/moment_wrapper';
|
||||||
import { ArrayVector } from '../vector/ArrayVector';
|
import { ArrayVector } from '../vector/ArrayVector';
|
||||||
import { MutableDataFrame } from './MutableDataFrame';
|
import { MutableDataFrame } from './MutableDataFrame';
|
||||||
import { SortedVector } from '../vector/SortedVector';
|
import { SortedVector } from '../vector/SortedVector';
|
||||||
import { ArrayDataFrame } from './ArrayDataFrame';
|
import { ArrayDataFrame } from './ArrayDataFrame';
|
||||||
|
import { getFieldTitle } from '../field/fieldState';
|
||||||
|
|
||||||
function convertTableToDataFrame(table: TableData): DataFrame {
|
function convertTableToDataFrame(table: TableData): DataFrame {
|
||||||
const fields = table.columns.map(c => {
|
const fields = table.columns.map(c => {
|
||||||
|
|
@ -61,6 +63,7 @@ function convertTableToDataFrame(table: TableData): DataFrame {
|
||||||
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||||
const times: number[] = [];
|
const times: number[] = [];
|
||||||
const values: TimeSeriesValue[] = [];
|
const values: TimeSeriesValue[] = [];
|
||||||
|
|
||||||
for (const point of timeSeries.datapoints) {
|
for (const point of timeSeries.datapoints) {
|
||||||
values.push(point[0]);
|
values.push(point[0]);
|
||||||
times.push(point[1] as number);
|
times.push(point[1] as number);
|
||||||
|
|
@ -74,7 +77,7 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||||
values: new ArrayVector<number>(times),
|
values: new ArrayVector<number>(times),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: timeSeries.target || 'Value',
|
name: TIME_SERIES_FIELD_NAME,
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
config: {
|
config: {
|
||||||
unit: timeSeries.unit,
|
unit: timeSeries.unit,
|
||||||
|
|
@ -84,6 +87,10 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (timeSeries.title) {
|
||||||
|
(fields[1].config as FieldConfig).title = timeSeries.title;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: timeSeries.target,
|
name: timeSeries.target,
|
||||||
refId: timeSeries.refId,
|
refId: timeSeries.refId,
|
||||||
|
|
@ -111,7 +118,7 @@ function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
|
||||||
name: graphSeries.label,
|
name: graphSeries.label,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: graphSeries.label || 'Value',
|
name: graphSeries.label || TIME_SERIES_FIELD_NAME,
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
config: {},
|
config: {},
|
||||||
values: x,
|
values: x,
|
||||||
|
|
@ -312,18 +319,20 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
|
||||||
const { timeField, timeIndex } = getTimeField(frame);
|
const { timeField, timeIndex } = getTimeField(frame);
|
||||||
if (timeField) {
|
if (timeField) {
|
||||||
const valueIndex = timeIndex === 0 ? 1 : 0;
|
const valueIndex = timeIndex === 0 ? 1 : 0;
|
||||||
|
const valueField = fields[valueIndex];
|
||||||
|
const timeField = fields[timeIndex!];
|
||||||
|
|
||||||
// Make sure it is [value,time]
|
// Make sure it is [value,time]
|
||||||
for (let i = 0; i < rowCount; i++) {
|
for (let i = 0; i < rowCount; i++) {
|
||||||
rows.push([
|
rows.push([
|
||||||
fields[valueIndex].values.get(i), // value
|
valueField.values.get(i), // value
|
||||||
fields[timeIndex!].values.get(i), // time
|
timeField.values.get(i), // time
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alias: fields[valueIndex].name || frame.name,
|
alias: frame.name,
|
||||||
target: fields[valueIndex].name || frame.name,
|
target: getFieldTitle(valueField, frame),
|
||||||
datapoints: rows,
|
datapoints: rows,
|
||||||
unit: fields[0].config ? fields[0].config.unit : undefined,
|
unit: fields[0].config ? fields[0].config.unit : undefined,
|
||||||
refId: frame.refId,
|
refId: frame.refId,
|
||||||
|
|
@ -432,18 +441,6 @@ export function reverseDataFrame(data: DataFrame): DataFrame {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTimeField = (series: DataFrame): { timeField?: Field; timeIndex?: number } => {
|
|
||||||
for (let i = 0; i < series.fields.length; i++) {
|
|
||||||
if (series.fields[i].type === FieldType.time) {
|
|
||||||
return {
|
|
||||||
timeField: series.fields[i],
|
|
||||||
timeIndex: i,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper to get an array from each field value
|
* Wrapper to get an array from each field value
|
||||||
*/
|
*/
|
||||||
|
|
@ -487,3 +484,15 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTimeField = (series: DataFrame): { timeField?: Field; timeIndex?: number } => {
|
||||||
|
for (let i = 0; i < series.fields.length; i++) {
|
||||||
|
if (series.fields[i].type === FieldType.time) {
|
||||||
|
return {
|
||||||
|
timeField: series.fields[i],
|
||||||
|
timeIndex: i,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,35 +37,17 @@ export interface ReduceDataOptions {
|
||||||
// TODO: use built in variables, same as for data links?
|
// TODO: use built in variables, same as for data links?
|
||||||
export const VAR_SERIES_NAME = '__series.name';
|
export const VAR_SERIES_NAME = '__series.name';
|
||||||
export const VAR_FIELD_NAME = '__field.name';
|
export const VAR_FIELD_NAME = '__field.name';
|
||||||
|
export const VAR_FIELD_LABELS = '__field.labels';
|
||||||
export const VAR_CALC = '__calc';
|
export const VAR_CALC = '__calc';
|
||||||
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
|
export const VAR_CELL_PREFIX = '__cell_'; // consistent with existing table templates
|
||||||
|
|
||||||
function getTitleTemplate(title: string | undefined, stats: string[], data?: DataFrame[]): string {
|
function getTitleTemplate(stats: string[]): string {
|
||||||
// If the title exists, use it as a template variable
|
|
||||||
if (title) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
if (!data || !data.length) {
|
|
||||||
return 'No Data';
|
|
||||||
}
|
|
||||||
|
|
||||||
let fieldCount = 0;
|
|
||||||
for (const field of data[0].fields) {
|
|
||||||
if (field.type === FieldType.number) {
|
|
||||||
fieldCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (stats.length > 1) {
|
if (stats.length > 1) {
|
||||||
parts.push('${' + VAR_CALC + '}');
|
parts.push('${' + VAR_CALC + '}');
|
||||||
}
|
}
|
||||||
if (data.length > 1) {
|
|
||||||
parts.push('${' + VAR_SERIES_NAME + '}');
|
|
||||||
}
|
|
||||||
if (fieldCount > 1 || !parts.length) {
|
|
||||||
parts.push('${' + VAR_FIELD_NAME + '}');
|
parts.push('${' + VAR_FIELD_NAME + '}');
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
@ -108,8 +90,8 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||||
const data = options.data;
|
const data = options.data;
|
||||||
let hitLimit = false;
|
let hitLimit = false;
|
||||||
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||||
const defaultTitle = getTitleTemplate(fieldConfig.defaults.title, calcs, data);
|
|
||||||
const scopedVars: ScopedVars = {};
|
const scopedVars: ScopedVars = {};
|
||||||
|
const defaultTitle = getTitleTemplate(calcs);
|
||||||
|
|
||||||
for (let s = 0; s < data.length && !hitLimit; s++) {
|
for (let s = 0; s < data.length && !hitLimit; s++) {
|
||||||
const series = data[s]; // Name is already set
|
const series = data[s]; // Name is already set
|
||||||
|
|
@ -120,11 +102,14 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||||
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
||||||
const field = series.fields[i];
|
const field = series.fields[i];
|
||||||
const fieldLinksSupplier = field.getLinks;
|
const fieldLinksSupplier = field.getLinks;
|
||||||
// Show all number fields
|
|
||||||
|
// To filter out time field, need an option for this
|
||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = field.config; // already set by the prepare task
|
const config = field.config; // already set by the prepare task
|
||||||
|
const title = field.config.title ?? defaultTitle;
|
||||||
|
|
||||||
const display =
|
const display =
|
||||||
field.display ??
|
field.display ??
|
||||||
|
|
@ -134,7 +119,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = config.title ? config.title : defaultTitle;
|
|
||||||
// Show all rows
|
// Show all rows
|
||||||
if (reduceOptions.values) {
|
if (reduceOptions.values) {
|
||||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||||
|
|
@ -151,9 +135,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayValue = display(field.values.get(j));
|
const displayValue = display(field.values.get(j));
|
||||||
displayValue.title = replaceVariables(title, {
|
displayValue.title = replaceVariables(title, {
|
||||||
...field.config.scopedVars, // series and field scoped vars
|
...field.state?.scopedVars, // series and field scoped vars
|
||||||
...scopedVars,
|
...scopedVars,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -197,9 +182,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||||
const displayValue = display(results[calc]);
|
const displayValue = display(results[calc]);
|
||||||
displayValue.title = replaceVariables(title, {
|
displayValue.title = replaceVariables(title, {
|
||||||
...field.config.scopedVars, // series and field scoped vars
|
...field.state?.scopedVars, // series and field scoped vars
|
||||||
...scopedVars,
|
...scopedVars,
|
||||||
});
|
});
|
||||||
|
|
||||||
values.push({
|
values.push({
|
||||||
name: calc,
|
name: calc,
|
||||||
field: config,
|
field: config,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { Registry } from '../utils';
|
||||||
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
|
import { mockStandardProperties } from '../utils/tests/mockStandardProperties';
|
||||||
import { FieldMatcherID } from '../transformations';
|
import { FieldMatcherID } from '../transformations';
|
||||||
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
||||||
|
import { getFieldTitle } from './fieldState';
|
||||||
|
|
||||||
const property1 = {
|
const property1 = {
|
||||||
id: 'custom.property1', // Match field properties
|
id: 'custom.property1', // Match field properties
|
||||||
|
|
@ -111,12 +112,14 @@ describe('applyFieldOverrides', () => {
|
||||||
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(withOverrides[0].fields[0].config.scopedVars).toMatchInlineSnapshot(`
|
expect(withOverrides[0].fields[0].state!.scopedVars).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"__field": Object {
|
"__field": Object {
|
||||||
"text": "Field",
|
"text": "Field",
|
||||||
"value": Object {
|
"value": Object {
|
||||||
"name": "message",
|
"label": undefined,
|
||||||
|
"labels": "",
|
||||||
|
"name": "A message",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"__series": Object {
|
"__series": Object {
|
||||||
|
|
@ -128,12 +131,14 @@ describe('applyFieldOverrides', () => {
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(withOverrides[1].fields[0].config.scopedVars).toMatchInlineSnapshot(`
|
expect(withOverrides[1].fields[0].state!.scopedVars).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"__field": Object {
|
"__field": Object {
|
||||||
"text": "Field",
|
"text": "Field",
|
||||||
"value": Object {
|
"value": Object {
|
||||||
"name": "info",
|
"label": undefined,
|
||||||
|
"labels": "",
|
||||||
|
"name": "B info",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"__series": Object {
|
"__series": Object {
|
||||||
|
|
@ -152,16 +157,19 @@ describe('applyFieldOverrides', () => {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
const f1 = {
|
const f1 = {
|
||||||
unit: 'ms',
|
unit: 'ms',
|
||||||
dateFormat: '', // should be ignored
|
dateFormat: '', // should be ignored
|
||||||
max: parseFloat('NOPE'), // should be ignored
|
max: parseFloat('NOPE'), // should be ignored
|
||||||
min: null, // should alo be ignored!
|
min: null, // should alo be ignored!
|
||||||
|
title: 'newTitle',
|
||||||
};
|
};
|
||||||
|
|
||||||
const f: DataFrame = toDataFrame({
|
const f: DataFrame = toDataFrame({
|
||||||
fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }],
|
fields: [{ type: FieldType.number, name: 'x', config: field, values: [] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const processed = applyFieldOverrides({
|
const processed = applyFieldOverrides({
|
||||||
data: [f],
|
data: [f],
|
||||||
fieldConfig: {
|
fieldConfig: {
|
||||||
|
|
@ -172,11 +180,13 @@ describe('applyFieldOverrides', () => {
|
||||||
replaceVariables: v => v,
|
replaceVariables: v => v,
|
||||||
theme: {} as GrafanaTheme,
|
theme: {} as GrafanaTheme,
|
||||||
})[0];
|
})[0];
|
||||||
const out = processed.fields[0].config;
|
|
||||||
|
|
||||||
expect(out.min).toEqual(0);
|
const outField = processed.fields[0];
|
||||||
expect(out.max).toEqual(100);
|
|
||||||
expect(out.unit).toEqual('ms');
|
expect(outField.config.min).toEqual(0);
|
||||||
|
expect(outField.config.max).toEqual(100);
|
||||||
|
expect(outField.config.unit).toEqual('ms');
|
||||||
|
expect(getFieldTitle(outField, f)).toEqual('newTitle');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will apply field overrides', () => {
|
it('will apply field overrides', () => {
|
||||||
|
|
@ -300,10 +310,8 @@ describe('setDynamicConfigValue', () => {
|
||||||
it('applies dynamic config values', () => {
|
it('applies dynamic config values', () => {
|
||||||
const config = {
|
const config = {
|
||||||
title: 'test',
|
title: 'test',
|
||||||
// custom: {
|
|
||||||
// property1: 1,
|
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setDynamicConfigValue(
|
setDynamicConfigValue(
|
||||||
config,
|
config,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,15 @@ import set from 'lodash/set';
|
||||||
import unset from 'lodash/unset';
|
import unset from 'lodash/unset';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { getTimeField, guessFieldTypeForField } from '../dataframe';
|
import { guessFieldTypeForField } from '../dataframe';
|
||||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||||
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
||||||
import { DataLinkBuiltInVars, locationUtil } from '../utils';
|
import { DataLinkBuiltInVars, locationUtil } from '../utils';
|
||||||
import { formattedValueToString } from '../valueFormats';
|
import { formattedValueToString } from '../valueFormats';
|
||||||
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
||||||
|
import { formatLabels } from '../utils/labels';
|
||||||
|
import { getFrameDisplayTitle, getFieldTitle } from './fieldState';
|
||||||
|
import { getTimeField } from '../dataframe/processDataFrame';
|
||||||
|
|
||||||
interface OverrideProps {
|
interface OverrideProps {
|
||||||
match: FieldMatcher;
|
match: FieldMatcher;
|
||||||
|
|
@ -96,25 +99,31 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||||
}
|
}
|
||||||
|
|
||||||
return options.data.map((frame, index) => {
|
return options.data.map((frame, index) => {
|
||||||
let name = frame.name;
|
|
||||||
if (!name) {
|
|
||||||
name = `Series[${index}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopedVars: ScopedVars = {
|
const scopedVars: ScopedVars = {
|
||||||
__series: { text: 'Series', value: { name } },
|
__series: { text: 'Series', value: { name: getFrameDisplayTitle(frame, index) } }, // might be missing
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields: Field[] = frame.fields.map((field, fieldIndex) => {
|
const fields: Field[] = frame.fields.map(field => {
|
||||||
// Config is mutable within this scope
|
// Config is mutable within this scope
|
||||||
let fieldName = field.name;
|
|
||||||
if (!fieldName) {
|
|
||||||
fieldName = `Field[${fieldIndex}]`;
|
|
||||||
}
|
|
||||||
const fieldScopedVars = { ...scopedVars };
|
const fieldScopedVars = { ...scopedVars };
|
||||||
fieldScopedVars['__field'] = { text: 'Field', value: { name: fieldName } };
|
const title = getFieldTitle(field, frame, options.data);
|
||||||
|
|
||||||
const config: FieldConfig = { ...field.config, scopedVars: fieldScopedVars } || {};
|
fieldScopedVars['__field'] = {
|
||||||
|
text: 'Field',
|
||||||
|
value: {
|
||||||
|
name: title, // Generally appropriate (may include the series name if useful)
|
||||||
|
labels: formatLabels(field.labels!),
|
||||||
|
label: field.labels,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
field.state = {
|
||||||
|
...field.state,
|
||||||
|
title: title,
|
||||||
|
scopedVars: fieldScopedVars,
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: FieldConfig = { ...field.config };
|
||||||
const context = {
|
const context = {
|
||||||
field,
|
field,
|
||||||
data: options.data!,
|
data: options.data!,
|
||||||
|
|
@ -183,6 +192,10 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||||
...field,
|
...field,
|
||||||
config,
|
config,
|
||||||
type,
|
type,
|
||||||
|
state: {
|
||||||
|
...field.state,
|
||||||
|
title: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// and set the display processor using it
|
// and set the display processor using it
|
||||||
|
|
@ -204,7 +217,6 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||||
return {
|
return {
|
||||||
...frame,
|
...frame,
|
||||||
fields,
|
fields,
|
||||||
name,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -7,3 +7,4 @@ export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
||||||
|
|
||||||
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
|
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
|
||||||
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
||||||
|
export { getFieldTitle, getFrameDisplayTitle } from './fieldState';
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const stringOverrideProcessor = (
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (settings && settings.expandTemplateVars && context.replaceVariables) {
|
if (settings && settings.expandTemplateVars && context.replaceVariables) {
|
||||||
return context.replaceVariables(value, context.field!.config.scopedVars);
|
return context.replaceVariables(value, context.field!.state!.scopedVars);
|
||||||
}
|
}
|
||||||
return `${value}`;
|
return `${value}`;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ describe('Stats Calculators', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support a single stat also', () => {
|
it('should support a single stat also', () => {
|
||||||
basicTable.fields[0].calcs = undefined; // clear the cache
|
basicTable.fields[0].state = undefined; // clear the cache
|
||||||
const stats = reduceField({
|
const stats = reduceField({
|
||||||
field: basicTable.fields[0],
|
field: basicTable.fields[0],
|
||||||
reducers: ['first'],
|
reducers: ['first'],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Libraries
|
// Libraries
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
|
||||||
import { NullValueMode, Field } from '../types/index';
|
import { NullValueMode, Field, FieldState, FieldCalcs } from '../types/index';
|
||||||
import { Registry, RegistryItem } from '../utils/Registry';
|
import { Registry, RegistryItem } from '../utils/Registry';
|
||||||
|
|
||||||
export enum ReducerID {
|
export enum ReducerID {
|
||||||
|
|
@ -28,10 +28,6 @@ export enum ReducerID {
|
||||||
allIsNull = 'allIsNull',
|
allIsNull = 'allIsNull',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldCalcs {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal function
|
// Internal function
|
||||||
type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
type FieldReducer = (field: Field, ignoreNulls: boolean, nullAsZero: boolean) => FieldCalcs;
|
||||||
|
|
||||||
|
|
@ -57,20 +53,23 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.calcs) {
|
if (field.state?.calcs) {
|
||||||
// Find the values we need to calculate
|
// Find the values we need to calculate
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
for (const s of reducers) {
|
for (const s of reducers) {
|
||||||
if (!field.calcs.hasOwnProperty(s)) {
|
if (!field.state.calcs.hasOwnProperty(s)) {
|
||||||
missing.push(s);
|
missing.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (missing.length < 1) {
|
if (missing.length < 1) {
|
||||||
return {
|
return {
|
||||||
...field.calcs,
|
...field.state.calcs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!field.state) {
|
||||||
|
field.state = {} as FieldState;
|
||||||
|
}
|
||||||
|
|
||||||
const queue = fieldReducers.list(reducers);
|
const queue = fieldReducers.list(reducers);
|
||||||
|
|
||||||
|
|
@ -78,11 +77,11 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||||
// This lets the concrete implementations assume at least one row
|
// This lets the concrete implementations assume at least one row
|
||||||
const data = field.values;
|
const data = field.values;
|
||||||
if (data.length < 1) {
|
if (data.length < 1) {
|
||||||
const calcs = { ...field.calcs } as FieldCalcs;
|
const calcs = { ...field.state.calcs } as FieldCalcs;
|
||||||
for (const reducer of queue) {
|
for (const reducer of queue) {
|
||||||
calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
|
calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null;
|
||||||
}
|
}
|
||||||
return (field.calcs = calcs);
|
return (field.state.calcs = calcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nullValueMode } = field.config;
|
const { nullValueMode } = field.config;
|
||||||
|
|
@ -92,8 +91,8 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||||
// Avoid calculating all the standard stats if possible
|
// Avoid calculating all the standard stats if possible
|
||||||
if (queue.length === 1 && queue[0].reduce) {
|
if (queue.length === 1 && queue[0].reduce) {
|
||||||
const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
|
const values = queue[0].reduce(field, ignoreNulls, nullAsZero);
|
||||||
field.calcs = {
|
field.state.calcs = {
|
||||||
...field.calcs,
|
...field.state.calcs,
|
||||||
...values,
|
...values,
|
||||||
};
|
};
|
||||||
return values;
|
return values;
|
||||||
|
|
@ -111,11 +110,10 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
field.calcs = {
|
field.state.calcs = {
|
||||||
...field.calcs,
|
...field.state.calcs,
|
||||||
...values,
|
...values,
|
||||||
};
|
};
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Field, DataFrame } from '../../types/dataFrame';
|
||||||
import { FieldMatcherID, FrameMatcherID } from './ids';
|
import { FieldMatcherID, FrameMatcherID } from './ids';
|
||||||
import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations';
|
import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations';
|
||||||
import { stringToJsRegex } from '../../text/string';
|
import { stringToJsRegex } from '../../text/string';
|
||||||
|
import { getFieldTitle } from '../../field/fieldState';
|
||||||
|
|
||||||
// General Field matcher
|
// General Field matcher
|
||||||
const fieldNameMacher: FieldMatcherInfo<string> = {
|
const fieldNameMacher: FieldMatcherInfo<string> = {
|
||||||
|
|
@ -18,7 +19,7 @@ const fieldNameMacher: FieldMatcherInfo<string> = {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
return (field: Field) => {
|
return (field: Field) => {
|
||||||
return regex.test(field.name);
|
return regex.test(getFieldTitle(field) ?? '');
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,18 +43,18 @@ describe('calculateField transformer w/ timeseries', () => {
|
||||||
expect(rows).toMatchInlineSnapshot(`
|
expect(rows).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"A {0}": 1,
|
"A": 1,
|
||||||
"B {1}": 2,
|
"B": 2,
|
||||||
"C {1}": 3,
|
"C": 3,
|
||||||
"D {1}": "first",
|
"D": "first",
|
||||||
"The Total": 6,
|
"The Total": 6,
|
||||||
"TheTime": 1000,
|
"TheTime": 1000,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"A {0}": 100,
|
"A": 100,
|
||||||
"B {1}": 200,
|
"B": 200,
|
||||||
"C {1}": 300,
|
"C": 300,
|
||||||
"D {1}": "second",
|
"D": "second",
|
||||||
"The Total": 600,
|
"The Total": 600,
|
||||||
"TheTime": 2000,
|
"TheTime": 2000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { RowVector } from '../../vector/RowVector';
|
||||||
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
|
import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector';
|
||||||
import { doStandardCalcs } from '../fieldReducer';
|
import { doStandardCalcs } from '../fieldReducer';
|
||||||
import { seriesToColumnsTransformer } from './seriesToColumns';
|
import { seriesToColumnsTransformer } from './seriesToColumns';
|
||||||
import { getTimeField } from '../../dataframe';
|
import { getTimeField } from '../../dataframe/processDataFrame';
|
||||||
import defaults from 'lodash/defaults';
|
import defaults from 'lodash/defaults';
|
||||||
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
|
import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators';
|
||||||
|
|
||||||
|
|
@ -168,7 +168,6 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
|
||||||
|
|
||||||
for (let i = 0; i < frame.length; i++) {
|
for (let i = 0; i < frame.length; i++) {
|
||||||
iter.rowIndex = i;
|
iter.rowIndex = i;
|
||||||
row.calcs = undefined; // bust the cache (just in case)
|
|
||||||
const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer];
|
const val = reducer(row, ignoreNulls, nullAsZero)[options.reducer];
|
||||||
vals.push(val);
|
vals.push(val);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,38 +43,6 @@ describe('Labels as Columns', () => {
|
||||||
expect(result[0].fields).toEqual(expected);
|
expect(result[0].fields).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('data frames where frame name is same as value field name replace field name with name Value', () => {
|
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
|
||||||
id: DataTransformerID.labelsToFields,
|
|
||||||
options: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const oneValueOneLabelA = toDataFrame({
|
|
||||||
name: 'A',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [1000] },
|
|
||||||
{ name: 'A', type: FieldType.number, values: [1], labels: { location: 'inside' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const oneValueOneLabelB = toDataFrame({
|
|
||||||
name: 'B',
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [2000] },
|
|
||||||
{ name: 'B', type: FieldType.number, values: [-1], labels: { location: 'outside' } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDataFrame([cfg], [oneValueOneLabelA, oneValueOneLabelB]);
|
|
||||||
const expected: Field[] = [
|
|
||||||
{ name: 'time', type: FieldType.time, values: new ArrayVector([1000, 2000]), config: {} },
|
|
||||||
{ name: 'location', type: FieldType.string, values: new ArrayVector(['inside', 'outside']), config: {} },
|
|
||||||
{ name: 'Value', type: FieldType.number, values: new ArrayVector([1, -1]), config: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(result[0].fields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('data frame with 2 values and 1 label', () => {
|
it('data frame with 2 values and 1 label', () => {
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
id: DataTransformerID.labelsToFields,
|
id: DataTransformerID.labelsToFields,
|
||||||
|
|
|
||||||
|
|
@ -73,22 +73,13 @@ function getFramesWithOnlyValueFields(data: DataFrame[]): DataFrame[] {
|
||||||
|
|
||||||
for (let i = 0; i < series.fields.length; i++) {
|
for (let i = 0; i < series.fields.length; i++) {
|
||||||
const field = series.fields[i];
|
const field = series.fields[i];
|
||||||
|
|
||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we transform a time series to DataFrame we put series name in field name.
|
|
||||||
// This casues problems for this transformer that want all time series values in a Value column
|
|
||||||
// So here we change field names that have same name as DataFrame to just Value
|
|
||||||
if (field.name === series.name) {
|
|
||||||
fields.push({
|
|
||||||
...field,
|
|
||||||
name: 'Value',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,23 @@ describe('OrganizeFields Transformer', () => {
|
||||||
expect(organized.fields).toEqual([
|
expect(organized.fields).toEqual([
|
||||||
{
|
{
|
||||||
config: {},
|
config: {},
|
||||||
|
labels: undefined,
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'renamed_humidity',
|
title: 'renamed_humidity',
|
||||||
|
},
|
||||||
|
labels: undefined,
|
||||||
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'renamed_humidity',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||||
},
|
},
|
||||||
|
|
@ -93,14 +103,24 @@ describe('OrganizeFields Transformer', () => {
|
||||||
|
|
||||||
expect(organized.fields).toEqual([
|
expect(organized.fields).toEqual([
|
||||||
{
|
{
|
||||||
config: {},
|
labels: undefined,
|
||||||
name: 'renamed_time',
|
config: {
|
||||||
|
title: 'renamed_time',
|
||||||
|
},
|
||||||
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'renamed_time',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {},
|
||||||
|
labels: undefined,
|
||||||
name: 'pressure',
|
name: 'pressure',
|
||||||
|
state: {
|
||||||
|
title: 'pressure',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,38 @@ describe('Rename Transformer', () => {
|
||||||
|
|
||||||
expect(renamed.fields).toEqual([
|
expect(renamed.fields).toEqual([
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'Total time',
|
title: 'Total time',
|
||||||
|
},
|
||||||
|
labels: undefined,
|
||||||
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'Total time',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'how cold is it?',
|
title: 'how cold is it?',
|
||||||
|
},
|
||||||
|
labels: undefined,
|
||||||
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'how cold is it?',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'Moistiness',
|
title: 'Moistiness',
|
||||||
|
},
|
||||||
|
name: 'humidity',
|
||||||
|
labels: undefined,
|
||||||
|
state: {
|
||||||
|
title: 'Moistiness',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||||
},
|
},
|
||||||
|
|
@ -87,20 +105,36 @@ describe('Rename Transformer', () => {
|
||||||
|
|
||||||
expect(renamed.fields).toEqual([
|
expect(renamed.fields).toEqual([
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'ttl',
|
title: 'ttl',
|
||||||
|
},
|
||||||
|
name: 'time',
|
||||||
|
labels: undefined,
|
||||||
|
state: {
|
||||||
|
title: 'ttl',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {},
|
||||||
|
labels: undefined,
|
||||||
name: 'pressure',
|
name: 'pressure',
|
||||||
|
state: {
|
||||||
|
title: 'pressure',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {},
|
config: {
|
||||||
name: 'hum',
|
title: 'hum',
|
||||||
|
},
|
||||||
|
labels: undefined,
|
||||||
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'hum',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { DataTransformerInfo } from '../../types/transformations';
|
import { DataTransformerInfo } from '../../types/transformations';
|
||||||
import { DataFrame, Field } from '../..';
|
import { DataFrame, Field } from '../../types/dataFrame';
|
||||||
|
import { getFieldTitle } from '../../field/fieldState';
|
||||||
|
|
||||||
export interface RenameFieldsTransformerOptions {
|
export interface RenameFieldsTransformerOptions {
|
||||||
renameByName: Record<string, string>;
|
renameByName: Record<string, string>;
|
||||||
|
|
@ -28,19 +29,20 @@ export const renameFieldsTransformer: DataTransformerInfo<RenameFieldsTransforme
|
||||||
|
|
||||||
return data.map(frame => ({
|
return data.map(frame => ({
|
||||||
...frame,
|
...frame,
|
||||||
fields: renamer(frame.fields),
|
fields: renamer(frame),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]): Field[] => {
|
const createRenamer = (renameByName: Record<string, string>) => (frame: DataFrame): Field[] => {
|
||||||
if (!renameByName || Object.keys(renameByName).length === 0) {
|
if (!renameByName || Object.keys(renameByName).length === 0) {
|
||||||
return fields;
|
return frame.fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map(field => {
|
return frame.fields.map(field => {
|
||||||
const renameTo = renameByName[field.name];
|
const title = getFieldTitle(field, frame);
|
||||||
|
const renameTo = renameByName[title];
|
||||||
|
|
||||||
if (typeof renameTo !== 'string' || renameTo.length === 0) {
|
if (typeof renameTo !== 'string' || renameTo.length === 0) {
|
||||||
return field;
|
return field;
|
||||||
|
|
@ -48,7 +50,14 @@ const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
name: renameTo,
|
config: {
|
||||||
|
...field.config,
|
||||||
|
title: renameTo,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
...field.state,
|
||||||
|
title: renameTo,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ describe('SeriesToColumns Transformer', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockTransformationsRegistry([seriesToColumnsTransformer]);
|
mockTransformationsRegistry([seriesToColumnsTransformer]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const everySecondSeries = toDataFrame({
|
const everySecondSeries = toDataFrame({
|
||||||
name: 'even',
|
name: 'even',
|
||||||
fields: [
|
fields: [
|
||||||
|
|
@ -44,38 +45,53 @@ describe('SeriesToColumns Transformer', () => {
|
||||||
expect(filtered.fields).toEqual([
|
expect(filtered.fields).toEqual([
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'time',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
|
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even,odd' },
|
labels: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature {even}',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature even',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
|
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {even}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity even',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
|
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature {odd}',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature odd',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
|
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {odd}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity odd',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
|
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -92,38 +108,53 @@ describe('SeriesToColumns Transformer', () => {
|
||||||
expect(filtered.fields).toEqual([
|
expect(filtered.fields).toEqual([
|
||||||
{
|
{
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]),
|
values: new ArrayVector([10.3, 10.4, 10.5, 10.6, 11.1, 11.3, 11.5, 11.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even,odd' },
|
labels: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'time {even}',
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'time even',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]),
|
values: new ArrayVector([3000, 4000, 5000, 6000, null, null, null, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {even}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity even',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]),
|
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6, null, null, null, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'time {odd}',
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'time odd',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]),
|
values: new ArrayVector([null, null, null, null, 1000, 3000, 5000, 7000]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {odd}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity odd',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]),
|
values: new ArrayVector([null, null, null, null, 11000.1, 11000.3, 11000.5, 11000.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -144,38 +175,53 @@ describe('SeriesToColumns Transformer', () => {
|
||||||
expect(filtered.fields).toEqual([
|
expect(filtered.fields).toEqual([
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'time',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
|
values: new ArrayVector([1000, 3000, 4000, 5000, 6000, 7000]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even,odd' },
|
labels: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature {even}',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature even',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
|
values: new ArrayVector([null, 10.3, 10.4, 10.5, 10.6, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {even}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity even',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
|
values: new ArrayVector([null, 10000.3, 10000.4, 10000.5, 10000.6, null]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'even' },
|
labels: { name: 'even' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature {odd}',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature odd',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
|
values: new ArrayVector([11.1, 11.3, null, 11.5, null, 11.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'humidity {odd}',
|
name: 'humidity',
|
||||||
|
state: {
|
||||||
|
title: 'humidity odd',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
|
values: new ArrayVector([11000.1, 11000.3, null, 11000.5, null, 11000.7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'odd' },
|
labels: { name: 'odd' },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -209,24 +255,33 @@ describe('SeriesToColumns Transformer', () => {
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
{
|
{
|
||||||
name: 'time',
|
name: 'time',
|
||||||
|
state: {
|
||||||
|
title: 'time',
|
||||||
|
},
|
||||||
type: FieldType.time,
|
type: FieldType.time,
|
||||||
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'temperature,B' },
|
labels: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([1, 3, 5, 7]),
|
values: new ArrayVector([1, 3, 5, 7]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'temperature' },
|
state: {
|
||||||
|
title: 'temperature temperature',
|
||||||
|
},
|
||||||
|
labels: { name: 'temperature' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'temperature {B}',
|
name: 'temperature',
|
||||||
|
state: {
|
||||||
|
title: 'temperature B',
|
||||||
|
},
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector([2, 4, 6, 8]),
|
values: new ArrayVector([2, 4, 6, 8]),
|
||||||
config: {},
|
config: {},
|
||||||
labels: { origin: 'B' },
|
labels: { name: 'B' },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,81 @@
|
||||||
import { DataFrame, DataTransformerInfo } from '../../types';
|
import { DataFrame, DataTransformerInfo, Field } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { MutableDataFrame } from '../../dataframe';
|
import { MutableDataFrame } from '../../dataframe';
|
||||||
import { filterFieldsByNameTransformer } from './filterByName';
|
|
||||||
import { ArrayVector } from '../../vector';
|
import { ArrayVector } from '../../vector';
|
||||||
|
import { getFieldTitle } from '../../field/fieldState';
|
||||||
|
|
||||||
export interface SeriesToColumnsOptions {
|
export interface SeriesToColumnsOptions {
|
||||||
byField?: string;
|
byField?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_KEY_FIELD = 'Time';
|
||||||
|
|
||||||
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
|
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
|
||||||
id: DataTransformerID.seriesToColumns,
|
id: DataTransformerID.seriesToColumns,
|
||||||
name: 'Series as columns',
|
name: 'Series as columns',
|
||||||
description: 'Groups series by field and returns values as columns',
|
description: 'Groups series by field and returns values as columns',
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
byField: 'Time',
|
byField: DEFAULT_KEY_FIELD,
|
||||||
},
|
},
|
||||||
transformer: options => (data: DataFrame[]) => {
|
transformer: options => (data: DataFrame[]) => {
|
||||||
const optionsArray = options.byField ? [options.byField] : [];
|
const keyFieldMatch = options.byField || DEFAULT_KEY_FIELD;
|
||||||
// not sure if I should use filterFieldsByNameTransformer to get the key field
|
const allFields: FieldsToProcess[] = [];
|
||||||
const keyDataFrames = filterFieldsByNameTransformer.transformer({
|
|
||||||
include: optionsArray,
|
for (let frameIndex = 0; frameIndex < data.length; frameIndex++) {
|
||||||
})(data);
|
const frame = data[frameIndex];
|
||||||
if (!keyDataFrames.length) {
|
const keyField = findKeyField(frame, keyFieldMatch);
|
||||||
// for now we only parse data frames with 2 fields
|
|
||||||
|
if (!keyField) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not sure if I should use filterFieldsByNameTransformer to get the other fields
|
|
||||||
const otherDataFrames = filterFieldsByNameTransformer.transformer({
|
|
||||||
exclude: optionsArray,
|
|
||||||
})(data);
|
|
||||||
if (!otherDataFrames.length) {
|
|
||||||
// for now we only parse data frames with 2 fields
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processed = new MutableDataFrame();
|
|
||||||
const origins: string[] = [];
|
|
||||||
for (let frameIndex = 0; frameIndex < keyDataFrames.length; frameIndex++) {
|
|
||||||
const frame = keyDataFrames[frameIndex];
|
|
||||||
const origin = getOrigin(frame, frameIndex);
|
|
||||||
origins.push(origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
processed.addField({
|
|
||||||
...keyDataFrames[0].fields[0],
|
|
||||||
values: new ArrayVector([]),
|
|
||||||
labels: { origin: origins.join(',') },
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let frameIndex = 0; frameIndex < otherDataFrames.length; frameIndex++) {
|
|
||||||
const frame = otherDataFrames[frameIndex];
|
|
||||||
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
|
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
|
||||||
const field = frame.fields[fieldIndex];
|
const sourceField = frame.fields[fieldIndex];
|
||||||
const origin = getOrigin(frame, frameIndex);
|
|
||||||
const name = getColumnName(otherDataFrames, frameIndex, fieldIndex, false);
|
if (sourceField === keyField) {
|
||||||
if (processed.fields.find(field => field.name === name)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
processed.addField({ ...field, name, values: new ArrayVector([]), labels: { origin } });
|
|
||||||
|
let labels = sourceField.labels ?? {};
|
||||||
|
|
||||||
|
if (frame.name) {
|
||||||
|
labels = { ...labels, name: frame.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
allFields.push({
|
||||||
|
keyField,
|
||||||
|
sourceField,
|
||||||
|
newField: {
|
||||||
|
...sourceField,
|
||||||
|
state: null,
|
||||||
|
values: new ArrayVector([]),
|
||||||
|
labels,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no key fields or more than one value field
|
||||||
|
if (allFields.length <= 1) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultFrame = new MutableDataFrame();
|
||||||
|
|
||||||
|
resultFrame.addField({
|
||||||
|
...allFields[0].keyField,
|
||||||
|
values: new ArrayVector([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of allFields) {
|
||||||
|
resultFrame.addField(item.newField);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyFieldTitle = getFieldTitle(resultFrame.fields[0], resultFrame);
|
||||||
const byKeyField: { [key: string]: { [key: string]: any } } = {};
|
const byKeyField: { [key: string]: { [key: string]: any } } = {};
|
||||||
// this loop creates a dictionary object that groups the key fields values
|
|
||||||
/*
|
/*
|
||||||
|
this loop creates a dictionary object that groups the key fields values
|
||||||
{
|
{
|
||||||
"key field first value as string" : {
|
"key field first value as string" : {
|
||||||
"key field name": key field first value,
|
"key field name": key field first value,
|
||||||
|
|
@ -78,25 +89,19 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
for (let seriesIndex = 0; seriesIndex < keyDataFrames.length; seriesIndex++) {
|
|
||||||
const keyDataFrame = keyDataFrames[seriesIndex];
|
for (let fieldIndex = 0; fieldIndex < allFields.length; fieldIndex++) {
|
||||||
const keyField = keyDataFrame.fields[0];
|
const { sourceField, keyField, newField } = allFields[fieldIndex];
|
||||||
const keyColumnName = getColumnName(keyDataFrames, seriesIndex, 0, true);
|
const newFieldTitle = getFieldTitle(newField, resultFrame);
|
||||||
const keyValues = keyField.values;
|
|
||||||
for (let valueIndex = 0; valueIndex < keyValues.length; valueIndex++) {
|
for (let valueIndex = 0; valueIndex < sourceField.values.length; valueIndex++) {
|
||||||
const keyValue = keyValues.get(valueIndex);
|
const value = sourceField.values.get(valueIndex);
|
||||||
const keyValueAsString = keyValue.toString();
|
const keyValue = keyField.values.get(valueIndex);
|
||||||
if (!byKeyField[keyValueAsString]) {
|
|
||||||
byKeyField[keyValueAsString] = { [keyColumnName]: keyValue };
|
if (!byKeyField[keyValue]) {
|
||||||
}
|
byKeyField[keyValue] = { [newFieldTitle]: value, [keyFieldTitle]: keyValue };
|
||||||
const otherDataFrame = otherDataFrames[seriesIndex];
|
} else {
|
||||||
for (let otherIndex = 0; otherIndex < otherDataFrame.fields.length; otherIndex++) {
|
byKeyField[keyValue][newFieldTitle] = value;
|
||||||
const otherColumnName = getColumnName(otherDataFrames, seriesIndex, otherIndex, false);
|
|
||||||
const otherField = otherDataFrame.fields[otherIndex];
|
|
||||||
const otherValue = otherField.values.get(valueIndex);
|
|
||||||
if (!byKeyField[keyValueAsString][otherColumnName]) {
|
|
||||||
byKeyField[keyValueAsString] = { ...byKeyField[keyValueAsString], [otherColumnName]: otherValue };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,27 +109,33 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
|
||||||
const keyValueStrings = Object.keys(byKeyField);
|
const keyValueStrings = Object.keys(byKeyField);
|
||||||
for (let rowIndex = 0; rowIndex < keyValueStrings.length; rowIndex++) {
|
for (let rowIndex = 0; rowIndex < keyValueStrings.length; rowIndex++) {
|
||||||
const keyValueAsString = keyValueStrings[rowIndex];
|
const keyValueAsString = keyValueStrings[rowIndex];
|
||||||
for (let fieldIndex = 0; fieldIndex < processed.fields.length; fieldIndex++) {
|
|
||||||
const field = processed.fields[fieldIndex];
|
for (let fieldIndex = 0; fieldIndex < resultFrame.fields.length; fieldIndex++) {
|
||||||
const value = byKeyField[keyValueAsString][field.name] ?? null;
|
const field = resultFrame.fields[fieldIndex];
|
||||||
|
const otherColumnName = getFieldTitle(field, resultFrame);
|
||||||
|
const value = byKeyField[keyValueAsString][otherColumnName] ?? null;
|
||||||
field.values.add(value);
|
field.values.add(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [processed];
|
return [resultFrame];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColumnName = (frames: DataFrame[], frameIndex: number, fieldIndex: number, isKeyField = false) => {
|
function findKeyField(frame: DataFrame, matchTitle: string): Field | null {
|
||||||
const frame = frames[frameIndex];
|
for (let fieldIndex = 0; fieldIndex < frame.fields.length; fieldIndex++) {
|
||||||
const field = frame.fields[fieldIndex];
|
const field = frame.fields[fieldIndex];
|
||||||
const frameName = frame.name || `${frameIndex}`;
|
|
||||||
const fieldName = field.name;
|
|
||||||
const seriesName = isKeyField ? fieldName : fieldName === frameName ? fieldName : `${fieldName} {${frameName}}`;
|
|
||||||
|
|
||||||
return seriesName;
|
if (matchTitle === getFieldTitle(field)) {
|
||||||
};
|
return field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getOrigin = (frame: DataFrame, index: number) => {
|
return null;
|
||||||
return frame.name || `${index}`;
|
}
|
||||||
};
|
|
||||||
|
interface FieldsToProcess {
|
||||||
|
newField: Field;
|
||||||
|
sourceField: Field;
|
||||||
|
keyField: Field;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,10 @@ export type TimeSeriesPoints = TimeSeriesValue[][];
|
||||||
|
|
||||||
export interface TimeSeries extends QueryResultBase {
|
export interface TimeSeries extends QueryResultBase {
|
||||||
target: string;
|
target: string;
|
||||||
|
/**
|
||||||
|
* If name is manually configured via an alias / legend pattern
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
datapoints: TimeSeriesPoints;
|
datapoints: TimeSeriesPoints;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
tags?: Labels;
|
tags?: Labels;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||||
import { DisplayProcessor, DisplayValue } from './displayValue';
|
import { DisplayProcessor, DisplayValue } from './displayValue';
|
||||||
import { DataLink, LinkModel } from './dataLink';
|
import { DataLink, LinkModel } from './dataLink';
|
||||||
import { Vector } from './vector';
|
import { Vector } from './vector';
|
||||||
import { FieldCalcs } from '../transformations/fieldReducer';
|
|
||||||
import { FieldColor } from './fieldColor';
|
import { FieldColor } from './fieldColor';
|
||||||
import { ScopedVars } from './ScopedVars';
|
import { ScopedVars } from './ScopedVars';
|
||||||
|
|
||||||
|
|
@ -53,8 +52,6 @@ export interface FieldConfig<TOptions extends object = any> {
|
||||||
|
|
||||||
// Panel Specific Values
|
// Panel Specific Values
|
||||||
custom?: TOptions;
|
custom?: TOptions;
|
||||||
|
|
||||||
scopedVars?: ScopedVars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValueLinkConfig {
|
export interface ValueLinkConfig {
|
||||||
|
|
@ -85,9 +82,9 @@ export interface Field<T = any, V = Vector<T>> {
|
||||||
labels?: Labels;
|
labels?: Labels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of reduced values
|
* Cached values with appropriate dispaly and id values
|
||||||
*/
|
*/
|
||||||
calcs?: FieldCalcs;
|
state?: FieldState | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert text to the field value
|
* Convert text to the field value
|
||||||
|
|
@ -105,6 +102,23 @@ export interface Field<T = any, V = Vector<T>> {
|
||||||
getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>;
|
getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FieldState {
|
||||||
|
/**
|
||||||
|
* An appropriate name for the field (does not include frame info)
|
||||||
|
*/
|
||||||
|
title?: string | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of reduced values
|
||||||
|
*/
|
||||||
|
calcs?: FieldCalcs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appropriate values for templating
|
||||||
|
*/
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataFrame extends QueryResultBase {
|
export interface DataFrame extends QueryResultBase {
|
||||||
name?: string;
|
name?: string;
|
||||||
fields: Field[]; // All fields of equal length
|
fields: Field[]; // All fields of equal length
|
||||||
|
|
@ -131,3 +145,7 @@ export interface DataFrameDTO extends QueryResultBase {
|
||||||
name?: string;
|
name?: string;
|
||||||
fields: Array<FieldDTO | Field>;
|
fields: Array<FieldDTO | Field>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FieldCalcs extends Record<string, any> {}
|
||||||
|
|
||||||
|
export const TIME_SERIES_FIELD_NAME = 'Value';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
|
import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types';
|
||||||
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
|
import { FieldMatcherID, fieldMatchers, getFieldTitle } from '@grafana/data';
|
||||||
import { Select } from '../Select/Select';
|
import { Select } from '../Select/Select';
|
||||||
|
|
||||||
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
|
export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<string>> {
|
||||||
|
|
@ -10,7 +10,7 @@ export class FieldNameMatcherEditor extends React.PureComponent<MatcherUIProps<s
|
||||||
|
|
||||||
for (const frame of data) {
|
for (const frame of data) {
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
names.add(field.name);
|
names.add(getFieldTitle(field, frame, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options) {
|
if (options) {
|
||||||
|
|
@ -32,6 +32,6 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||||
id: FieldMatcherID.byName,
|
id: FieldMatcherID.byName,
|
||||||
component: FieldNameMatcherEditor,
|
component: FieldNameMatcherEditor,
|
||||||
matcher: fieldMatchers.get(FieldMatcherID.byName),
|
matcher: fieldMatchers.get(FieldMatcherID.byName),
|
||||||
name: 'Filter by name',
|
name: 'Filter by field',
|
||||||
description: 'Set properties for fields matching the name',
|
description: 'Set properties for fields matching the name',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { TextAlignProperty } from 'csstype';
|
import { TextAlignProperty } from 'csstype';
|
||||||
import { DataFrame, Field, FieldType } from '@grafana/data';
|
import { DataFrame, Field, FieldType, getFieldTitle } from '@grafana/data';
|
||||||
import { Column } from 'react-table';
|
import { Column } from 'react-table';
|
||||||
import { DefaultCell } from './DefaultCell';
|
import { DefaultCell } from './DefaultCell';
|
||||||
import { BarGaugeCell } from './BarGaugeCell';
|
import { BarGaugeCell } from './BarGaugeCell';
|
||||||
|
|
@ -48,11 +48,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||||
}
|
}
|
||||||
|
|
||||||
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
|
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
|
||||||
|
|
||||||
columns.push({
|
columns.push({
|
||||||
Cell,
|
Cell,
|
||||||
id: fieldIndex.toString(),
|
id: fieldIndex.toString(),
|
||||||
Header: field.config.title ?? field.name,
|
Header: getFieldTitle(field, data),
|
||||||
accessor: (row: any, i: number) => {
|
accessor: (row: any, i: number) => {
|
||||||
return field.values.get(i);
|
return field.values.get(i);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
binaryOperators,
|
binaryOperators,
|
||||||
CalculateFieldMode,
|
CalculateFieldMode,
|
||||||
getResultFieldNameForCalculateFieldTransformerOptions,
|
getResultFieldNameForCalculateFieldTransformerOptions,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { StatsPicker } from '../StatsPicker/StatsPicker';
|
import { StatsPicker } from '../StatsPicker/StatsPicker';
|
||||||
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
import { Switch } from '../Forms/Legacy/Switch/Switch';
|
||||||
|
|
@ -80,14 +81,18 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
||||||
|
|
||||||
const allNames: string[] = [];
|
const allNames: string[] = [];
|
||||||
const byName: KeyValue<boolean> = {};
|
const byName: KeyValue<boolean> = {};
|
||||||
|
|
||||||
for (const frame of input) {
|
for (const frame of input) {
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!byName[field.name]) {
|
|
||||||
byName[field.name] = true;
|
const title = getFieldTitle(field, frame, input);
|
||||||
allNames.push(field.name);
|
|
||||||
|
if (!byName[title]) {
|
||||||
|
byName[title] = true;
|
||||||
|
allNames.push(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +100,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
||||||
if (configuredOptions.length) {
|
if (configuredOptions.length) {
|
||||||
const options: string[] = [];
|
const options: string[] = [];
|
||||||
const selected: string[] = [];
|
const selected: string[] = [];
|
||||||
|
|
||||||
for (const v of allNames) {
|
for (const v of allNames) {
|
||||||
if (configuredOptions.includes(v)) {
|
if (configuredOptions.includes(v)) {
|
||||||
selected.push(v);
|
selected.push(v);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
TransformerRegistyItem,
|
TransformerRegistyItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { HorizontalGroup } from '../Layout/Layout';
|
import { HorizontalGroup } from '../Layout/Layout';
|
||||||
import { Input } from '../Input/Input';
|
import { Input } from '../Input/Input';
|
||||||
|
|
@ -45,6 +46,12 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||||
this.initOptions();
|
this.initOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(oldProps: FilterByNameTransformerEditorProps) {
|
||||||
|
if (this.props.input !== oldProps.input) {
|
||||||
|
this.initOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private initOptions() {
|
private initOptions() {
|
||||||
const { input, options } = this.props;
|
const { input, options } = this.props;
|
||||||
const configuredOptions = options.include ? options.include : [];
|
const configuredOptions = options.include ? options.include : [];
|
||||||
|
|
@ -54,10 +61,11 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||||
|
|
||||||
for (const frame of input) {
|
for (const frame of input) {
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
let v = byName[field.name];
|
const id = getFieldTitle(field, frame, input);
|
||||||
|
let v = byName[id];
|
||||||
if (!v) {
|
if (!v) {
|
||||||
v = byName[field.name] = {
|
v = byName[id] = {
|
||||||
name: field.name,
|
name: id,
|
||||||
count: 0,
|
count: 0,
|
||||||
};
|
};
|
||||||
allNames.push(v);
|
allNames.push(v);
|
||||||
|
|
@ -147,7 +155,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label width-8">Field name</div>
|
<div className="gf-form-label width-8">Identifier</div>
|
||||||
<HorizontalGroup spacing="xs" align="flex-start" wrap>
|
<HorizontalGroup spacing="xs" align="flex-start" wrap>
|
||||||
<Field
|
<Field
|
||||||
invalid={!isRegexValid}
|
invalid={!isRegexValid}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
TransformerRegistyItem,
|
TransformerRegistyItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { stylesFactory, useTheme } from '../../themes';
|
import { stylesFactory, useTheme } from '../../themes';
|
||||||
import { Input } from '../Input/Input';
|
import { Input } from '../Input/Input';
|
||||||
|
|
@ -71,6 +72,11 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
||||||
[onChange, fieldNames, renameByName]
|
[onChange, fieldNames, renameByName]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show warning that we only apply the first frame
|
||||||
|
if (input.length > 1) {
|
||||||
|
return <div>Organize fields only works with a single frame. Consider applying a join transformation first.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
|
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
|
||||||
|
|
@ -210,10 +216,11 @@ export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return frame.fields.reduce((names, field) => {
|
return frame.fields.reduce((names, field) => {
|
||||||
names[field.name] = null;
|
const t = getFieldTitle(field, frame, input);
|
||||||
|
names[t] = true;
|
||||||
return names;
|
return names;
|
||||||
}, names);
|
}, names);
|
||||||
}, {} as Record<string, null>)
|
}, {} as Record<string, boolean>)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -221,7 +228,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<Organiz
|
||||||
id: DataTransformerID.organize,
|
id: DataTransformerID.organize,
|
||||||
editor: OrganizeFieldsTransformerEditor,
|
editor: OrganizeFieldsTransformerEditor,
|
||||||
transformation: standardTransformers.organizeFieldsTransformer,
|
transformation: standardTransformers.organizeFieldsTransformer,
|
||||||
name: 'Change order, hide and rename',
|
name: 'Organize fields',
|
||||||
description:
|
description:
|
||||||
"Allows the user to re-order, hide, or rename columns. Useful when data source doesn't allow overrides for visualizing data.",
|
"Allows the user to re-order, hide, or rename fields / columns. Useful when data source doesn't allow overrides for visualizing data.",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,8 @@ export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './themes';
|
export * from './themes';
|
||||||
export * from './slate-plugins';
|
export * from './slate-plugins';
|
||||||
|
|
||||||
|
// Exposes standard editors for registries of optionsUi config and panel options UI
|
||||||
|
export { getStandardFieldConfigs, getStandardOptionEditors } from './utils//standardEditors';
|
||||||
|
// Exposes standard transformers for registry of Transformations
|
||||||
|
export { getStandardTransformers } from './utils/standardTransformers';
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,3 @@ export { default as ansicolor } from './ansicolor';
|
||||||
import * as DOMUtil from './dom'; // includes Element.closest polyfil
|
import * as DOMUtil from './dom'; // includes Element.closest polyfil
|
||||||
export { DOMUtil };
|
export { DOMUtil };
|
||||||
export { renderOrCallToRender } from './renderOrCallToRender';
|
export { renderOrCallToRender } from './renderOrCallToRender';
|
||||||
|
|
||||||
// Exposes standard editors for registries of optionsUi config and panel options UI
|
|
||||||
export { getStandardFieldConfigs, getStandardOptionEditors } from './standardEditors';
|
|
||||||
// Exposes standard transformers for registry of Transformations
|
|
||||||
export { getStandardTransformers } from './standardTransformers';
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
toCSV,
|
toCSV,
|
||||||
transformDataFrame,
|
transformDataFrame,
|
||||||
|
getFrameDisplayTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Button, Field, Icon, Select, Table } from '@grafana/ui';
|
import { Button, Field, Icon, Select, Table } from '@grafana/ui';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
@ -105,7 +106,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||||
const choices = dataFrames.map((frame, index) => {
|
const choices = dataFrames.map((frame, index) => {
|
||||||
return {
|
return {
|
||||||
value: index,
|
value: index,
|
||||||
label: `${frame.name} (${index})`,
|
label: `${getFrameDisplayTitle(frame)} (${index})`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,9 @@ export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataF
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const dataFrame = guessFieldTypes(toDataFrame(result));
|
const dataFrame = guessFieldTypes(toDataFrame(result));
|
||||||
|
|
||||||
// clear out any cached calcs
|
// clear out the cached info
|
||||||
for (const field of dataFrame.fields) {
|
for (const field of dataFrame.fields) {
|
||||||
field.calcs = null;
|
field.state = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataFrames.push(dataFrame);
|
dataFrames.push(dataFrame);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import '../datasource';
|
import '../datasource';
|
||||||
import { CloudWatchDatasource } from '../datasource';
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
import * as redux from 'app/store/store';
|
import * as redux from 'app/store/store';
|
||||||
import { DataSourceInstanceSettings, dateMath } from '@grafana/data';
|
import { DataSourceInstanceSettings, dateMath, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { CustomVariable } from 'app/features/templating/all';
|
import { CustomVariable } from 'app/features/templating/all';
|
||||||
import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types';
|
import { CloudWatchQuery, CloudWatchMetricsQuery } from '../types';
|
||||||
|
|
@ -233,7 +233,7 @@ describe('CloudWatchDatasource', () => {
|
||||||
|
|
||||||
it('should return series list', done => {
|
it('should return series list', done => {
|
||||||
ctx.ds.query(query).then((result: any) => {
|
ctx.ds.query(query).then((result: any) => {
|
||||||
expect(result.data[0].name).toBe(response.results.A.series[0].name);
|
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
|
||||||
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
@ -249,7 +249,7 @@ describe('CloudWatchDatasource', () => {
|
||||||
it('should be built correctly if theres one search expressions returned in meta for a given query row', done => {
|
it('should be built correctly if theres one search expressions returned in meta for a given query row', done => {
|
||||||
response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
|
response.results['A'].meta.gmdMeta = [{ Expression: `REMOVE_EMPTY(SEARCH('some expression'))`, Period: '300' }];
|
||||||
ctx.ds.query(query).then((result: any) => {
|
ctx.ds.query(query).then((result: any) => {
|
||||||
expect(result.data[0].name).toBe(response.results.A.series[0].name);
|
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
|
||||||
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
||||||
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
|
expect(decodeURIComponent(result.data[0].fields[1].config.links[0].url)).toContain(
|
||||||
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
|
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'some expression\'))"}]}`
|
||||||
|
|
@ -264,7 +264,7 @@ describe('CloudWatchDatasource', () => {
|
||||||
{ Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
|
{ Expression: `REMOVE_EMPTY(SEARCH('second expression'))` },
|
||||||
];
|
];
|
||||||
ctx.ds.query(query).then((result: any) => {
|
ctx.ds.query(query).then((result: any) => {
|
||||||
expect(result.data[0].name).toBe(response.results.A.series[0].name);
|
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
|
||||||
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
||||||
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
|
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
|
||||||
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
|
`region=us-east-1#metricsV2:graph={"view":"timeSeries","stacked":false,"title":"A","start":"2016-12-31T15:00:00.000Z","end":"2016-12-31T16:00:00.000Z","region":"us-east-1","metrics":[{"expression":"REMOVE_EMPTY(SEARCH(\'first expression\'))"},{"expression":"REMOVE_EMPTY(SEARCH(\'second expression\'))"}]}`
|
||||||
|
|
@ -276,7 +276,7 @@ describe('CloudWatchDatasource', () => {
|
||||||
it('should be built correctly if the query is a metric stat query', done => {
|
it('should be built correctly if the query is a metric stat query', done => {
|
||||||
response.results['A'].meta.gmdMeta = [{ Period: '300' }];
|
response.results['A'].meta.gmdMeta = [{ Period: '300' }];
|
||||||
ctx.ds.query(query).then((result: any) => {
|
ctx.ds.query(query).then((result: any) => {
|
||||||
expect(result.data[0].name).toBe(response.results.A.series[0].name);
|
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
|
||||||
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
expect(result.data[0].fields[1].config.links[0].title).toBe('View in CloudWatch console');
|
||||||
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
|
expect(decodeURIComponent(result.data[0].fields[0].config.links[0].url)).toContain(
|
||||||
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
|
`region=us-east-1#metricsV2:graph={\"view\":\"timeSeries\",\"stacked\":false,\"title\":\"A\",\"start\":\"2016-12-31T15:00:00.000Z\",\"end\":\"2016-12-31T16:00:00.000Z\",\"region\":\"us-east-1\",\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-12345678\",{\"stat\":\"Average\",\"period\":\"300\"}]]}`
|
||||||
|
|
@ -517,7 +517,7 @@ describe('CloudWatchDatasource', () => {
|
||||||
|
|
||||||
it('should return series list', done => {
|
it('should return series list', done => {
|
||||||
ctx.ds.query(query).then((result: any) => {
|
ctx.ds.query(query).then((result: any) => {
|
||||||
expect(result.data[0].name).toBe(response.results.A.series[0].name);
|
expect(getFrameDisplayTitle(result.data[0])).toBe(response.results.A.series[0].name);
|
||||||
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
expect(result.data[0].fields[1].values.buffer[0]).toBe(response.results.A.series[0].points[0][0]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -929,9 +929,10 @@ describe('ElasticResponse', () => {
|
||||||
const hist: KeyValue<number> = {};
|
const hist: KeyValue<number> = {};
|
||||||
const histogramResults = new MutableDataFrame(result.data[1]);
|
const histogramResults = new MutableDataFrame(result.data[1]);
|
||||||
rows = new DataFrameView(histogramResults);
|
rows = new DataFrameView(histogramResults);
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
const row = rows.get(i);
|
const row = rows.get(i);
|
||||||
hist[row.Time] = row.Count;
|
hist[row.Time] = row.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => {
|
response.responses[0].aggregations['2'].buckets.forEach((bucket: any) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Datasource from '../datasource';
|
import Datasource from '../datasource';
|
||||||
import { DataFrame, toUtc } from '@grafana/data';
|
import { DataFrame, toUtc, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@ describe('AppInsightsDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
const data = results.data[0] as DataFrame;
|
const data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('PrimaryResult');
|
expect(getFrameDisplayTitle(data)).toEqual('PrimaryResult');
|
||||||
expect(data.fields[0].values.length).toEqual(1);
|
expect(data.fields[0].values.length).toEqual(1);
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||||
|
|
@ -218,7 +218,7 @@ describe('AppInsightsDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
const data = results.data[0] as DataFrame;
|
const data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('paritionA');
|
expect(getFrameDisplayTitle(data)).toEqual('paritionA');
|
||||||
expect(data.fields[0].values.length).toEqual(1);
|
expect(data.fields[0].values.length).toEqual(1);
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||||
|
|
@ -279,7 +279,7 @@ describe('AppInsightsDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
const data = results.data[0] as DataFrame;
|
const data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('exceptions/server');
|
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server');
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||||
});
|
});
|
||||||
|
|
@ -322,7 +322,7 @@ describe('AppInsightsDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
const data = results.data[0] as DataFrame;
|
const data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('exceptions/server');
|
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server');
|
||||||
expect(data.fields[0].values.length).toEqual(2);
|
expect(data.fields[0].values.length).toEqual(2);
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(3);
|
expect(data.fields[1].values.get(0)).toEqual(3);
|
||||||
|
|
@ -376,14 +376,14 @@ describe('AppInsightsDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(2);
|
expect(results.data.length).toBe(2);
|
||||||
let data = results.data[0] as DataFrame;
|
let data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('exceptions/server{client/city="Miami"}');
|
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="Miami"}');
|
||||||
expect(data.fields[1].values.length).toEqual(2);
|
expect(data.fields[1].values.length).toEqual(2);
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(10);
|
expect(data.fields[1].values.get(0)).toEqual(10);
|
||||||
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
expect(data.fields[0].values.get(1)).toEqual(1504112400000);
|
||||||
expect(data.fields[1].values.get(1)).toEqual(20);
|
expect(data.fields[1].values.get(1)).toEqual(20);
|
||||||
data = results.data[1] as DataFrame;
|
data = results.data[1] as DataFrame;
|
||||||
expect(data.name).toEqual('exceptions/server{client/city="San Antonio"}');
|
expect(getFrameDisplayTitle(data)).toEqual('exceptions/server{client/city="San Antonio"}');
|
||||||
expect(data.fields[1].values.length).toEqual(2);
|
expect(data.fields[1].values.length).toEqual(2);
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
expect(data.fields[0].values.get(0)).toEqual(1504108800000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(1);
|
expect(data.fields[1].values.get(0)).toEqual(1);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource';
|
||||||
import FakeSchemaData from './__mocks__/schema';
|
import FakeSchemaData from './__mocks__/schema';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { KustoSchema, AzureLogsVariable } from '../types';
|
import { KustoSchema, AzureLogsVariable } from '../types';
|
||||||
import { toUtc } from '@grafana/data';
|
import { toUtc, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
|
@ -183,10 +183,11 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||||
it('should return a list of datapoints', () => {
|
it('should return a list of datapoints', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
expect(results.data[0].name).toEqual('grafana-vm');
|
expect(getFrameDisplayTitle(results.data[0])).toEqual('grafana-vm');
|
||||||
expect(results.data[0].fields.length).toBe(2);
|
expect(results.data[0].fields.length).toBe(2);
|
||||||
|
expect(results.data[0].name).toBe('grafana-vm');
|
||||||
expect(results.data[0].fields[0].name).toBe('Time');
|
expect(results.data[0].fields[0].name).toBe('Time');
|
||||||
expect(results.data[0].fields[1].name).toBe('grafana-vm');
|
expect(results.data[0].fields[1].name).toBe('Value');
|
||||||
expect(results.data[0].fields[0].values.toArray().length).toBe(6);
|
expect(results.data[0].fields[0].values.toArray().length).toBe(6);
|
||||||
expect(results.data[0].fields[0].values.get(0)).toEqual(1587633300000);
|
expect(results.data[0].fields[0].values.get(0)).toEqual(1587633300000);
|
||||||
expect(results.data[0].fields[1].values.get(0)).toEqual(2017.25);
|
expect(results.data[0].fields[1].values.get(0)).toEqual(2017.25);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import AzureMonitorDatasource from '../datasource';
|
import AzureMonitorDatasource from '../datasource';
|
||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { toUtc, DataFrame } from '@grafana/data';
|
import { toUtc, DataFrame, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
|
@ -137,7 +137,7 @@ describe('AzureMonitorDatasource', () => {
|
||||||
return ctx.ds.query(options).then((results: any) => {
|
return ctx.ds.query(options).then((results: any) => {
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
const data = results.data[0] as DataFrame;
|
const data = results.data[0] as DataFrame;
|
||||||
expect(data.name).toEqual('Percentage CPU');
|
expect(getFrameDisplayTitle(data)).toEqual('Percentage CPU');
|
||||||
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
expect(data.fields[0].values.get(0)).toEqual(1558278660000);
|
||||||
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
expect(data.fields[1].values.get(0)).toEqual(2.2075);
|
||||||
expect(data.fields[0].values.get(1)).toEqual(1558278720000);
|
expect(data.fields[0].values.get(1)).toEqual(1558278720000);
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
|
||||||
for (let i = 0; i < series.length; i++) {
|
for (let i = 0; i < series.length; i++) {
|
||||||
const s = series[i];
|
const s = series[i];
|
||||||
|
|
||||||
|
// Disables Grafana own series naming
|
||||||
|
s.title = s.target;
|
||||||
|
|
||||||
for (let y = 0; y < s.datapoints.length; y++) {
|
for (let y = 0; y < s.datapoints.length; y++) {
|
||||||
s.datapoints[y][1] *= 1000;
|
s.datapoints[y][1] *= 1000;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { GraphiteDatasource } from '../datasource';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTime, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
|
@ -91,8 +91,8 @@ describe('graphiteDatasource', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.data.length).toBe(2);
|
expect(result.data.length).toBe(2);
|
||||||
expect(result.data[0].name).toBe('seriesA');
|
expect(getFrameDisplayTitle(result.data[0])).toBe('seriesA');
|
||||||
expect(result.data[1].name).toBe('seriesB');
|
expect(getFrameDisplayTitle(result.data[1])).toBe('seriesB');
|
||||||
expect(result.data[0].length).toBe(2);
|
expect(result.data[0].length).toBe(2);
|
||||||
expect(result.data[0].meta.notices.length).toBe(1);
|
expect(result.data[0].meta.notices.length).toBe(1);
|
||||||
expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');
|
expect(result.data[0].meta.notices[0].text).toBe('Data is rolled up, aggregated over 2h using Average function');
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import {
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
dateTime,
|
dateTime,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
toDataFrame,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { PromOptions, PromQuery } from './types';
|
import { PromOptions, PromQuery } from './types';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
|
@ -586,8 +588,9 @@ describe('PrometheusDatasource', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series list', async () => {
|
it('should return series list', async () => {
|
||||||
|
const frame = toDataFrame(results.data[0]);
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
expect(results.data[0].target).toBe('test{job="testjob"}');
|
expect(getFieldTitle(frame.fields[1])).toBe('test{job="testjob"}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -730,8 +733,10 @@ describe('PrometheusDatasource', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series list', () => {
|
it('should return series list', () => {
|
||||||
|
const frame = toDataFrame(results.data[0]);
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
expect(results.data[0].target).toBe('test{job="testjob"}');
|
expect(frame.name).toBe('test{job="testjob"}');
|
||||||
|
expect(getFieldTitle(frame.fields[1])).toBe('Value');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1634,8 +1639,9 @@ describe('PrometheusDatasource for POST', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series list', () => {
|
it('should return series list', () => {
|
||||||
|
const frame = toDataFrame(results.data[0]);
|
||||||
expect(results.data.length).toBe(1);
|
expect(results.data.length).toBe(1);
|
||||||
expect(results.data[0].target).toBe('test{job="testjob"}');
|
expect(getFieldTitle(frame.fields[1])).toBe('test{job="testjob"}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ describe('Prometheus Result Transformer', () => {
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
target: '1',
|
target: '1',
|
||||||
|
title: '1',
|
||||||
query: undefined,
|
query: undefined,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[10, 1445000010000],
|
[10, 1445000010000],
|
||||||
|
|
@ -172,6 +173,7 @@ describe('Prometheus Result Transformer', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: '2',
|
target: '2',
|
||||||
|
title: '2',
|
||||||
query: undefined,
|
query: undefined,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[10, 1445000010000],
|
[10, 1445000010000],
|
||||||
|
|
@ -182,6 +184,7 @@ describe('Prometheus Result Transformer', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: '3',
|
target: '3',
|
||||||
|
title: '3',
|
||||||
query: undefined,
|
query: undefined,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[10, 1445000010000],
|
[10, 1445000010000],
|
||||||
|
|
@ -285,6 +288,7 @@ describe('Prometheus Result Transformer', () => {
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
target: 'test{job="testjob"}',
|
target: 'test{job="testjob"}',
|
||||||
|
title: 'test{job="testjob"}',
|
||||||
query: undefined,
|
query: undefined,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[10, 0],
|
[10, 0],
|
||||||
|
|
@ -324,6 +328,7 @@ describe('Prometheus Result Transformer', () => {
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
target: 'test{job="testjob"}',
|
target: 'test{job="testjob"}',
|
||||||
|
title: 'test{job="testjob"}',
|
||||||
query: undefined,
|
query: undefined,
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[null, 0],
|
[null, 0],
|
||||||
|
|
@ -335,6 +340,64 @@ describe('Prometheus Result Transformer', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use __name__ label as series name', () => {
|
||||||
|
const response = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
resultType: 'matrix',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: { __name__: 'test', job: 'testjob' },
|
||||||
|
values: [
|
||||||
|
[1, '10'],
|
||||||
|
[2, '0'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
format: 'timeseries',
|
||||||
|
step: 1,
|
||||||
|
start: 0,
|
||||||
|
end: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
|
expect(result[0].target).toEqual('test{job="testjob"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set frame name to undefined if no __name__ label but there are other labels', () => {
|
||||||
|
const response = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
resultType: 'matrix',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: { job: 'testjob' },
|
||||||
|
values: [
|
||||||
|
[1, '10'],
|
||||||
|
[2, '0'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
format: 'timeseries',
|
||||||
|
step: 1,
|
||||||
|
query: 'Some query',
|
||||||
|
start: 0,
|
||||||
|
end: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
|
expect(result[0].target).toBe('{job="testjob"}');
|
||||||
|
expect(result[0].tags.job).toEqual('testjob');
|
||||||
|
});
|
||||||
|
|
||||||
it('should align null values with step', () => {
|
it('should align null values with step', () => {
|
||||||
const response = {
|
const response = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
|
@ -356,13 +419,20 @@ describe('Prometheus Result Transformer', () => {
|
||||||
step: 2,
|
step: 2,
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 8,
|
end: 8,
|
||||||
|
refId: 'A',
|
||||||
|
meta: { custom: { hello: '1' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = ctx.resultTransformer.transform({ data: response }, options);
|
const result = ctx.resultTransformer.transform({ data: response }, options);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
target: 'test{job="testjob"}',
|
target: 'test{job="testjob"}',
|
||||||
|
title: 'test{job="testjob"}',
|
||||||
|
meta: {
|
||||||
|
custom: { hello: '1' },
|
||||||
|
},
|
||||||
query: undefined,
|
query: undefined,
|
||||||
|
refId: 'A',
|
||||||
datapoints: [
|
datapoints: [
|
||||||
[null, 0],
|
[null, 0],
|
||||||
[null, 2000],
|
[null, 2000],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
import { TimeSeries, FieldType } from '@grafana/data';
|
import { TimeSeries, FieldType, Labels, formatLabels } from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
|
||||||
export class ResultTransformer {
|
export class ResultTransformer {
|
||||||
|
|
@ -42,9 +42,7 @@ export class ResultTransformer {
|
||||||
|
|
||||||
transformMetricData(metricData: any, options: any, start: number, end: number) {
|
transformMetricData(metricData: any, options: any, start: number, end: number) {
|
||||||
const dps = [];
|
const dps = [];
|
||||||
let metricLabel = null;
|
const { name, labels, title } = this.createLabelInfo(metricData.metric, options);
|
||||||
|
|
||||||
metricLabel = this.createMetricLabel(metricData.metric, options);
|
|
||||||
|
|
||||||
const stepMs = parseFloat(options.step) * 1000;
|
const stepMs = parseFloat(options.step) * 1000;
|
||||||
let baseTimestamp = start * 1000;
|
let baseTimestamp = start * 1000;
|
||||||
|
|
@ -76,9 +74,10 @@ export class ResultTransformer {
|
||||||
datapoints: dps,
|
datapoints: dps,
|
||||||
query: options.query,
|
query: options.query,
|
||||||
refId: options.refId,
|
refId: options.refId,
|
||||||
|
target: name,
|
||||||
|
tags: labels,
|
||||||
|
title,
|
||||||
meta: options.meta,
|
meta: options.meta,
|
||||||
target: metricLabel,
|
|
||||||
tags: metricData.metric,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,23 +141,39 @@ export class ResultTransformer {
|
||||||
|
|
||||||
transformInstantMetricData(md: any, options: any) {
|
transformInstantMetricData(md: any, options: any) {
|
||||||
const dps = [];
|
const dps = [];
|
||||||
let metricLabel = null;
|
const { name, labels } = this.createLabelInfo(md.metric, options);
|
||||||
metricLabel = this.createMetricLabel(md.metric, options);
|
|
||||||
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
|
||||||
return { target: metricLabel, datapoints: dps, tags: md.metric, refId: options.refId, meta: options.meta };
|
return { target: name, datapoints: dps, tags: labels, refId: options.refId, meta: options.meta };
|
||||||
}
|
}
|
||||||
|
|
||||||
createMetricLabel(labelData: { [key: string]: string }, options: any) {
|
createLabelInfo(labels: { [key: string]: string }, options: any): { name?: string; labels: Labels; title?: string } {
|
||||||
let label = '';
|
if (options?.legendFormat) {
|
||||||
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
|
const title = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labels);
|
||||||
label = this.getOriginalMetricName(labelData);
|
return { name: title, title, labels };
|
||||||
} else {
|
|
||||||
label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData);
|
|
||||||
}
|
}
|
||||||
if (!label || label === '{}') {
|
|
||||||
label = options.query;
|
let { __name__, ...labelsWithoutName } = labels;
|
||||||
|
|
||||||
|
let title = __name__ || '';
|
||||||
|
|
||||||
|
const labelPart = formatLabels(labelsWithoutName);
|
||||||
|
|
||||||
|
if (!title && !labelPart) {
|
||||||
|
title = options.query;
|
||||||
}
|
}
|
||||||
return label;
|
|
||||||
|
title = `${__name__ ?? ''}${labelPart}`;
|
||||||
|
|
||||||
|
return { name: title, title, labels: labelsWithoutName };
|
||||||
|
}
|
||||||
|
|
||||||
|
getOriginalMetricName(labelData: { [key: string]: string }) {
|
||||||
|
const metricName = labelData.__name__ || '';
|
||||||
|
delete labelData.__name__;
|
||||||
|
const labelPart = Object.entries(labelData)
|
||||||
|
.map(label => `${label[0]}="${label[1]}"`)
|
||||||
|
.join(',');
|
||||||
|
return `${metricName}{${labelPart}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {
|
renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {
|
||||||
|
|
@ -171,15 +186,6 @@ export class ResultTransformer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getOriginalMetricName(labelData: { [key: string]: string }) {
|
|
||||||
const metricName = labelData.__name__ || '';
|
|
||||||
delete labelData.__name__;
|
|
||||||
const labelPart = _.map(_.toPairs(labelData), label => {
|
|
||||||
return label[0] + '="' + label[1] + '"';
|
|
||||||
}).join(',');
|
|
||||||
return metricName + '{' + labelPart + '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
transformToHistogramOverTime(seriesList: TimeSeries[]) {
|
transformToHistogramOverTime(seriesList: TimeSeries[]) {
|
||||||
/* t1 = timestamp1, t2 = timestamp2 etc.
|
/* t1 = timestamp1, t2 = timestamp2 etc.
|
||||||
t1 t2 t3 t1 t2 t3
|
t1 t2 t3 t1 t2 t3
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
getTimeField,
|
getTimeField,
|
||||||
dateTime,
|
dateTime,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
@ -31,27 +32,24 @@ export class DataProcessor {
|
||||||
for (let i = 0; i < dataList.length; i++) {
|
for (let i = 0; i < dataList.length; i++) {
|
||||||
const series = dataList[i];
|
const series = dataList[i];
|
||||||
const { timeField } = getTimeField(series);
|
const { timeField } = getTimeField(series);
|
||||||
|
|
||||||
if (!timeField) {
|
if (!timeField) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesName = series.name ? series.name : series.refId;
|
|
||||||
for (let j = 0; j < series.fields.length; j++) {
|
for (let j = 0; j < series.fields.length; j++) {
|
||||||
const field = series.fields[j];
|
const field = series.fields[j];
|
||||||
|
|
||||||
if (field.type !== FieldType.number) {
|
if (field.type !== FieldType.number) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const name = getFieldTitle(field, series, dataList);
|
||||||
let name = field.config && field.config.title ? field.config.title : field.name;
|
|
||||||
|
|
||||||
if (seriesName && dataList.length > 0 && name !== seriesName) {
|
|
||||||
name = seriesName + ' ' + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const datapoints = [];
|
const datapoints = [];
|
||||||
|
|
||||||
for (let r = 0; r < series.length; r++) {
|
for (let r = 0; r < series.length; r++) {
|
||||||
datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]);
|
datapoints.push([field.values.get(r), dateTime(timeField.values.get(r)).valueOf()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range));
|
list.push(this.toTimeSeries(field, name, i, j, datapoints, list.length, range));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +58,11 @@ export class DataProcessor {
|
||||||
if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) {
|
if (this.panel.xaxis.mode === 'histogram' && !this.panel.stack && list.length > 1) {
|
||||||
const first = list[0];
|
const first = list[0];
|
||||||
first.alias = first.aliasEscaped = 'Count';
|
first.alias = first.aliasEscaped = 'Count';
|
||||||
|
|
||||||
for (let i = 1; i < list.length; i++) {
|
for (let i = 1; i < list.length; i++) {
|
||||||
first.datapoints = first.datapoints.concat(list[i].datapoints);
|
first.datapoints = first.datapoints.concat(list[i].datapoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [first];
|
return [first];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
FieldColor,
|
FieldColor,
|
||||||
FieldColorMode,
|
FieldColorMode,
|
||||||
FieldConfigSource,
|
FieldConfigSource,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { SeriesOptions, GraphOptions, GraphLegendEditorLegendOptions } from './types';
|
import { SeriesOptions, GraphOptions, GraphLegendEditorLegendOptions } from './types';
|
||||||
|
|
@ -122,7 +123,7 @@ export const getGraphSeriesModel = (
|
||||||
});
|
});
|
||||||
|
|
||||||
graphs.push({
|
graphs.push({
|
||||||
label: field.name,
|
label: getFieldTitle(field, series, dataFrames),
|
||||||
data: points,
|
data: points,
|
||||||
color: field.config.color?.fixedColor,
|
color: field.config.color?.fixedColor,
|
||||||
info: statsDisplayValues,
|
info: statsDisplayValues,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
PanelEvents,
|
PanelEvents,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
locationUtil,
|
locationUtil,
|
||||||
|
getFieldTitle,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { convertOldAngularValueMapping } from '@grafana/ui';
|
import { convertOldAngularValueMapping } from '@grafana/ui';
|
||||||
|
|
@ -156,6 +157,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||||
|
|
||||||
onFramesReceived(frames: DataFrame[]) {
|
onFramesReceived(frames: DataFrame[]) {
|
||||||
const { panel } = this;
|
const { panel } = this;
|
||||||
|
this.dataList = frames;
|
||||||
|
|
||||||
if (frames && frames.length > 1) {
|
if (frames && frames.length > 1) {
|
||||||
this.data = {
|
this.data = {
|
||||||
|
|
@ -204,7 +206,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||||
processField(fieldInfo: FieldInfo) {
|
processField(fieldInfo: FieldInfo) {
|
||||||
const { panel, dashboard } = this;
|
const { panel, dashboard } = this;
|
||||||
|
|
||||||
const name = fieldInfo.field.config.title || fieldInfo.field.name;
|
const name = getFieldTitle(fieldInfo.field, fieldInfo.frame.frame, this.dataList as DataFrame[]);
|
||||||
let calc = panel.valueName;
|
let calc = panel.valueName;
|
||||||
let calcField = fieldInfo.field;
|
let calcField = fieldInfo.field;
|
||||||
let val: any = undefined;
|
let val: any = undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { SingleStatCtrl, ShowData } from '../module';
|
import { SingleStatCtrl, ShowData } from '../module';
|
||||||
import { dateTime, ReducerID } from '@grafana/data';
|
import { dateTime, ReducerID, getFieldTitle } from '@grafana/data';
|
||||||
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||||
import { LegacyResponseData } from '@grafana/data';
|
import { LegacyResponseData } from '@grafana/data';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
@ -90,7 +90,8 @@ describe('SingleStatCtrl', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should use series avg as default main value', () => {
|
it('Should use series avg as default main value', () => {
|
||||||
expect(ctx.data.value).toBe('test.cpu1');
|
const title = getFieldTitle(ctx.data.field);
|
||||||
|
expect(title).toBe('test.cpu1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set formatted value', () => {
|
it('should set formatted value', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Table, Select } from '@grafana/ui';
|
import { Table, Select } from '@grafana/ui';
|
||||||
import { FieldMatcherID, PanelProps, DataFrame, SelectableValue } from '@grafana/data';
|
import { FieldMatcherID, PanelProps, DataFrame, SelectableValue, getFrameDisplayTitle } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
@ -101,7 +101,7 @@ export class TablePanel extends Component<Props> {
|
||||||
const currentIndex = this.getCurrentFrameIndex();
|
const currentIndex = this.getCurrentFrameIndex();
|
||||||
const names = data.series.map((frame, index) => {
|
const names = data.series.map((frame, index) => {
|
||||||
return {
|
return {
|
||||||
label: `${frame.name ?? 'Series'}`,
|
label: getFrameDisplayTitle(frame),
|
||||||
value: index,
|
value: index,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue