Added padding and factored out the templating

This commit is contained in:
rmawatson
2025-07-24 23:33:52 +00:00
parent e2da156c41
commit 8422d1f0f0
5 changed files with 77 additions and 40 deletions
-3
View File
@@ -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"]
@@ -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<CalculateFieldTransf
});
case CalculateFieldMode.TemplateExpression:
const expression = options.template?.expression ?? '';
const exprFieldNames = [
...new Set([...expression.matchAll(/\{([\w\d\._-]+)\}/g)].map((match) => 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<CalculateFieldTransf
const field: Field = {
name: getNameFromOptions(options),
type: FieldType.number,
type: options.returnType ?? FieldType.number,
config: {},
values,
};
@@ -6,6 +6,7 @@ import { fieldMatchers } from '../matchers';
import { FieldMatcherID } from '../matchers/ids';
import { DataTransformerID } from './ids';
import { parseTemplateTokens, processTemplateTokens } from './utils';
export enum FormatStringOutput {
UpperCase = 'Upper Case',
@@ -64,18 +65,14 @@ export const getFormatStringFunction = (options: FormatStringTransformerOptions)
case FormatStringOutput.Substring:
return value.substring(options.substringStart, options.substringEnd);
case FormatStringOutput.Affix:
let prefix = { value: options.stringPrefix ?? '' };
let suffix = { value: options.stringSuffix ?? '' };
[prefix, suffix].map((affix) => {
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;
}
});
@@ -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 = /{(?<fieldName>[\w\d\._-]+)(?::(?<fillChar>.?(?=[<>^]))?(?<alignment>[<>^])?(?<width>\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;
}
@@ -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.`;
},