diff --git a/packages/jaeger-ui/src/utils/link-formatting.test.js b/packages/jaeger-ui/src/utils/link-formatting.test.js index 149ca934d1..23e68dd16d 100644 --- a/packages/jaeger-ui/src/utils/link-formatting.test.js +++ b/packages/jaeger-ui/src/utils/link-formatting.test.js @@ -56,6 +56,63 @@ describe('getParameterAndFormatter()', () => { }); }); + describe('add', () => { + test.each([1000, -1000])('offset: %s', offset => { + const result = getParameterAndFormatter(`startTime | add ${offset}`); + expect(result).toEqual({ + parameterName: 'startTime', + formatFunction: expect.any(Function), + }); + const startTime = new Date('2020-01-01').getTime() * 1000; + expect(result.formatFunction(startTime)).toEqual(startTime + offset); + }); + + test('Invalid value', () => { + const result = getParameterAndFormatter(`startTime | add 1000`); + expect(result.formatFunction('invalid')).toEqual('invalid'); + }); + + test('Invalid offset', () => { + const result = getParameterAndFormatter('startTime | add invalid'); + const startTime = new Date('2020-01-01').getTime() * 1000; + expect(result.formatFunction(startTime)).toEqual(startTime); + }); + }); + + describe('Chaining formatting functions', () => { + test.each(['', ' ', ' ', ' '])( + 'add and epoch_micros_to_date_iso - delimeter: %p', + spaceChars => { + const expression = ['startTime', 'add 60000000', 'epoch_micros_to_date_iso'].join( + `${spaceChars}|${spaceChars}` + ); + const result = getParameterAndFormatter(expression); + expect(result).toEqual({ + parameterName: 'startTime', + formatFunction: expect.any(Function), + }); + + const startTime = new Date('2020-01-01').getTime() * 1000; // Convert to microseconds + const expectedDate = new Date('2020-01-01T00:01:00.000Z').toISOString(); + expect(result.formatFunction(startTime)).toEqual(expectedDate); + } + ); + + test.each([' ', ' ', ' '])( + 'add and epoch_micros_to_date_iso with extra spaces between functions and arguments - delimeter: %', + spaceChars => { + const expression = [`startTime | add${spaceChars}60000000 | epoch_micros_to_date_iso`].join( + `${spaceChars}|${spaceChars}` + ); + const result = getParameterAndFormatter(expression); + + const startTime = new Date('2020-01-01').getTime() * 1000; // Convert to microseconds + const expectedDate = new Date('2020-01-01T00:01:00.000Z').toISOString(); + expect(result.formatFunction(startTime)).toEqual(expectedDate); + } + ); + }); + test('No function', () => { const result = getParameterAndFormatter('startTime'); expect(result).toEqual({ diff --git a/packages/jaeger-ui/src/utils/link-formatting.tsx b/packages/jaeger-ui/src/utils/link-formatting.tsx index 299e6005db..feea355c72 100644 --- a/packages/jaeger-ui/src/utils/link-formatting.tsx +++ b/packages/jaeger-ui/src/utils/link-formatting.tsx @@ -14,66 +14,98 @@ import { Trace } from '../types/trace'; -function getFormatFunctions(): Record< +const formatFunctions: Record< string, - (value: T, ...args: string[]) => string | T -> { - return { - epoch_micros_to_date_iso: microsSinceEpoch => { - if (typeof microsSinceEpoch !== 'number') { - console.error('epoch_micros_to_date_iso can only operate on numbers, ignoring formatting', { - value: microsSinceEpoch, - }); - return microsSinceEpoch; - } + (value: T, ...args: string[]) => T | string | number +> = { + epoch_micros_to_date_iso: microsSinceEpoch => { + if (typeof microsSinceEpoch !== 'number') { + console.error('epoch_micros_to_date_iso() can only operate on numbers, ignoring formatting', { + value: microsSinceEpoch, + }); + return microsSinceEpoch; + } - return new Date(microsSinceEpoch / 1000).toISOString(); - }, - pad_start: (value, desiredLengthString: string, padCharacter: string) => { - if (typeof value !== 'string') { - console.error('pad_start can only operate on strings, ignoring formatting', { - value, - desiredLength: desiredLengthString, - padCharacter, - }); - return value; - } - const desiredLength = parseInt(desiredLengthString, 10); - if (Number.isNaN(desiredLength)) { - console.error('pad_start needs a desired length as second argument, ignoring formatting', { - value, - desiredLength: desiredLengthString, - padCharacter, - }); - } + return new Date(microsSinceEpoch / 1000).toISOString(); + }, + pad_start: (value, desiredLengthString: string, padCharacter: string) => { + if (typeof value !== 'string') { + console.error('pad_start() can only operate on strings, ignoring formatting', { + value, + desiredLength: desiredLengthString, + padCharacter, + }); + return value; + } + const desiredLength = parseInt(desiredLengthString, 10); + if (Number.isNaN(desiredLength)) { + console.error('pad_start() needs a desired length as second argument, ignoring formatting', { + value, + desiredLength: desiredLengthString, + padCharacter, + }); + } - return value.padStart(desiredLength, padCharacter); - }, - }; -} + return value.padStart(desiredLength, padCharacter); + }, + + add: (value, offsetString: string) => { + if (typeof value !== 'number') { + console.error('add() needs a numeric offset as an argument, ignoring formatting', { + value, + offsetString, + }); + return value; + } + + const offset = parseInt(offsetString, 10); + if (Number.isNaN(offset)) { + console.error('add() needs a valid offset in microseconds as second argument, ignoring formatting', { + value, + offsetString, + }); + return value; + } + + return value + offset; + }, +}; export function getParameterAndFormatter( parameter: string ): { parameterName: string; - formatFunction: ((value: T) => T | string) | null; + formatFunction: ((value: T) => T | string | number) | null; } { - const parts = parameter.split('|').map(part => part.trim()); - const parameterName = parts[0]; - if (parts.length === 1) return { parameterName, formatFunction: null }; + const [parameterName, ...formatStrings] = parameter.split('|').map(part => part.trim()); - const [formatFunctionName, ...args] = parts[1].split(' '); + // const formatFunctions = getFormatFunctions(); - const formatFunctions = getFormatFunctions(); + const formatters = formatStrings + .map(formatString => { + const [formatFunctionName, ...args] = formatString.split(/ +/); + const formatFunction = formatFunctions[formatFunctionName] as + | ((value: T, ...args: string[]) => T | string | number) + | undefined; + if (!formatFunction) { + console.error( + 'Unrecognized format function name, ignoring formatting. Other formatting functions may be applied', + { + parameter, + formatFunctionName, + validValues: Object.keys(formatFunctions), + } + ); + return null; + } + return (val: T) => formatFunction(val, ...args); + }) + .filter((fn): fn is NonNullable => fn != null); - const formatFunction = formatFunctions[formatFunctionName]; - if (!formatFunction) { - console.error('Unrecognized format function name, ignoring formatting', { - parameter, - formatFunctionName, - validValues: Object.keys(formatFunctions), - }); - } + const chainedFormatFunction = (value: T) => formatters.reduce((acc, fn) => fn(acc) as T, value); - return { parameterName, formatFunction: formatFunction ? val => formatFunction(val, ...args) : null }; + return { + parameterName, + formatFunction: formatters.length ? chainedFormatFunction : null, + }; }