From 00681578cc7bb3de6dbc018b0a57ac9999663ec8 Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 17 Oct 2026 03:36:27 +0200 Subject: [PATCH] Upstream grafana/grafana state of the touched files before 5492c79366 --- .../matchers/nameMatcher.test.ts | 485 ++++++++++++++++++ .../transformations/matchers/nameMatcher.ts | 243 +++++++++ 2 files changed, 728 insertions(+) create mode 100644 packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts create mode 100644 packages/grafana-data/src/transformations/matchers/nameMatcher.ts diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts new file mode 100644 index 0000000..00b9d44 --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts @@ -0,0 +1,485 @@ +import { toDataFrame } from '../../dataframe/processDataFrame'; +import { type DataFrame, FieldType } from '../../types/dataFrame'; +import { getFieldMatcher } from '../matchers'; + +import { FieldMatcherID } from './ids'; +import { ByNamesMatcherMode } from './nameMatcher'; + +describe('Field Name by Regexp Matcher', () => { + it('Match all with wildcard regex', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexp, + options: '/.*/', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Match all with decimals regex', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexp, + options: '/^\\d+$/', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Match complex regex', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexp, + options: '/\\b(?:\\S+?\\.)+\\S+\\b$/', + }; + + const matcher = getFieldMatcher(config); + let resultCount = 0; + for (const field of seriesWithNames.fields) { + if (matcher(field, seriesWithNames, [seriesWithNames])) { + resultCount++; + } + expect(resultCount).toBe(1); + } + }); +}); + +describe('Field Name Matcher', () => { + it('Match only exact name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byName, + options: 'C', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match should respect letter case', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byName, + options: 'c', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match name or displayName', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'a', config: { displayName: 'LongerName' } }], + }); + const field = seriesWithNames.fields[0]; + expect( + getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'c', + })(field, seriesWithNames, [seriesWithNames]) + ).toBe(false); // No match + + expect( + getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'a', + })(field, seriesWithNames, [seriesWithNames]) + ).toBe(true); + + expect( + getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'LongerName', + })(field, seriesWithNames, [seriesWithNames]) + ).toBe(true); + }); + + it('Match none of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byName, + options: '', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); +}); + +describe('Field Multiple Names Matcher', () => { + it('Match only exact name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match should default to include mode', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match should respect letter case', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['c'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match none of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: [], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match all of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['some.instance.path', '112', '13'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Match all but supplied names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.exclude, + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name !== 'C'); + } + }); +}); + +describe('Field Regexp or Names Matcher', () => { + it('Match only exact name by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match all starting with AA', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/^AA/', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'AAA'); + } + }); + + it('Match all starting with AA and C', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/^AA/', + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'AAA' || field.name === 'C'); + } + }); + + it('Match should respect letter case by name if not igored in pattern', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['c'], + pattern: '/c/i', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match none of the field names by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: [], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match all of the field names by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['some.instance.path', '112', '13'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Match all of the field names by regexp', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/.*/', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Support fallback name matchers', () => { + const frame: DataFrame = { + fields: [ + { name: 'time', type: FieldType.time, config: {}, values: [1, 2] }, + { + name: 'UP', + type: FieldType.number, + config: {}, + values: [1, 2], + labels: { __name__: 'UP' }, + }, + ], + name: 'X', + length: 2, + }; + + let matcher = getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'Value', + }); + expect(matcher(frame.fields[0], frame, [])).toBeFalsy(); + expect(matcher(frame.fields[1], frame, [])).toBeTruthy(); + + matcher = getFieldMatcher({ + id: FieldMatcherID.byName, + options: 'Time', + }); + expect(matcher(frame.fields[0], frame, [])).toBeTruthy(); + expect(matcher(frame.fields[1], frame, [])).toBeFalsy(); + }); +}); + +it('Support fallback multiple names matchers', () => { + const frame: DataFrame = { + fields: [ + { name: 'time', type: FieldType.time, config: {}, values: [1, 2] }, + { + name: 'UP', + type: FieldType.number, + config: {}, + values: [1, 2], + labels: { __name__: 'UP' }, + }, + ], + name: 'X', + length: 2, + }; + + let matcher = getFieldMatcher({ + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['Value'], + }, + }); + expect(matcher(frame.fields[0], frame, [])).toBeFalsy(); + expect(matcher(frame.fields[1], frame, [])).toBeTruthy(); + + matcher = getFieldMatcher({ + id: FieldMatcherID.byNames, + options: { + mode: ByNamesMatcherMode.include, + names: ['Time'], + }, + }); + expect(matcher(frame.fields[0], frame, [])).toBeTruthy(); + expect(matcher(frame.fields[1], frame, [])).toBeFalsy(); +}); + +describe('Fields returned by query with refId', () => { + it('Match all fields in frame with refId: A', () => { + const data = [ + toDataFrame({ + refId: 'A', + fields: [{ name: 'field_1' }, { name: 'field_2' }], + }), + toDataFrame({ + refId: 'B', + fields: [{ name: 'field_1' }, { name: 'field_2' }], + }), + ]; + + const matcher = getFieldMatcher({ + id: FieldMatcherID.byFrameRefID, + options: 'A', + }); + + const frameA = data[0]; + expect(matcher(frameA.fields[0], frameA, data)).toBe(true); + expect(matcher(frameA.fields[1], frameA, data)).toBe(true); + + const frameB = data[1]; + expect(matcher(frameB.fields[0], frameB, data)).toBe(false); + expect(matcher(frameB.fields[1], frameB, data)).toBe(false); + }); +}); diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts new file mode 100644 index 0000000..2ab10da --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -0,0 +1,243 @@ +import { getFieldDisplayName } from '../../field/fieldState'; +import { stringToJsRegex } from '../../text/string'; +import { type DataFrame, type Field, FieldType, TIME_SERIES_VALUE_FIELD_NAME } from '../../types/dataFrame'; +import { type FieldMatcher, type FieldMatcherInfo, type FrameMatcherInfo } from '../../types/transformations'; + +import { FieldMatcherID, FrameMatcherID } from './ids'; + +export interface RegexpOrNamesMatcherOptions { + pattern?: string; + names?: string[]; + variable?: string; +} + +/** + * Mode to be able to toggle if the names matcher should match fields in provided + * list or all except provided names. + * @public + */ +export enum ByNamesMatcherMode { + exclude = 'exclude', + include = 'include', +} + +/** + * Options to instruct the by names matcher to either match all fields in given list + * or all except the fields in the list. + * @public + */ +export interface ByNamesMatcherOptions { + mode?: ByNamesMatcherMode; + names?: string[]; + readOnly?: boolean; + prefix?: string; +} + +// General Field matcher +const fieldNameMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byName, + name: 'Field Name', + description: 'match the field name', + defaultOptions: '', + + get: (name: string): FieldMatcher => { + const uniqueNames = new Set([name]); + + const fallback = fieldNameFallback(uniqueNames); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return ( + name === field.name || + name === getFieldDisplayName(field, frame, allFrames) || + Boolean(fallback && fallback(field, frame, allFrames)) + ); + }; + }, + + getOptionsDisplayText: (name: string) => { + return `Field name: ${name}`; + }, +}; + +const multipleFieldNamesMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byNames, + name: 'Field Names', + description: 'match any of the given the field names', + defaultOptions: { + mode: ByNamesMatcherMode.include, + names: [], + }, + + get: (options: ByNamesMatcherOptions): FieldMatcher => { + const { names, mode = ByNamesMatcherMode.include } = options; + const uniqueNames = new Set(names ?? []); + + const fallback = fieldNameFallback(uniqueNames); + + const matcher = (field: Field, frame: DataFrame, frames: DataFrame[]) => { + return ( + uniqueNames.has(field.name) || + uniqueNames.has(getFieldDisplayName(field, frame, frames)) || + Boolean(fallback && fallback(field, frame, frames)) + ); + }; + + if (mode === ByNamesMatcherMode.exclude) { + return (field: Field, frame: DataFrame, frames: DataFrame[]) => { + return !matcher(field, frame, frames); + }; + } + return matcher; + }, + + getOptionsDisplayText: (options: ByNamesMatcherOptions): string => { + const { names, mode } = options; + const displayText = (names ?? []).join(', '); + if (mode === ByNamesMatcherMode.exclude) { + return `All except: ${displayText}`; + } + return `All of: ${displayText}`; + }, +}; + +// In an effort to support migrating to a consistent data contract, the +// naming conventions need to get normalized. However, many existing setups +// exist that would no longer match names if that changes. This injects +// fallback logic when the data frame has not type version specified +function fieldNameFallback(fields: Set) { + let fallback: FieldMatcher | undefined = undefined; + + if (fields.has(TIME_SERIES_VALUE_FIELD_NAME)) { + fallback = (field: Field, frame: DataFrame) => { + return ( + Boolean(field.labels) && // Value was reasonable when the name was set in labels or on the frame + field.labels?.__name__ === field.name + ); + }; + } else if (fields.has('Time') || fields.has('time')) { + fallback = (field: Field, frame: DataFrame) => { + return frame.meta?.typeVersion == null && field.type === FieldType.time; + }; + } + + return fallback; +} + +const regexpFieldNameMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byRegexp, + name: 'Field Name by Regexp', + description: 'match the field name by a given regexp pattern', + defaultOptions: '/.*/', + + get: (pattern: string): FieldMatcher => { + const regexp = patternToRegex(pattern); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + const displayName = getFieldDisplayName(field, frame, allFrames); + return !!regexp && regexp.test(displayName); + }; + }, + + getOptionsDisplayText: (pattern: string): string => { + return `Field name by pattern: ${pattern}`; + }, +}; + +/** + * Field matcher that will match all fields that exists in a + * data frame with configured refId. + * @public + */ +const fieldsInFrameMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byFrameRefID, + name: 'Fields by frame refId', + description: 'match all fields returned in data frame with refId.', + defaultOptions: '', + + get: (refId: string): FieldMatcher => { + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return frame.refId === refId; + }; + }, + + getOptionsDisplayText: (refId: string): string => { + return `Math all fields returned by query with reference ID: ${refId}`; + }, +}; + +const regexpOrMultipleNamesMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byRegexpOrNames, + name: 'Field Name by Regexp or Names', + description: 'match the field name by a given regexp pattern or given names', + defaultOptions: { + pattern: '/.*/', + names: [], + }, + + get: (options: RegexpOrNamesMatcherOptions): FieldMatcher => { + const regexpMatcher = regexpFieldNameMatcher.get(options?.pattern || ''); + const namesMatcher = multipleFieldNamesMatcher.get({ + mode: ByNamesMatcherMode.include, + names: options?.names ?? [], + }); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return namesMatcher(field, frame, allFrames) || regexpMatcher(field, frame, allFrames); + }; + }, + + getOptionsDisplayText: (options: RegexpOrNamesMatcherOptions): string => { + const pattern = options?.pattern ?? ''; + const names = options?.names?.join(',') ?? ''; + return `Field name by pattern: ${pattern} or names: ${names}`; + }, +}; + +const patternToRegex = (pattern?: string): RegExp | undefined => { + if (!pattern) { + return undefined; + } + + try { + return stringToJsRegex(pattern); + } catch (error) { + console.error(error); + return undefined; + } +}; + +// General Frame matcher +const frameNameMatcher: FrameMatcherInfo = { + id: FrameMatcherID.byName, + name: 'Frame Name', + description: 'match the frame name', + defaultOptions: '/.*/', + + get: (pattern: string) => { + const regex = stringToJsRegex(pattern); + return (frame: DataFrame) => { + return regex.test(frame.name || ''); + }; + }, + + getOptionsDisplayText: (pattern: string) => { + return `Frame name: ${pattern}`; + }, +}; + +/** + * Registry Initialization + */ +export function getFieldNameMatchers(): FieldMatcherInfo[] { + return [ + fieldNameMatcher, + regexpFieldNameMatcher, + multipleFieldNamesMatcher, + regexpOrMultipleNamesMatcher, + fieldsInFrameMatcher, + ]; +} + +export function getFrameNameMatchers(): FrameMatcherInfo[] { + return [frameNameMatcher]; +}