[Drilldows] Url Drilldown basic template helpers (#80500)

This commit is contained in:
Anton Dosov 2020-10-19 17:41:35 +02:00 committed by GitHub
parent 30fc4bed7d
commit 9aa4b4de1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 291 additions and 0 deletions

View file

@ -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 ","}}`
|===

View file

@ -139,3 +139,161 @@ 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('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) =>
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"`);
});
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"`);
});
});

View file

@ -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,52 @@ 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);
});
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('left', (rawValue: unknown, numberOfChars: number) => {
if (typeof numberOfChars !== 'number')
throw new Error('[left]: expected "number of characters to extract" to be a number');
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 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 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')
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 });
return encodeURI(template(context));