Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Drilldows] Url Drilldown basic template helpers #80500

Merged
merged 9 commits into from
Oct 19, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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"`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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());
Dosant marked this conversation as resolved.
Show resolved Hide resolved
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);
Dosant marked this conversation as resolved.
Show resolved Hide resolved
});
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));
Expand Down