mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FieldDisplay: Smarter naming of stat values when visualising row values (all values) in stat panels (#31704)
* StatPanels: Improving naming * Finally making progress * Removed default templaet * Removed unused function
This commit is contained in:
@@ -203,6 +203,100 @@ describe('FieldDisplay', () => {
|
||||
expect(result[3].display.text).toEqual(mappedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto option', () => {
|
||||
it('No string fields, single value', () => {
|
||||
const options = createDisplayOptions({
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
},
|
||||
data: [
|
||||
toDataFrame({
|
||||
name: 'Series Name',
|
||||
fields: [{ name: 'A', values: [10] }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = getFieldDisplayValues(options);
|
||||
expect(result[0].display.title).toEqual('A');
|
||||
expect(result[0].display.text).toEqual('10');
|
||||
});
|
||||
|
||||
it('Single other string field', () => {
|
||||
const options = createDisplayOptions({
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
},
|
||||
data: [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Name', values: ['A', 'B'] },
|
||||
{ name: 'Value', values: [10, 20] },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = getFieldDisplayValues(options);
|
||||
expect(result[0].display.title).toEqual('A');
|
||||
expect(result[0].display.text).toEqual('10');
|
||||
expect(result[1].display.title).toEqual('B');
|
||||
expect(result[1].display.text).toEqual('20');
|
||||
});
|
||||
|
||||
it('Single string field multiple value fields', () => {
|
||||
const options = createDisplayOptions({
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
},
|
||||
data: [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Name', values: ['A', 'B'] },
|
||||
{ name: 'SensorA', values: [10, 20] },
|
||||
{ name: 'SensorB', values: [10, 20] },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = getFieldDisplayValues(options);
|
||||
expect(result[0].display.title).toEqual('A SensorA');
|
||||
expect(result[0].display.text).toEqual('10');
|
||||
expect(result[1].display.title).toEqual('B SensorA');
|
||||
expect(result[1].display.text).toEqual('20');
|
||||
expect(result[2].display.title).toEqual('A SensorB');
|
||||
expect(result[3].display.title).toEqual('B SensorB');
|
||||
});
|
||||
|
||||
it('Multiple other string fields', () => {
|
||||
const options = createDisplayOptions({
|
||||
reduceOptions: {
|
||||
values: true,
|
||||
calcs: [],
|
||||
},
|
||||
data: [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Country', values: ['Sweden', 'Norway'] },
|
||||
{ name: 'City', values: ['Stockholm', 'Oslo'] },
|
||||
{ name: 'Value', values: [10, 20] },
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = getFieldDisplayValues(options);
|
||||
expect(result[0].display.title).toEqual('Sweden Stockholm');
|
||||
expect(result[0].display.text).toEqual('10');
|
||||
expect(result[1].display.title).toEqual('Norway Oslo');
|
||||
expect(result[1].display.text).toEqual('20');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createEmptyDisplayOptions(extend = {}): GetFieldDisplayValuesOptions {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ScopedVars } from '../types/ScopedVars';
|
||||
import { getTimeField } from '../dataframe/processDataFrame';
|
||||
import { getFieldMatcher } from '../transformations';
|
||||
import { FieldMatcherID } from '../transformations/matchers/ids';
|
||||
import { getFieldDisplayName } from './fieldState';
|
||||
|
||||
/**
|
||||
* Options for how to turn DataFrames into an array of display values
|
||||
@@ -44,17 +45,6 @@ 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(stats: string[]): string {
|
||||
const parts: string[] = [];
|
||||
if (stats.length > 1) {
|
||||
parts.push('${' + VAR_CALC + '}');
|
||||
}
|
||||
|
||||
parts.push('${' + VAR_FIELD_NAME + '}');
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export interface FieldSparkline {
|
||||
y: Field; // Y values
|
||||
x?: Field; // if this does not exist, use the index
|
||||
@@ -104,136 +94,144 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
}
|
||||
);
|
||||
|
||||
if (options.data) {
|
||||
// Field overrides are applied already
|
||||
const data = options.data;
|
||||
let hitLimit = false;
|
||||
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||
const scopedVars: ScopedVars = {};
|
||||
const defaultDisplayName = getTitleTemplate(calcs);
|
||||
const data = options.data ?? [];
|
||||
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||
const scopedVars: ScopedVars = {};
|
||||
|
||||
for (let s = 0; s < data.length && !hitLimit; s++) {
|
||||
const series = data[s]; // Name is already set
|
||||
let hitLimit = false;
|
||||
|
||||
const { timeField } = getTimeField(series);
|
||||
const view = new DataFrameView(series);
|
||||
for (let s = 0; s < data.length && !hitLimit; s++) {
|
||||
const dataFrame = data[s]; // Name is already set
|
||||
|
||||
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
||||
const field = series.fields[i];
|
||||
const fieldLinksSupplier = field.getLinks;
|
||||
const { timeField } = getTimeField(dataFrame);
|
||||
const view = new DataFrameView(dataFrame);
|
||||
|
||||
// To filter out time field, need an option for this
|
||||
if (!fieldMatcher(field, series, data)) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < dataFrame.fields.length && !hitLimit; i++) {
|
||||
const field = dataFrame.fields[i];
|
||||
const fieldLinksSupplier = field.getLinks;
|
||||
|
||||
let config = field.config; // already set by the prepare task
|
||||
// To filter out time field, need an option for this
|
||||
if (!fieldMatcher(field, dataFrame, data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.state?.range) {
|
||||
// Us the global min/max values
|
||||
config = {
|
||||
...config,
|
||||
...field.state?.range,
|
||||
};
|
||||
}
|
||||
let config = field.config; // already set by the prepare task
|
||||
|
||||
const displayName = field.config.displayName ?? defaultDisplayName;
|
||||
if (field.state?.range) {
|
||||
// Us the global min/max values
|
||||
config = {
|
||||
...config,
|
||||
...field.state?.range,
|
||||
};
|
||||
}
|
||||
|
||||
const display =
|
||||
field.display ??
|
||||
getDisplayProcessor({
|
||||
field,
|
||||
theme: options.theme,
|
||||
timeZone,
|
||||
});
|
||||
// const displayName = getFieldDisplayName(field, dataFrame, data);
|
||||
const displayName = field.config.displayName ?? '';
|
||||
|
||||
// Show all rows
|
||||
if (reduceOptions.values) {
|
||||
const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||
const display =
|
||||
field.display ??
|
||||
getDisplayProcessor({
|
||||
field,
|
||||
theme: options.theme,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
for (let j = 0; j < field.values.length; j++) {
|
||||
// Add all the row variables
|
||||
if (usesCellValues) {
|
||||
for (let k = 0; k < series.fields.length; k++) {
|
||||
const f = series.fields[k];
|
||||
const v = f.values.get(j);
|
||||
scopedVars[VAR_CELL_PREFIX + k] = {
|
||||
value: v,
|
||||
text: toString(v),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Show all rows
|
||||
if (reduceOptions.values) {
|
||||
const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||
|
||||
const displayValue = display(field.values.get(j));
|
||||
displayValue.title = replaceVariables(displayName, {
|
||||
...field.state?.scopedVars, // series and field scoped vars
|
||||
...scopedVars,
|
||||
});
|
||||
|
||||
values.push({
|
||||
name: '',
|
||||
field: config,
|
||||
display: displayValue,
|
||||
view,
|
||||
colIndex: i,
|
||||
rowIndex: j,
|
||||
getLinks: fieldLinksSupplier
|
||||
? () =>
|
||||
fieldLinksSupplier({
|
||||
valueRowIndex: j,
|
||||
})
|
||||
: () => [],
|
||||
hasLinks: hasLinks(field),
|
||||
});
|
||||
|
||||
if (values.length >= limit) {
|
||||
hitLimit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const results = reduceField({
|
||||
field,
|
||||
reducers: calcs, // The stats to calculate
|
||||
});
|
||||
|
||||
for (const calc of calcs) {
|
||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||
const displayValue = display(results[calc]);
|
||||
displayValue.title = replaceVariables(displayName, {
|
||||
...field.state?.scopedVars, // series and field scoped vars
|
||||
...scopedVars,
|
||||
});
|
||||
|
||||
let sparkline: FieldSparkline | undefined = undefined;
|
||||
if (options.sparkline) {
|
||||
sparkline = {
|
||||
y: series.fields[i],
|
||||
x: timeField,
|
||||
for (let j = 0; j < field.values.length; j++) {
|
||||
// Add all the row variables
|
||||
if (usesCellValues) {
|
||||
for (let k = 0; k < dataFrame.fields.length; k++) {
|
||||
const f = dataFrame.fields[k];
|
||||
const v = f.values.get(j);
|
||||
scopedVars[VAR_CELL_PREFIX + k] = {
|
||||
value: v,
|
||||
text: toString(v),
|
||||
};
|
||||
if (calc === ReducerID.last) {
|
||||
sparkline.highlightIndex = sparkline.y.values.length - 1;
|
||||
} else if (calc === ReducerID.first) {
|
||||
sparkline.highlightIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
values.push({
|
||||
name: calc,
|
||||
field: config,
|
||||
display: displayValue,
|
||||
sparkline,
|
||||
view,
|
||||
colIndex: i,
|
||||
getLinks: fieldLinksSupplier
|
||||
? () =>
|
||||
fieldLinksSupplier({
|
||||
calculatedValue: displayValue,
|
||||
})
|
||||
: () => [],
|
||||
hasLinks: hasLinks(field),
|
||||
});
|
||||
}
|
||||
|
||||
const displayValue = display(field.values.get(j));
|
||||
|
||||
if (displayName !== '') {
|
||||
displayValue.title = replaceVariables(displayName, {
|
||||
...field.state?.scopedVars, // series and field scoped vars
|
||||
...scopedVars,
|
||||
});
|
||||
} else {
|
||||
displayValue.title = getSmartDisplayNameForRow(dataFrame, field, j);
|
||||
}
|
||||
|
||||
values.push({
|
||||
name: '',
|
||||
field: config,
|
||||
display: displayValue,
|
||||
view,
|
||||
colIndex: i,
|
||||
rowIndex: j,
|
||||
getLinks: fieldLinksSupplier
|
||||
? () =>
|
||||
fieldLinksSupplier({
|
||||
valueRowIndex: j,
|
||||
})
|
||||
: () => [],
|
||||
hasLinks: hasLinks(field),
|
||||
});
|
||||
|
||||
if (values.length >= limit) {
|
||||
hitLimit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const results = reduceField({
|
||||
field,
|
||||
reducers: calcs, // The stats to calculate
|
||||
});
|
||||
|
||||
for (const calc of calcs) {
|
||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||
const displayValue = display(results[calc]);
|
||||
|
||||
if (displayName !== '') {
|
||||
displayValue.title = replaceVariables(displayName, {
|
||||
...field.state?.scopedVars, // series and field scoped vars
|
||||
...scopedVars,
|
||||
});
|
||||
} else {
|
||||
displayValue.title = getFieldDisplayName(field, dataFrame, data);
|
||||
}
|
||||
|
||||
let sparkline: FieldSparkline | undefined = undefined;
|
||||
if (options.sparkline) {
|
||||
sparkline = {
|
||||
y: dataFrame.fields[i],
|
||||
x: timeField,
|
||||
};
|
||||
if (calc === ReducerID.last) {
|
||||
sparkline.highlightIndex = sparkline.y.values.length - 1;
|
||||
} else if (calc === ReducerID.first) {
|
||||
sparkline.highlightIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
values.push({
|
||||
name: calc,
|
||||
field: config,
|
||||
display: displayValue,
|
||||
sparkline,
|
||||
view,
|
||||
colIndex: i,
|
||||
getLinks: fieldLinksSupplier
|
||||
? () =>
|
||||
fieldLinksSupplier({
|
||||
calculatedValue: displayValue,
|
||||
})
|
||||
: () => [],
|
||||
hasLinks: hasLinks(field),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +244,32 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
return values;
|
||||
};
|
||||
|
||||
function getSmartDisplayNameForRow(frame: DataFrame, field: Field, rowIndex: number): string {
|
||||
let parts: string[] = [];
|
||||
let otherNumericFields = 0;
|
||||
|
||||
for (const otherField of frame.fields) {
|
||||
if (otherField === field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherField.type === FieldType.string) {
|
||||
const value = otherField.values.get(rowIndex) ?? '';
|
||||
if (value.length > 0) {
|
||||
parts.push(value);
|
||||
}
|
||||
} else if (otherField.type === FieldType.number) {
|
||||
otherNumericFields++;
|
||||
}
|
||||
}
|
||||
|
||||
if (otherNumericFields || parts.length === 0) {
|
||||
parts.push(getFieldDisplayName(field));
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
export function hasLinks(field: Field): boolean {
|
||||
return field.config?.links?.length ? field.config.links.length > 0 : false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user