From 8422d1f0f0424f39693a2b4c2bfca9f59a7f9b5f Mon Sep 17 00:00:00 2001 From: rmawatson Date: Thu, 24 Jul 2025 23:32:37 +0000 Subject: [PATCH] Added padding and factored out the templating --- .betterer.results | 3 - .../transformers/calculateField.ts | 30 +++------- .../transformers/formatString.ts | 19 +++---- .../src/transformations/transformers/utils.ts | 56 ++++++++++++++++++- .../app/features/transformers/docs/content.ts | 9 ++- 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/.betterer.results b/.betterer.results index f87310233eb..dfa3e27e33b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -167,9 +167,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], - "packages/grafana-data/src/transformations/transformers/calculateField.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 2e36ce7a193..bb11165793d 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -15,6 +15,7 @@ import { FieldMatcherID } from '../matchers/ids'; import { ensureColumnsTransformer } from './ensureColumns'; import { DataTransformerID } from './ids'; import { noopTransformer } from './noop'; +import { parseTemplateTokens, processTemplateTokens } from './utils'; export enum CalculateFieldMode { ReduceRow = 'reduceRow', @@ -119,6 +120,7 @@ export interface CalculateFieldTransformerOptions { // Output field properties alias?: string; // The output field name // TODO: config?: FieldConfig; or maybe field overrides? since the UI exists + returnType?: FieldType; } type ValuesCreator = (data: DataFrame) => unknown[] | undefined; @@ -257,30 +259,12 @@ export const calculateFieldTransformer: DataTransformerInfo match[1])), - ]; - type FieldData = { fieldName: string; values: any[] }; - + const parsedTemplateTokens = parseTemplateTokens(expression); + options.returnType = FieldType.string; creator = (frame: DataFrame) => { - if (!options.template) { - return undefined; - } - const exprFieldValues: FieldData[] = exprFieldNames.reduce((values: FieldData[], fieldName: string) => { - const field = frame.fields.find((f) => f.name === fieldName); - if (field) { - values.push({ fieldName, values: field.values }); - } - return values; - }, []); - - const outValues = []; + const outValues: string[] = []; for (let i = 0; i < frame.length; i++) { - let rowValue = expression; - exprFieldValues.map(({ fieldName, values }) => { - rowValue = rowValue.replaceAll(`{${fieldName}}`, values[i]); - }); - outValues.push(rowValue); + outValues.push(processTemplateTokens(expression, parsedTemplateTokens, frame.fields, i)); } return outValues; }; @@ -305,7 +289,7 @@ export const calculateFieldTransformer: DataTransformerInfo { - const matches = [...affix.value.matchAll(/\{([\w\d\._-]+)\}/g)]; - matches.forEach((match) => { - const fieldName = match[1]; - const matchingField = allFields.find((field) => field.name === fieldName); - if (matchingField) { - affix.value = affix.value.replace(`{${fieldName}}`, matchingField.values[index]); - } - }); + const prefix = { value: options.stringPrefix ?? '' }; + const suffix = { value: options.stringSuffix ?? '' }; + + [prefix, suffix].forEach((affix) => { + const parsedTemplateTokens = parseTemplateTokens(affix.value); + affix.value = processTemplateTokens(affix.value, parsedTemplateTokens, allFields, index); }); + return prefix.value + value + suffix.value; } }); diff --git a/packages/grafana-data/src/transformations/transformers/utils.ts b/packages/grafana-data/src/transformations/transformers/utils.ts index c10b1e8a1d7..4cc5d7fdabe 100644 --- a/packages/grafana-data/src/transformations/transformers/utils.ts +++ b/packages/grafana-data/src/transformations/transformers/utils.ts @@ -1,5 +1,5 @@ import { BootData } from '../../types/config'; -import { DataFrame } from '../../types/dataFrame'; +import { DataFrame, Field } from '../../types/dataFrame'; declare global { interface Window { @@ -22,3 +22,57 @@ export function findMaxFields(data: DataFrame[]) { return maxFields; } + +export interface TemplateToken { + token: string; + fieldName: string; + fillChar?: string; + alignment?: string; + width: number; +} + +/** + * Extract all template tokens from a string along with any padding modifiers. + */ +export function parseTemplateTokens(templateString: string): TemplateToken[] { + const matchExpr = /{(?[\w\d\._-]+)(?::(?.?(?=[<>^]))?(?[<>^])?(?\d+))?}/g; + return [...templateString.matchAll(matchExpr)].map((match) => ({ + token: match[0], + fieldName: match.groups!.fieldName, + fillChar: match.groups!.fillChar || ' ', + alignment: match.groups!.alignment || '<', + width: parseInt(match.groups!.width, 10), + })); +} + +/** + * Process a template string based on the extracted tokens. + */ +export function processTemplateTokens( + templateString: string, + templateTokens: TemplateToken[], + allFields: Field[], + index: number +) { + let resultString = templateString; + templateTokens.forEach((templateToken) => { + const { token, fieldName, fillChar, alignment, width } = templateToken; + const matchingField = allFields.find((field) => field.name === fieldName); + if (!matchingField) { + return; + } + let replacementValue = matchingField.values[index]?.toString() ?? ''; + if (alignment === '<') { + replacementValue = replacementValue.padEnd(width, fillChar || ' '); + } else if (alignment === '>') { + replacementValue = replacementValue.padStart(width, fillChar || ' '); + } else if (alignment === '^') { + const padding = Math.floor(Math.max(0, width - replacementValue.length) / 2); + replacementValue = replacementValue + .padStart(replacementValue.length + padding, fillChar || ' ') + .padEnd(Number(width), fillChar || ' '); + } + resultString = resultString.replaceAll(token, replacementValue); + }); + return resultString; +} diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index afc0688179c..0fa271db633 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -61,7 +61,12 @@ Use this transformation to add a new field calculated from two other fields. Eac - **Stddev** - Calculates the moving standard deviation. - **Variance** - Calculates the moving variance. - **Row index** - Insert a field with the row index. - - **Template expression** - Insert a field with the value generated from a template expression. The expression can use the values of other fields in the calculation by using the {field} syntax. + - **Template expression** - Insert a field with the value generated from a template expression. The expression can use the values of other fields in the calculation by using the {field} syntax, with optional modifiers for padding. + - **No alignment** - Pastes the contents of 'field': {field} + - **Left alignment** - Pastes the contents of 'field' aligned left with default padding character, padded to 10 characters: {field:<10} + - **Right alignment** - Pastes the contents of 'field' aligned right with default padding character, padded to 10 characters: {field:>10} + - **Center alignment** - Pastes the contents of 'field' center aligned with default padding character, padded to 10 characters: {field:^10} + - **Custom padding character** - Pastes the contents of 'field' center aligned with an underscore padding character, padded to 10 characters: {field:_^10} - **Field name** - Select the names of fields you want to use in the calculation for the new field. - **Calculation** - If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. - **Operation** - If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. @@ -516,7 +521,7 @@ Use this transformation to customize the output of a string field. This transfor - **Kebab case** - Formats all characters in the string in lowercase and uses dashes instead of spaces between words. - **Trim** - Removes all leading and trailing spaces from the string. - **Substring** - Returns a substring of the string, using the specified start and end positions. -- **Affix** - Add a prefix or suffix string to the string. Allows pasting the value of other fields when provided {field} as the affix expression. +- **Affix** - Add a prefix or suffix to the field. Allows pasting of values of other fields using {fieldName} with padding modifiers . This transformation provides a convenient way to standardize and tailor the presentation of string data for better visualization and analysis.`; },