mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Drilldows] Url Drilldown basic template helpers (#80500)
This commit is contained in:
parent
30fc4bed7d
commit
9aa4b4de1a
3 changed files with 291 additions and 0 deletions
|
@ -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 ","}}`
|
||||
|
||||
|===
|
||||
|
||||
|
||||
|
|
|
@ -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"`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue