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