mirror of
				https://github.com/grafana/grafana.git
				synced 2025-02-25 18:55:37 -06:00 
			
		
		
		
	DataLinks: Respects display name and adds field quoting (#27616)
* DataLinks: Adds field quoting and respects DisplayName * Update public/app/features/panel/panellinks/link_srv.ts Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
This commit is contained in:
		@@ -5,22 +5,23 @@ import coreModule from 'app/core/core_module';
 | 
			
		||||
import { getConfig } from 'app/core/config';
 | 
			
		||||
import {
 | 
			
		||||
  DataFrame,
 | 
			
		||||
  DataLink,
 | 
			
		||||
  DataLinkBuiltInVars,
 | 
			
		||||
  DataLinkClickEvent,
 | 
			
		||||
  deprecationWarning,
 | 
			
		||||
  Field,
 | 
			
		||||
  FieldType,
 | 
			
		||||
  getFieldDisplayName,
 | 
			
		||||
  KeyValue,
 | 
			
		||||
  LinkModel,
 | 
			
		||||
  locationUtil,
 | 
			
		||||
  PanelPlugin,
 | 
			
		||||
  ScopedVars,
 | 
			
		||||
  textUtil,
 | 
			
		||||
  urlUtil,
 | 
			
		||||
  VariableOrigin,
 | 
			
		||||
  VariableSuggestion,
 | 
			
		||||
  VariableSuggestionsScope,
 | 
			
		||||
  urlUtil,
 | 
			
		||||
  textUtil,
 | 
			
		||||
  DataLink,
 | 
			
		||||
  PanelPlugin,
 | 
			
		||||
  DataLinkClickEvent,
 | 
			
		||||
} from '@grafana/data';
 | 
			
		||||
 | 
			
		||||
const timeRangeVars = [
 | 
			
		||||
@@ -75,7 +76,7 @@ const valueVars = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const buildLabelPath = (label: string) => {
 | 
			
		||||
  return label.indexOf('.') > -1 ? `["${label}"]` : `.${label}`;
 | 
			
		||||
  return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
 | 
			
		||||
@@ -126,33 +127,35 @@ const getFieldVars = (dataFrames: DataFrame[]) => {
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDataFrameVars = (dataFrames: DataFrame[]) => {
 | 
			
		||||
export const getDataFrameVars = (dataFrames: DataFrame[]) => {
 | 
			
		||||
  let numeric: Field | undefined = undefined;
 | 
			
		||||
  let title: Field | undefined = undefined;
 | 
			
		||||
  const suggestions: VariableSuggestion[] = [];
 | 
			
		||||
  const keys: KeyValue<true> = {};
 | 
			
		||||
 | 
			
		||||
  for (const df of dataFrames) {
 | 
			
		||||
    for (const f of df.fields) {
 | 
			
		||||
      if (keys[f.name]) {
 | 
			
		||||
  for (const frame of dataFrames) {
 | 
			
		||||
    for (const field of frame.fields) {
 | 
			
		||||
      const displayName = getFieldDisplayName(field, frame, dataFrames);
 | 
			
		||||
 | 
			
		||||
      if (keys[displayName]) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      suggestions.push({
 | 
			
		||||
        value: `__data.fields[${f.name}]`,
 | 
			
		||||
        label: `${f.name}`,
 | 
			
		||||
        documentation: `Formatted value for ${f.name} on the same row`,
 | 
			
		||||
        value: `__data.fields${buildLabelPath(displayName)}`,
 | 
			
		||||
        label: `${displayName}`,
 | 
			
		||||
        documentation: `Formatted value for ${displayName} on the same row`,
 | 
			
		||||
        origin: VariableOrigin.Fields,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      keys[f.name] = true;
 | 
			
		||||
      keys[displayName] = true;
 | 
			
		||||
 | 
			
		||||
      if (!numeric && f.type === FieldType.number) {
 | 
			
		||||
        numeric = f;
 | 
			
		||||
      if (!numeric && field.type === FieldType.number) {
 | 
			
		||||
        numeric = { ...field, name: displayName };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!title && f.config.displayName && f.config.displayName !== f.name) {
 | 
			
		||||
        title = f;
 | 
			
		||||
      if (!title && field.config.displayName && field.config.displayName !== field.name) {
 | 
			
		||||
        title = { ...field, name: displayName };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -168,13 +171,13 @@ const getDataFrameVars = (dataFrames: DataFrame[]) => {
 | 
			
		||||
 | 
			
		||||
  if (numeric) {
 | 
			
		||||
    suggestions.push({
 | 
			
		||||
      value: `__data.fields[${numeric.name}].numeric`,
 | 
			
		||||
      value: `__data.fields${buildLabelPath(numeric.name)}.numeric`,
 | 
			
		||||
      label: `Show numeric value`,
 | 
			
		||||
      documentation: `the numeric field value`,
 | 
			
		||||
      origin: VariableOrigin.Fields,
 | 
			
		||||
    });
 | 
			
		||||
    suggestions.push({
 | 
			
		||||
      value: `__data.fields[${numeric.name}].text`,
 | 
			
		||||
      value: `__data.fields${buildLabelPath(numeric.name)}.text`,
 | 
			
		||||
      label: `Show text value`,
 | 
			
		||||
      documentation: `the text value`,
 | 
			
		||||
      origin: VariableOrigin.Fields,
 | 
			
		||||
@@ -183,7 +186,7 @@ const getDataFrameVars = (dataFrames: DataFrame[]) => {
 | 
			
		||||
 | 
			
		||||
  if (title) {
 | 
			
		||||
    suggestions.push({
 | 
			
		||||
      value: `__data.fields[${title.config.displayName}]`,
 | 
			
		||||
      value: `__data.fields${buildLabelPath(title.name)}`,
 | 
			
		||||
      label: `Select by title`,
 | 
			
		||||
      documentation: `Use the title to pick the field`,
 | 
			
		||||
      origin: VariableOrigin.Fields,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,16 @@
 | 
			
		||||
import { LinkSrv } from '../link_srv';
 | 
			
		||||
import { DataLinkBuiltInVars, locationUtil, VariableModel } from '@grafana/data';
 | 
			
		||||
import { advanceTo } from 'jest-date-mock';
 | 
			
		||||
import {
 | 
			
		||||
  DataLinkBuiltInVars,
 | 
			
		||||
  FieldType,
 | 
			
		||||
  locationUtil,
 | 
			
		||||
  toDataFrame,
 | 
			
		||||
  VariableModel,
 | 
			
		||||
  VariableOrigin,
 | 
			
		||||
} from '@grafana/data';
 | 
			
		||||
 | 
			
		||||
import { getDataFrameVars, LinkSrv } from '../link_srv';
 | 
			
		||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
 | 
			
		||||
import { TemplateSrv } from 'app/features/templating/template_srv';
 | 
			
		||||
import { advanceTo } from 'jest-date-mock';
 | 
			
		||||
import { updateConfig } from '../../../../core/config';
 | 
			
		||||
import { variableAdapters } from '../../../variables/adapters';
 | 
			
		||||
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
 | 
			
		||||
@@ -267,3 +275,212 @@ describe('linkSrv', () => {
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('getDataFrameVars', () => {
 | 
			
		||||
  describe('when called with a DataFrame that contains fields without nested path', () => {
 | 
			
		||||
    it('then it should return correct suggestions', () => {
 | 
			
		||||
      const frame = toDataFrame({
 | 
			
		||||
        name: 'indoor',
 | 
			
		||||
        fields: [
 | 
			
		||||
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
 | 
			
		||||
          { name: 'temperature', type: FieldType.number, values: [10, 11, 12] },
 | 
			
		||||
        ],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const suggestions = getDataFrameVars([frame]);
 | 
			
		||||
 | 
			
		||||
      expect(suggestions).toEqual([
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields.time',
 | 
			
		||||
          label: 'time',
 | 
			
		||||
          documentation: `Formatted value for time on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields.temperature',
 | 
			
		||||
          label: 'temperature',
 | 
			
		||||
          documentation: `Formatted value for temperature on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields[0]`,
 | 
			
		||||
          label: `Select by index`,
 | 
			
		||||
          documentation: `Enter the field order`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields.temperature.numeric`,
 | 
			
		||||
          label: `Show numeric value`,
 | 
			
		||||
          documentation: `the numeric field value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields.temperature.text`,
 | 
			
		||||
          label: `Show text value`,
 | 
			
		||||
          documentation: `the text value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('when called with a DataFrame that contains fields with nested path', () => {
 | 
			
		||||
    it('then it should return correct suggestions', () => {
 | 
			
		||||
      const frame = toDataFrame({
 | 
			
		||||
        name: 'temperatures',
 | 
			
		||||
        fields: [
 | 
			
		||||
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
 | 
			
		||||
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
 | 
			
		||||
        ],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const suggestions = getDataFrameVars([frame]);
 | 
			
		||||
 | 
			
		||||
      expect(suggestions).toEqual([
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields.time',
 | 
			
		||||
          label: 'time',
 | 
			
		||||
          documentation: `Formatted value for time on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields["temperature.indoor"]',
 | 
			
		||||
          label: 'temperature.indoor',
 | 
			
		||||
          documentation: `Formatted value for temperature.indoor on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields[0]`,
 | 
			
		||||
          label: `Select by index`,
 | 
			
		||||
          documentation: `Enter the field order`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["temperature.indoor"].numeric`,
 | 
			
		||||
          label: `Show numeric value`,
 | 
			
		||||
          documentation: `the numeric field value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["temperature.indoor"].text`,
 | 
			
		||||
          label: `Show text value`,
 | 
			
		||||
          documentation: `the text value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('when called with a DataFrame that contains fields with displayName', () => {
 | 
			
		||||
    it('then it should return correct suggestions', () => {
 | 
			
		||||
      const frame = toDataFrame({
 | 
			
		||||
        name: 'temperatures',
 | 
			
		||||
        fields: [
 | 
			
		||||
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
 | 
			
		||||
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
 | 
			
		||||
        ],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };
 | 
			
		||||
 | 
			
		||||
      const suggestions = getDataFrameVars([frame]);
 | 
			
		||||
 | 
			
		||||
      expect(suggestions).toEqual([
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields.time',
 | 
			
		||||
          label: 'time',
 | 
			
		||||
          documentation: `Formatted value for time on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields["Indoor Temperature"]',
 | 
			
		||||
          label: 'Indoor Temperature',
 | 
			
		||||
          documentation: `Formatted value for Indoor Temperature on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields[0]`,
 | 
			
		||||
          label: `Select by index`,
 | 
			
		||||
          documentation: `Enter the field order`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"].numeric`,
 | 
			
		||||
          label: `Show numeric value`,
 | 
			
		||||
          documentation: `the numeric field value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"].text`,
 | 
			
		||||
          label: `Show text value`,
 | 
			
		||||
          documentation: `the text value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"]`,
 | 
			
		||||
          label: `Select by title`,
 | 
			
		||||
          documentation: `Use the title to pick the field`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('when called with a DataFrame that contains fields with duplicate names', () => {
 | 
			
		||||
    it('then it should ignore duplicates', () => {
 | 
			
		||||
      const frame = toDataFrame({
 | 
			
		||||
        name: 'temperatures',
 | 
			
		||||
        fields: [
 | 
			
		||||
          { name: 'time', type: FieldType.time, values: [1, 2, 3] },
 | 
			
		||||
          { name: 'temperature.indoor', type: FieldType.number, values: [10, 11, 12] },
 | 
			
		||||
          { name: 'temperature.outdoor', type: FieldType.number, values: [20, 21, 22] },
 | 
			
		||||
        ],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      frame.fields[1].config = { ...frame.fields[1].config, displayName: 'Indoor Temperature' };
 | 
			
		||||
      // Someone makes a mistake when renaming a field
 | 
			
		||||
      frame.fields[2].config = { ...frame.fields[2].config, displayName: 'Indoor Temperature' };
 | 
			
		||||
 | 
			
		||||
      const suggestions = getDataFrameVars([frame]);
 | 
			
		||||
 | 
			
		||||
      expect(suggestions).toEqual([
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields.time',
 | 
			
		||||
          label: 'time',
 | 
			
		||||
          documentation: `Formatted value for time on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: '__data.fields["Indoor Temperature"]',
 | 
			
		||||
          label: 'Indoor Temperature',
 | 
			
		||||
          documentation: `Formatted value for Indoor Temperature on the same row`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields[0]`,
 | 
			
		||||
          label: `Select by index`,
 | 
			
		||||
          documentation: `Enter the field order`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"].numeric`,
 | 
			
		||||
          label: `Show numeric value`,
 | 
			
		||||
          documentation: `the numeric field value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"].text`,
 | 
			
		||||
          label: `Show text value`,
 | 
			
		||||
          documentation: `the text value`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          value: `__data.fields["Indoor Temperature"]`,
 | 
			
		||||
          label: `Select by title`,
 | 
			
		||||
          documentation: `Use the title to pick the field`,
 | 
			
		||||
          origin: VariableOrigin.Fields,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user