From 4922834b1e202331ea2d9a46baf1260e3efba9e5 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 14 Oct 2020 14:37:42 +0200 Subject: [PATCH 1/5] basic url drilldown template helpers --- .../url_drilldown/url_template.test.ts | 110 ++++++++++++++++++ .../drilldowns/url_drilldown/url_template.ts | 58 +++++++++ 2 files changed, 168 insertions(+) diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 64b8cc49292b3..099567ec53f6e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -139,3 +139,113 @@ describe('date helper', () => { ); }); }); + +describe('formatNumber helper', () => { + test('formats string numbers', () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: '32.9999' })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`); + expect(compile(url, { value: '32.555' })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`); + }); + + test('formats numbers', () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: 32.9999 })).toMatchInlineSnapshot(`"https://elastic.co/33.0"`); + expect(compile(url, { value: 32.555 })).toMatchInlineSnapshot(`"https://elastic.co/32.6"`); + }); + + test("doesn't fail on Nan", () => { + const url = 'https://elastic.co/{{formatNumber value "0.0"}}'; + expect(compile(url, { value: null })).toMatchInlineSnapshot(`"https://elastic.co/"`); + expect(compile(url, { value: undefined })).toMatchInlineSnapshot(`"https://elastic.co/"`); + expect(compile(url, { value: 'not a number' })).toMatchInlineSnapshot( + `"https://elastic.co/not%20a%20number"` + ); + }); + + test('fails on missing format string', () => { + const url = 'https://elastic.co/{{formatNumber value}}'; + expect(() => compile(url, { value: 12 })).toThrowError(); + }); + + // this doesn't work and doesn't seem + // possible to validate with our version of numeral + test.skip('fails on malformed format string', () => { + const url = 'https://elastic.co/{{formatNumber value "not a real format string"}}'; + expect(() => compile(url, { value: 12 })).toThrowError(); + }); +}); + +describe('match helper', () => { + test('matches RegExp and uses capture group', () => { + const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}'; + + expect(compile(url, { value: 'Label:Feature:Something' })).toMatchInlineSnapshot( + `"https://elastic.co/Feature:Something"` + ); + }); + + test('no matches', () => { + const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}'; + + expect(compile(url, { value: 'No matches' })).toMatchInlineSnapshot(`"https://elastic.co/"`); + }); +}); + +describe('basic string formatting helpers', () => { + test('lowercase', () => { + const compileUrl = (value: unknown) => + compile('https://elastic.co/{{lowercase value}}', { value }); + + expect(compileUrl('Some String Value')).toMatchInlineSnapshot( + `"https://elastic.co/some%20string%20value"` + ); + expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`); + expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/null"`); + }); + test('uppercase', () => { + const compileUrl = (value: unknown) => + compile('https://elastic.co/{{uppercase value}}', { value }); + + expect(compileUrl('Some String Value')).toMatchInlineSnapshot( + `"https://elastic.co/SOME%20STRING%20VALUE"` + ); + expect(compileUrl(4)).toMatchInlineSnapshot(`"https://elastic.co/4"`); + expect(compileUrl(null)).toMatchInlineSnapshot(`"https://elastic.co/NULL"`); + }); + test('trim', () => { + const compileUrl = (fn: 'trim' | 'trimLeft' | 'trimRight', value: unknown) => + compile(`https://elastic.co/{{${fn} value}}`, { value }); + + expect(compileUrl('trim', ' trim-me ')).toMatchInlineSnapshot(`"https://elastic.co/trim-me"`); + expect(compileUrl('trimRight', ' trim-me ')).toMatchInlineSnapshot( + `"https://elastic.co/%20%20trim-me"` + ); + expect(compileUrl('trimLeft', ' trim-me ')).toMatchInlineSnapshot( + `"https://elastic.co/trim-me%20%20"` + ); + }); + test('left,right,mid', () => { + const compileExpression = (expression: string, value: unknown) => + compile(`https://elastic.co/${expression}`, { value }); + + expect(compileExpression('{{left value 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/123"` + ); + expect(compileExpression('{{right value 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/345"` + ); + expect(compileExpression('{{mid value 1 3}}', '12345')).toMatchInlineSnapshot( + `"https://elastic.co/234"` + ); + }); + + test('concat', () => { + expect( + compile(`https://elastic.co/{{concat value1 "," value2}}`, { value1: 'v1', value2: 'v2' }) + ).toMatchInlineSnapshot(`"https://elastic.co/v1,v2"`); + + expect( + compile(`https://elastic.co/{{concat valueArray}}`, { valueArray: ['1', '2', '3'] }) + ).toMatchInlineSnapshot(`"https://elastic.co/1,2,3"`); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index 2c3537636b9da..393564d78ba50 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -8,6 +8,7 @@ import { create as createHandlebars, HelperDelegate, HelperOptions } from 'handl import { encode, RisonValue } from 'rison-node'; import dateMath from '@elastic/datemath'; import moment, { Moment } from 'moment'; +import numeral from '@elastic/numeral'; const handlebars = createHandlebars(); @@ -69,6 +70,63 @@ handlebars.registerHelper('date', (...args) => { return format ? momentDate.format(format) : momentDate.toISOString(); }); +handlebars.registerHelper('formatNumber', (rawValue: unknown, pattern: string) => { + if (!pattern || typeof pattern !== 'string') + throw new Error(`[formatNumber]: pattern string is required`); + const value = Number(rawValue); + if (rawValue == null || Number.isNaN(value)) return rawValue; + return numeral(value).format(pattern); +}); + +/** + * Allows to match regex patterns and extract capturing groups. + * Result is array of arrays. + * + * @example + * + * Have a string: "Label:Feature:Something" + * and want to extract: "Feature:Something" + * + * expression: `{{match value "Label:(.*)"}}`, + * returns: [["Label:Feature:Something", "Feature:Something"]] + */ +handlebars.registerHelper('match', (rawValue: unknown, regexpString: string) => { + if (!regexpString || typeof regexpString !== 'string') + throw new Error(`[match]: regexp string is required`); + const regexp = new RegExp(regexpString, 'g'); + const valueString = String(rawValue); + return Array.from(valueString.matchAll(regexp)); +}); + +function toString(value: unknown): string { + return String(value); +} +handlebars.registerHelper('lowercase', (rawValue: unknown) => toString(rawValue).toLowerCase()); +handlebars.registerHelper('uppercase', (rawValue: unknown) => toString(rawValue).toUpperCase()); +handlebars.registerHelper('trim', (rawValue: unknown) => toString(rawValue).trim()); +handlebars.registerHelper('trimLeft', (rawValue: unknown) => toString(rawValue).trimLeft()); +handlebars.registerHelper('trimRight', (rawValue: unknown) => toString(rawValue).trimRight()); +handlebars.registerHelper('concat', (...args) => { + const values = args.slice(0, -1) as unknown[]; + return values.join(''); +}); + +handlebars.registerHelper('left', (rawValue: unknown, numberOfChars: number) => { + if (typeof numberOfChars !== 'number') + throw new Error('[left]: expected "number of characters to extract" to be a number'); + return toString(rawValue).slice(0, numberOfChars); +}); +handlebars.registerHelper('right', (rawValue: unknown, numberOfChars: number) => { + if (typeof numberOfChars !== 'number') + throw new Error('[left]: expected "number of characters to extract" to be a number'); + return toString(rawValue).slice(-1 * numberOfChars); +}); +handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: number) => { + if (typeof start !== 'number') throw new Error('[left]: expected "start" to be a number'); + if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number'); + return toString(rawValue).substr(start, length); +}); + export function compile(url: string, context: object): string { const template = handlebars.compile(url, { strict: true, noEscape: true }); return encodeURI(template(context)); From 62ad2a1c1645f334574df586029a319606367e17 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 16 Oct 2020 16:44:09 +0200 Subject: [PATCH 2/5] review and remove match helper --- .../url_drilldown/url_template.test.ts | 16 -------- .../drilldowns/url_drilldown/url_template.ts | 39 ++++--------------- 2 files changed, 8 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 099567ec53f6e..09f5a089aa1ef 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -175,22 +175,6 @@ describe('formatNumber helper', () => { }); }); -describe('match helper', () => { - test('matches RegExp and uses capture group', () => { - const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}'; - - expect(compile(url, { value: 'Label:Feature:Something' })).toMatchInlineSnapshot( - `"https://elastic.co/Feature:Something"` - ); - }); - - test('no matches', () => { - const url = 'https://elastic.co/{{lookup (lookup (match value "Label:(.*)") 0) 1}}'; - - expect(compile(url, { value: 'No matches' })).toMatchInlineSnapshot(`"https://elastic.co/"`); - }); -}); - describe('basic string formatting helpers', () => { test('lowercase', () => { const compileUrl = (value: unknown) => diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index 393564d78ba50..d5b982ceceb9c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -78,34 +78,11 @@ handlebars.registerHelper('formatNumber', (rawValue: unknown, pattern: string) = return numeral(value).format(pattern); }); -/** - * Allows to match regex patterns and extract capturing groups. - * Result is array of arrays. - * - * @example - * - * Have a string: "Label:Feature:Something" - * and want to extract: "Feature:Something" - * - * expression: `{{match value "Label:(.*)"}}`, - * returns: [["Label:Feature:Something", "Feature:Something"]] - */ -handlebars.registerHelper('match', (rawValue: unknown, regexpString: string) => { - if (!regexpString || typeof regexpString !== 'string') - throw new Error(`[match]: regexp string is required`); - const regexp = new RegExp(regexpString, 'g'); - const valueString = String(rawValue); - return Array.from(valueString.matchAll(regexp)); -}); - -function toString(value: unknown): string { - return String(value); -} -handlebars.registerHelper('lowercase', (rawValue: unknown) => toString(rawValue).toLowerCase()); -handlebars.registerHelper('uppercase', (rawValue: unknown) => toString(rawValue).toUpperCase()); -handlebars.registerHelper('trim', (rawValue: unknown) => toString(rawValue).trim()); -handlebars.registerHelper('trimLeft', (rawValue: unknown) => toString(rawValue).trimLeft()); -handlebars.registerHelper('trimRight', (rawValue: unknown) => toString(rawValue).trimRight()); +handlebars.registerHelper('lowercase', (rawValue: unknown) => String(rawValue).toLowerCase()); +handlebars.registerHelper('uppercase', (rawValue: unknown) => String(rawValue).toUpperCase()); +handlebars.registerHelper('trim', (rawValue: unknown) => String(rawValue).trim()); +handlebars.registerHelper('trimLeft', (rawValue: unknown) => String(rawValue).trimLeft()); +handlebars.registerHelper('trimRight', (rawValue: unknown) => String(rawValue).trimRight()); handlebars.registerHelper('concat', (...args) => { const values = args.slice(0, -1) as unknown[]; return values.join(''); @@ -114,17 +91,17 @@ handlebars.registerHelper('concat', (...args) => { handlebars.registerHelper('left', (rawValue: unknown, numberOfChars: number) => { if (typeof numberOfChars !== 'number') throw new Error('[left]: expected "number of characters to extract" to be a number'); - return toString(rawValue).slice(0, numberOfChars); + return String(rawValue).slice(0, numberOfChars); }); handlebars.registerHelper('right', (rawValue: unknown, numberOfChars: number) => { if (typeof numberOfChars !== 'number') throw new Error('[left]: expected "number of characters to extract" to be a number'); - return toString(rawValue).slice(-1 * numberOfChars); + return String(rawValue).slice(-numberOfChars); }); handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: number) => { if (typeof start !== 'number') throw new Error('[left]: expected "start" to be a number'); if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number'); - return toString(rawValue).substr(start, length); + return String(rawValue).substr(start, length); }); export function compile(url: string, context: object): string { From 4213b66ff971e40c200fa18b4f5cd34381fedac1 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 16 Oct 2020 16:59:09 +0200 Subject: [PATCH 3/5] replace helper --- .../url_drilldown/url_template.test.ts | 49 +++++++++++++++++++ .../drilldowns/url_drilldown/url_template.ts | 8 +++ 2 files changed, 57 insertions(+) diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 09f5a089aa1ef..05a1a6de4ee8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -175,6 +175,55 @@ describe('formatNumber helper', () => { }); }); +describe('replace helper', () => { + test('replaces all occurrences', () => { + const url = 'https://elastic.co/{{replace value "replace-me" "with-me"}}'; + + expect(compile(url, { value: 'replace-me test replace-me' })).toMatchInlineSnapshot( + `"https://elastic.co/with-me%20test%20with-me"` + ); + }); + + test('can be used to remove a substring', () => { + const url = 'https://elastic.co/{{replace value "Label:" ""}}'; + + expect(compile(url, { value: 'Label:Feature:Something' })).toMatchInlineSnapshot( + `"https://elastic.co/Feature:Something"` + ); + }); + + test('works if no matches', () => { + const url = 'https://elastic.co/{{replace value "Label:" ""}}'; + + expect(compile(url, { value: 'No matches' })).toMatchInlineSnapshot( + `"https://elastic.co/No%20matches"` + ); + }); + + test('throws on incorrect args', () => { + expect(() => + compile('https://elastic.co/{{replace value "Label:"}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value "Label:" 4}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value 4 ""}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + expect(() => + compile('https://elastic.co/{{replace value}}', { value: 'No matches' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[replace]: \\"searchString\\" and \\"valueString\\" parameters expected to be strings, but not a string or missing"` + ); + }); +}); + describe('basic string formatting helpers', () => { test('lowercase', () => { const compileUrl = (value: unknown) => diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index d5b982ceceb9c..9b1ff645b7a64 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -103,6 +103,14 @@ handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: numb if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number'); return String(rawValue).substr(start, length); }); +handlebars.registerHelper('replace', (...args) => { + const [str, searchString, valueString] = args.slice(0, -1) as [string, string, string]; + if (typeof searchString !== 'string' || typeof valueString !== 'string') + throw new Error( + '[replace]: "searchString" and "valueString" parameters expected to be strings, but not a string or missing' + ); + return String(str).split(searchString).join(valueString); +}); export function compile(url: string, context: object): string { const template = handlebars.compile(url, { strict: true, noEscape: true }); From d1d92d0830a4d7d056ca2a8b7dba905193f19ea3 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 19 Oct 2020 11:58:12 +0200 Subject: [PATCH 4/5] split helper --- .../drilldowns/url_drilldown/url_template.test.ts | 15 +++++++++++++++ .../drilldowns/url_drilldown/url_template.ts | 14 +++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts index 05a1a6de4ee8d..68a9654316d43 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -281,4 +281,19 @@ describe('basic string formatting helpers', () => { compile(`https://elastic.co/{{concat valueArray}}`, { valueArray: ['1', '2', '3'] }) ).toMatchInlineSnapshot(`"https://elastic.co/1,2,3"`); }); + + test('split', () => { + expect( + compile( + `https://elastic.co/{{lookup (split value ",") 0 }}&{{lookup (split value ",") 1 }}`, + { + value: '47.766201,-122.257057', + } + ) + ).toMatchInlineSnapshot(`"https://elastic.co/47.766201&-122.257057"`); + + expect(() => + compile(`https://elastic.co/{{split value}}`, { value: '47.766201,-122.257057' }) + ).toThrowErrorMatchingInlineSnapshot(`"[split] \\"splitter\\" expected to be a string"`); + }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts index 9b1ff645b7a64..f4a1acff8762b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -83,11 +83,6 @@ handlebars.registerHelper('uppercase', (rawValue: unknown) => String(rawValue).t handlebars.registerHelper('trim', (rawValue: unknown) => String(rawValue).trim()); handlebars.registerHelper('trimLeft', (rawValue: unknown) => String(rawValue).trimLeft()); handlebars.registerHelper('trimRight', (rawValue: unknown) => String(rawValue).trimRight()); -handlebars.registerHelper('concat', (...args) => { - const values = args.slice(0, -1) as unknown[]; - return values.join(''); -}); - handlebars.registerHelper('left', (rawValue: unknown, numberOfChars: number) => { if (typeof numberOfChars !== 'number') throw new Error('[left]: expected "number of characters to extract" to be a number'); @@ -103,6 +98,15 @@ handlebars.registerHelper('mid', (rawValue: unknown, start: number, length: numb if (typeof length !== 'number') throw new Error('[left]: expected "length" to be a number'); return String(rawValue).substr(start, length); }); +handlebars.registerHelper('concat', (...args) => { + const values = args.slice(0, -1) as unknown[]; + return values.join(''); +}); +handlebars.registerHelper('split', (...args) => { + const [str, splitter] = args.slice(0, -1) as [string, string]; + if (typeof splitter !== 'string') throw new Error('[split] "splitter" expected to be a string'); + return String(str).split(splitter); +}); handlebars.registerHelper('replace', (...args) => { const [str, searchString, valueString] = args.slice(0, -1) as [string, string, string]; if (typeof searchString !== 'string' || typeof valueString !== 'string') From 2b14a9ff2eaaab041e964e4efe329e98dc89f6e8 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 19 Oct 2020 12:20:41 +0200 Subject: [PATCH 5/5] docs --- docs/user/dashboard/url-drilldown.asciidoc | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index b71dfb016c765..cdb17e9daa5e3 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -135,6 +135,92 @@ Example: `{{ date event.from “YYYY MM DD”}}` + `{{date “now-15”}}` + +|formatNumber +a|Format numbers. Numbers can be formatted to look like currency, percentages, times or numbers with decimal places, thousands, and abbreviations. +Refer to the http://numeraljs.com/#format[numeral.js] for different formatting options. + +Example: + +`{{formatNumber event.value "0.0"}}` + +|lowercase +a|Converts a string to lower case. + +Example: + +`{{lowercase event.value}}` + +|uppercase +a|Converts a string to upper case. + +Example: + +`{{uppercase event.value}}` + +|trim +a|Removes leading and trailing spaces from a string. + +Example: + +`{{trim event.value}}` + +|trimLeft +a|Removes leading spaces from a string. + +Example: + +`{{trimLeft event.value}}` + +|trimRight +a|Removes trailing spaces from a string. + +Example: + +`{{trimRight event.value}}` + +|mid +a|Extracts a substring from a string by start position and number of characters to extract. + +Example: + +`{{mid event.value 3 5}}` - extracts five characters starting from a third character. + +|left +a|Extracts a number of characters from a string (starting from left). + +Example: + +`{{left event.value 3}}` + +|right +a|Extracts a number of characters from a string (starting from right). + +Example: + +`{{right event.value 3}}` + +|concat +a|Concatenates two or more strings. + +Example: + +`{{concat event.value "," event.key}}` + +|replace +a|Replaces all substrings within a string. + +Example: + +`{{replace event.value "stringToReplace" "stringToReplaceWith"}}` + +|split +a|Splits a string using a provided splitter. + +Example: + +`{{split event.value ","}}` + |===