Fix fieldFormat plugins (#14984) (#15097)

* [ui/registry] allow custom provider invokation

* [fieldFormats] define custom formats as functions of FieldFormat class

* [fieldFormats/registerFieldFormat] call format creator to get class

* [fieldFormats/fieldFormatsService] fix tests

* [fieldFormats/registerFieldFormat] add tests

* [metrics/tickFormatter] fix import from test
This commit is contained in:
Spencer 2017-11-21 17:04:56 -07:00 committed by GitHub
parent 92bad847b5
commit 65c7e65053
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 629 additions and 474 deletions

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { BoolFormat } from '../boolean';
import { createBoolFormat } from '../boolean';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const BoolFormat = createBoolFormat(FieldFormat);
describe('Boolean Format', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { BytesFormat } from '../bytes';
import { createBytesFormat } from '../bytes';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const BytesFormat = createBytesFormat(FieldFormat);
describe('BytesFormat', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { ColorFormat } from '../color';
import { createColorFormat } from '../color';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const ColorFormat = createColorFormat(FieldFormat);
describe('Color Format', function () {

View file

@ -1,6 +1,9 @@
import expect from 'expect.js';
import moment from 'moment-timezone';
import { DateFormat } from '../date';
import { createDateFormat } from '../date';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const DateFormat = createDateFormat(FieldFormat);
describe('Date Format', function () {
let convert;

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { DurationFormat } from '../duration';
import { createDurationFormat } from '../duration';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const DurationFormat = createDurationFormat(FieldFormat);
describe('Duration Format', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { IpFormat } from '../ip';
import { createIpFormat } from '../ip';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const IpFormat = createIpFormat(FieldFormat);
describe('IP Address Format', function () {
let ip;

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { NumberFormat } from '../number';
import { createNumberFormat } from '../number';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const NumberFormat = createNumberFormat(FieldFormat);
describe('NumberFormat', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { PercentFormat } from '../percent';
import { createPercentFormat } from '../percent';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const PercentFormat = createPercentFormat(FieldFormat);
describe('PercentFormat', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { StringFormat } from '../string';
import { createStringFormat } from '../string';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const StringFormat = createStringFormat(FieldFormat);
describe('String Format', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { TruncateFormat } from '../truncate';
import { createTruncateFormat } from '../truncate';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const TruncateFormat = createTruncateFormat(FieldFormat);
describe('String TruncateFormat', function () {

View file

@ -1,5 +1,8 @@
import expect from 'expect.js';
import { UrlFormat } from '../url';
import { createUrlFormat } from '../url';
import { FieldFormat } from '../../../../../../ui/field_formats/field_format';
const UrlFormat = createUrlFormat(FieldFormat);
describe('UrlFormat', function () {

View file

@ -1,49 +1,50 @@
import _ from 'lodash';
import numeral from '@elastic/numeral';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const numeralInst = numeral();
export class Numeral extends FieldFormat {
_convert(val) {
if (val === -Infinity) return '-∞';
if (val === +Infinity) return '+∞';
if (typeof val !== 'number') {
val = parseFloat(val);
export function createNumeralFormat(FieldFormat, opts) {
class NumeralFormat extends FieldFormat {
static id = opts.id;
static title = opts.title;
static fieldType = 'number';
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
}
if (isNaN(val)) return '';
return numeralInst.set(val).format(this.param('pattern'));
}
static factory(opts) {
class Class extends Numeral {
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
getParamDefaults() {
if (_.has(opts, 'getParamDefaults')) {
return opts.getParamDefaults(this.getConfig);
}
getParamDefaults() {
if (_.has(opts, 'getParamDefaults')) {
return opts.getParamDefaults(this.getConfig);
}
return {
pattern: this.getConfig(`format:${opts.id}:defaultPattern`)
};
}
return {
pattern: this.getConfig(`format:${opts.id}:defaultPattern`)
};
_convert(val) {
if (val === -Infinity) return '-∞';
if (val === +Infinity) return '+∞';
if (typeof val !== 'number') {
val = parseFloat(val);
}
static id = opts.id;
static title = opts.title;
static fieldType = 'number';
}
if (isNaN(val)) return '';
if (opts.prototype) {
_.assign(Class.prototype, opts.prototype);
}
const formatted = numeralInst.set(val).format(this.param('pattern'));
return Class;
return opts.afterConvert
? opts.afterConvert.call(this, formatted)
: formatted;
}
}
if (opts.prototype) {
_.assign(NumeralFormat.prototype, opts.prototype);
}
return NumeralFormat;
}

View file

@ -1,29 +1,30 @@
import { asPrettyString } from '../../utils/as_pretty_string';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
export class BoolFormat extends FieldFormat {
_convert(value) {
if (typeof value === 'string') {
value = value.trim().toLowerCase();
export function createBoolFormat(FieldFormat) {
return class BoolFormat extends FieldFormat {
_convert(value) {
if (typeof value === 'string') {
value = value.trim().toLowerCase();
}
switch (value) {
case false:
case 0:
case 'false':
case 'no':
return 'false';
case true:
case 1:
case 'true':
case 'yes':
return 'true';
default:
return asPrettyString(value);
}
}
switch (value) {
case false:
case 0:
case 'false':
case 'no':
return 'false';
case true:
case 1:
case 'true':
case 'yes':
return 'true';
default:
return asPrettyString(value);
}
}
static id = 'boolean';
static title = 'Boolean';
static fieldType = ['boolean', 'number', 'string'];
static id = 'boolean';
static title = 'Boolean';
static fieldType = ['boolean', 'number', 'string'];
};
}

View file

@ -1,6 +1,8 @@
import { Numeral } from './_numeral';
import { createNumeralFormat } from './_numeral';
export const BytesFormat = Numeral.factory({
id: 'bytes',
title: 'Bytes'
});
export function createBytesFormat(FieldFormat) {
return createNumeralFormat(FieldFormat, {
id: 'bytes',
title: 'Bytes'
});
}

View file

@ -1,53 +1,56 @@
import _ from 'lodash';
import { asPrettyString } from '../../utils/as_pretty_string';
import { DEFAULT_COLOR } from './color_default';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const convertTemplate = _.template('<span style="<%- style %>"><%- val %></span>');
export class ColorFormat extends FieldFormat {
getParamDefaults() {
return {
fieldType: null, // populated by editor, see controller below
colors: [_.cloneDeep(DEFAULT_COLOR)]
};
}
findColorRuleForVal(val) {
switch (this.param('fieldType')) {
case 'string':
return _.findLast(this.param('colors'), (colorParam) => {
return new RegExp(colorParam.regex).test(val);
});
case 'number':
return _.findLast(this.param('colors'), ({ range }) => {
if (!range) return;
const [start, end] = range.split(':');
return val >= Number(start) && val <= Number(end);
});
default:
return null;
export function createColorFormat(FieldFormat) {
class ColorFormat extends FieldFormat {
getParamDefaults() {
return {
fieldType: null, // populated by editor, see controller below
colors: [_.cloneDeep(DEFAULT_COLOR)]
};
}
findColorRuleForVal(val) {
switch (this.param('fieldType')) {
case 'string':
return _.findLast(this.param('colors'), (colorParam) => {
return new RegExp(colorParam.regex).test(val);
});
case 'number':
return _.findLast(this.param('colors'), ({ range }) => {
if (!range) return;
const [start, end] = range.split(':');
return val >= Number(start) && val <= Number(end);
});
default:
return null;
}
}
static id = 'color';
static title = 'Color';
static fieldType = [
'number',
'string'
];
}
static id = 'color';
static title = 'Color';
static fieldType = [
'number',
'string'
];
ColorFormat.prototype._convert = {
html(val) {
const color = this.findColorRuleForVal(val);
if (!color) return asPrettyString(val);
let style = '';
if (color.text) style += `color: ${color.text};`;
if (color.background) style += `background-color: ${color.background};`;
return convertTemplate({ val, style });
}
};
return ColorFormat;
}
ColorFormat.prototype._convert = {
html(val) {
const color = this.findColorRuleForVal(val);
if (!color) return asPrettyString(val);
let style = '';
if (color.text) style += `color: ${color.text};`;
if (color.background) style += `background-color: ${color.background};`;
return convertTemplate({ val, style });
}
};

View file

@ -1,51 +1,52 @@
import _ from 'lodash';
import moment from 'moment';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
export class DateFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
export function createDateFormat(FieldFormat) {
return class DateFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
}
getParamDefaults() {
return {
pattern: this.getConfig('dateFormat'),
timezone: this.getConfig('dateFormat:tz')
};
}
_convert(val) {
// don't give away our ref to converter so
// we can hot-swap when config changes
const pattern = this.param('pattern');
const timezone = this.param('timezone');
const timezoneChanged = this._timeZone !== timezone;
const datePatternChanged = this._memoizedPattern !== pattern;
if (timezoneChanged || datePatternChanged) {
this._timeZone = timezone;
this._memoizedPattern = pattern;
this._memoizedConverter = _.memoize(function converter(val) {
if (val === null || val === undefined) {
return '-';
}
const date = moment(val);
if (date.isValid()) {
return date.format(pattern);
} else {
return val;
}
});
this.getConfig = getConfig;
}
return this._memoizedConverter(val);
}
getParamDefaults() {
return {
pattern: this.getConfig('dateFormat'),
timezone: this.getConfig('dateFormat:tz')
};
}
static id = 'date';
static title = 'Date';
static fieldType = 'date';
_convert(val) {
// don't give away our ref to converter so
// we can hot-swap when config changes
const pattern = this.param('pattern');
const timezone = this.param('timezone');
const timezoneChanged = this._timeZone !== timezone;
const datePatternChanged = this._memoizedPattern !== pattern;
if (timezoneChanged || datePatternChanged) {
this._timeZone = timezone;
this._memoizedPattern = pattern;
this._memoizedConverter = _.memoize(function converter(val) {
if (val === null || val === undefined) {
return '-';
}
const date = moment(val);
if (date.isValid()) {
return date.format(pattern);
} else {
return val;
}
});
}
return this._memoizedConverter(val);
}
static id = 'date';
static title = 'Date';
static fieldType = 'date';
};
}

View file

@ -1,5 +1,4 @@
import moment from 'moment';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const ratioToSeconds = {
picoseconds: 0.000000000001,
@ -35,39 +34,41 @@ const outputFormats = [
{ text: 'Years', method: 'asYears' }
];
export class DurationFormat extends FieldFormat {
isHuman() {
return this.param('outputFormat') === HUMAN_FRIENDLY;
}
getParamDefaults() {
return {
inputFormat: DEFAULT_INPUT_FORMAT.kind,
outputFormat: DEFAULT_OUTPUT_FORMAT.method,
outputPrecision: DEFAULT_OUTPUT_PRECISION
};
}
_convert(val) {
const inputFormat = this.param('inputFormat');
const outputFormat = this.param('outputFormat');
const outputPrecision = this.param('outputPrecision');
const human = this.isHuman();
const prefix = val < 0 && human ? 'minus ' : '';
const duration = parseInputAsDuration(val, inputFormat);
const formatted = duration[outputFormat]();
const precise = human ? formatted : formatted.toFixed(outputPrecision);
return prefix + precise;
}
static id = 'duration';
static title = 'Duration';
static fieldType = 'number';
static inputFormats = inputFormats;
static outputFormats = outputFormats;
}
function parseInputAsDuration(val, inputFormat) {
const ratio = ratioToSeconds[inputFormat] || 1;
const kind = inputFormat in ratioToSeconds ? 'seconds' : inputFormat;
return moment.duration(val * ratio, kind);
}
export function createDurationFormat(FieldFormat) {
return class DurationFormat extends FieldFormat {
isHuman() {
return this.param('outputFormat') === HUMAN_FRIENDLY;
}
getParamDefaults() {
return {
inputFormat: DEFAULT_INPUT_FORMAT.kind,
outputFormat: DEFAULT_OUTPUT_FORMAT.method,
outputPrecision: DEFAULT_OUTPUT_PRECISION
};
}
_convert(val) {
const inputFormat = this.param('inputFormat');
const outputFormat = this.param('outputFormat');
const outputPrecision = this.param('outputPrecision');
const human = this.isHuman();
const prefix = val < 0 && human ? 'minus ' : '';
const duration = parseInputAsDuration(val, inputFormat);
const formatted = duration[outputFormat]();
const precise = human ? formatted : formatted.toFixed(outputPrecision);
return prefix + precise;
}
static id = 'duration';
static title = 'Duration';
static fieldType = 'number';
static inputFormats = inputFormats;
static outputFormats = outputFormats;
};
}

View file

@ -1,15 +1,15 @@
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
export function createIpFormat(FieldFormat) {
return class IpFormat extends FieldFormat {
_convert(val) {
if (val === undefined || val === null) return '-';
if (!isFinite(val)) return val;
export class IpFormat extends FieldFormat {
_convert(val) {
if (val === undefined || val === null) return '-';
if (!isFinite(val)) return val;
// shazzam!
return [val >>> 24, val >>> 16 & 0xFF, val >>> 8 & 0xFF, val & 0xFF].join('.');
}
// shazzam!
return [val >>> 24, val >>> 16 & 0xFF, val >>> 8 & 0xFF, val & 0xFF].join('.');
}
static id = 'ip';
static title = 'IP Address';
static fieldType = 'ip';
static id = 'ip';
static title = 'IP Address';
static fieldType = 'ip';
};
}

View file

@ -1,6 +1,8 @@
import { Numeral } from './_numeral';
import { createNumeralFormat } from './_numeral';
export const NumberFormat = Numeral.factory({
id: 'number',
title: 'Number'
});
export function createNumberFormat(FieldFormat) {
return createNumeralFormat(FieldFormat, {
id: 'number',
title: 'Number'
});
}

View file

@ -1,18 +1,17 @@
import _ from 'lodash';
import { Numeral } from './_numeral';
import { createNumeralFormat } from './_numeral';
export const PercentFormat = Numeral.factory({
id: 'percent',
title: 'Percentage',
getParamDefaults: (getConfig) => {
return {
pattern: getConfig('format:percent:defaultPattern'),
fractional: true
};
},
prototype: {
_convert: _.compose(Numeral.prototype._convert, function (val) {
export function createPercentFormat(FieldFormat) {
return createNumeralFormat(FieldFormat, {
id: 'percent',
title: 'Percentage',
getParamDefaults: (getConfig) => {
return {
pattern: getConfig('format:percent:defaultPattern'),
fractional: true
};
},
afterConvert(val) {
return this.param('fractional') ? val : val / 100;
})
}
});
}
});
}

View file

@ -1,7 +1,6 @@
import _ from 'lodash';
import { noWhiteSpace } from '../../utils/no_white_space';
import { toJson } from '../../utils/aggressive_parse';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
import { shortenDottedString } from '../../utils/shorten_dotted_string';
const templateHtml = `
@ -14,36 +13,40 @@ const templateHtml = `
</dl>`;
const template = _.template(noWhiteSpace(templateHtml));
export class SourceFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
export function createSourceFormat(FieldFormat) {
class SourceFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
this.getConfig = getConfig;
}
static id = '_source';
static title = '_source';
static fieldType = '_source';
}
static id = '_source';
static title = '_source';
static fieldType = '_source';
SourceFormat.prototype._convert = {
text: (value) => toJson(value),
html: function sourceToHtml(source, field, hit) {
if (!field) return this.getConverterFor('text')(source, field, hit);
const highlights = (hit && hit.highlight) || {};
const formatted = field.indexPattern.formatHit(hit);
const highlightPairs = [];
const sourcePairs = [];
const isShortDots = this.getConfig('shortDots:enable');
_.keys(formatted).forEach((key) => {
const pairs = highlights[key] ? highlightPairs : sourcePairs;
const field = isShortDots ? shortenDottedString(key) : key;
const val = formatted[key];
pairs.push([field, val]);
}, []);
return template({ defPairs: highlightPairs.concat(sourcePairs) });
}
};
return SourceFormat;
}
SourceFormat.prototype._convert = {
text: (value) => toJson(value),
html: function sourceToHtml(source, field, hit) {
if (!field) return this.getConverterFor('text')(source, field, hit);
const highlights = (hit && hit.highlight) || {};
const formatted = field.indexPattern.formatHit(hit);
const highlightPairs = [];
const sourcePairs = [];
const isShortDots = this.getConfig('shortDots:enable');
_.keys(formatted).forEach((key) => {
const pairs = highlights[key] ? highlightPairs : sourcePairs;
const field = isShortDots ? shortenDottedString(key) : key;
const val = formatted[key];
pairs.push([field, val]);
}, []);
return template({ defPairs: highlightPairs.concat(sourcePairs) });
}
};

View file

@ -1,50 +1,51 @@
import { asPrettyString } from '../../utils/as_pretty_string';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
import { shortenDottedString } from '../../utils/shorten_dotted_string';
export class StringFormat extends FieldFormat {
getParamDefaults() {
return {
transform: false
};
}
_base64Decode(val) {
try {
return Buffer.from(val, 'base64').toString('utf8');
} catch (e) {
return asPrettyString(val);
export function createStringFormat(FieldFormat) {
return class StringFormat extends FieldFormat {
getParamDefaults() {
return {
transform: false
};
}
}
_toTitleCase(val) {
return val.replace(/\w\S*/g, txt => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
}
_convert(val) {
switch (this.param('transform')) {
case 'lower': return String(val).toLowerCase();
case 'upper': return String(val).toUpperCase();
case 'title': return this._toTitleCase(val);
case 'short': return shortenDottedString(val);
case 'base64': return this._base64Decode(val);
default: return asPrettyString(val);
_base64Decode(val) {
try {
return Buffer.from(val, 'base64').toString('utf8');
} catch (e) {
return asPrettyString(val);
}
}
}
static id = 'string';
static title = 'String';
static fieldType = [
'number',
'boolean',
'date',
'ip',
'attachment',
'geo_point',
'geo_shape',
'string',
'murmur3',
'unknown',
'conflict'
];
_toTitleCase(val) {
return val.replace(/\w\S*/g, txt => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
}
_convert(val) {
switch (this.param('transform')) {
case 'lower': return String(val).toLowerCase();
case 'upper': return String(val).toUpperCase();
case 'title': return this._toTitleCase(val);
case 'short': return shortenDottedString(val);
case 'base64': return this._base64Decode(val);
default: return asPrettyString(val);
}
}
static id = 'string';
static title = 'String';
static fieldType = [
'number',
'boolean',
'date',
'ip',
'attachment',
'geo_point',
'geo_shape',
'string',
'murmur3',
'unknown',
'conflict'
];
};
}

View file

@ -1,22 +1,23 @@
import _ from 'lodash';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const omission = '...';
export class TruncateFormat extends FieldFormat {
_convert(val) {
const length = this.param('fieldLength');
if (length > 0) {
return _.trunc(val, {
'length': length + omission.length,
'omission': omission
});
export function createTruncateFormat(FieldFormat) {
return class TruncateFormat extends FieldFormat {
_convert(val) {
const length = this.param('fieldLength');
if (length > 0) {
return _.trunc(val, {
'length': length + omission.length,
'omission': omission
});
}
return val;
}
return val;
}
static id = 'truncate';
static title = 'Truncated String';
static fieldType = ['string'];
static id = 'truncate';
static title = 'Truncated String';
static fieldType = ['string'];
};
}

View file

@ -1,112 +1,115 @@
import _ from 'lodash';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
import { getHighlightHtml } from '../../highlight/highlight_html';
const templateMatchRE = /{{([\s\S]+?)}}/g;
export class UrlFormat extends FieldFormat {
constructor(params) {
super(params);
this._compileTemplate = _.memoize(this._compileTemplate);
}
getParamDefaults() {
return {
type: 'a',
urlTemplate: null,
labelTemplate: null
};
}
_formatLabel(value, url) {
const template = this.param('labelTemplate');
if (url == null) url = this._formatUrl(value);
if (!template) return url;
return this._compileTemplate(template)({
value: value,
url: url
});
}
_formatUrl(value) {
const template = this.param('urlTemplate');
if (!template) return value;
return this._compileTemplate(template)({
value: encodeURIComponent(value),
rawValue: value
});
}
_compileTemplate(template) {
const parts = template.split(templateMatchRE).map(function (part, i) {
// trim all the odd bits, the variable names
return (i % 2) ? part.trim() : part;
});
return function (locals) {
// replace all the odd bits with their local var
let output = '';
let i = -1;
while (++i < parts.length) {
if (i % 2) {
if (locals.hasOwnProperty(parts[i])) {
const local = locals[parts[i]];
output += local == null ? '' : local;
}
} else {
output += parts[i];
}
}
return output;
};
}
static id = 'url';
static title = 'Url';
static fieldType = [
'number',
'boolean',
'date',
'ip',
'string',
'murmur3',
'unknown',
'conflict'
];
}
UrlFormat.prototype._convert = {
text: function (value) {
return this._formatLabel(value);
},
html: function (rawValue, field, hit) {
const url = _.escape(this._formatUrl(rawValue));
const label = _.escape(this._formatLabel(rawValue, url));
switch (this.param('type')) {
case 'img':
// If the URL hasn't been formatted to become a meaningful label then the best we can do
// is tell screen readers where the image comes from.
const imageLabel =
label === url
? `A dynamically-specified image located at ${url}`
: label;
return `<img src="${url}" alt="${imageLabel}">`;
default:
let linkLabel;
if (hit && hit.highlight && hit.highlight[field.name]) {
linkLabel = getHighlightHtml(label, hit.highlight[field.name]);
} else {
linkLabel = label;
}
return `<a href="${url}" target="_blank">${linkLabel}</a>`;
export function createUrlFormat(FieldFormat) {
class UrlFormat extends FieldFormat {
constructor(params) {
super(params);
this._compileTemplate = _.memoize(this._compileTemplate);
}
getParamDefaults() {
return {
type: 'a',
urlTemplate: null,
labelTemplate: null
};
}
_formatLabel(value, url) {
const template = this.param('labelTemplate');
if (url == null) url = this._formatUrl(value);
if (!template) return url;
return this._compileTemplate(template)({
value: value,
url: url
});
}
_formatUrl(value) {
const template = this.param('urlTemplate');
if (!template) return value;
return this._compileTemplate(template)({
value: encodeURIComponent(value),
rawValue: value
});
}
_compileTemplate(template) {
const parts = template.split(templateMatchRE).map(function (part, i) {
// trim all the odd bits, the variable names
return (i % 2) ? part.trim() : part;
});
return function (locals) {
// replace all the odd bits with their local var
let output = '';
let i = -1;
while (++i < parts.length) {
if (i % 2) {
if (locals.hasOwnProperty(parts[i])) {
const local = locals[parts[i]];
output += local == null ? '' : local;
}
} else {
output += parts[i];
}
}
return output;
};
}
static id = 'url';
static title = 'Url';
static fieldType = [
'number',
'boolean',
'date',
'ip',
'string',
'murmur3',
'unknown',
'conflict'
];
}
};
UrlFormat.prototype._convert = {
text: function (value) {
return this._formatLabel(value);
},
html: function (rawValue, field, hit) {
const url = _.escape(this._formatUrl(rawValue));
const label = _.escape(this._formatLabel(rawValue, url));
switch (this.param('type')) {
case 'img':
// If the URL hasn't been formatted to become a meaningful label then the best we can do
// is tell screen readers where the image comes from.
const imageLabel =
label === url
? `A dynamically-specified image located at ${url}`
: label;
return `<img src="${url}" alt="${imageLabel}">`;
default:
let linkLabel;
if (hit && hit.highlight && hit.highlight[field.name]) {
linkLabel = getHighlightHtml(label, hit.highlight[field.name]);
} else {
linkLabel = label;
}
return `<a href="${url}" target="_blank">${linkLabel}</a>`;
}
}
};
return UrlFormat;
}

View file

@ -1,26 +1,26 @@
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
import { UrlFormat } from '../../common/field_formats/types/url';
import { BytesFormat } from '../../common/field_formats/types/bytes';
import { DateFormat } from '../../common/field_formats/types/date';
import { DurationFormat } from '../../common/field_formats/types/duration';
import { IpFormat } from '../../common/field_formats/types/ip';
import { NumberFormat } from '../../common/field_formats/types/number';
import { PercentFormat } from '../../common/field_formats/types/percent';
import { StringFormat } from '../../common/field_formats/types/string';
import { SourceFormat } from '../../common/field_formats/types/source';
import { ColorFormat } from '../../common/field_formats/types/color';
import { TruncateFormat } from '../../common/field_formats/types/truncate';
import { BoolFormat } from '../../common/field_formats/types/boolean';
import { createUrlFormat } from '../../common/field_formats/types/url';
import { createBytesFormat } from '../../common/field_formats/types/bytes';
import { createDateFormat } from '../../common/field_formats/types/date';
import { createDurationFormat } from '../../common/field_formats/types/duration';
import { createIpFormat } from '../../common/field_formats/types/ip';
import { createNumberFormat } from '../../common/field_formats/types/number';
import { createPercentFormat } from '../../common/field_formats/types/percent';
import { createStringFormat } from '../../common/field_formats/types/string';
import { createSourceFormat } from '../../common/field_formats/types/source';
import { createColorFormat } from '../../common/field_formats/types/color';
import { createTruncateFormat } from '../../common/field_formats/types/truncate';
import { createBoolFormat } from '../../common/field_formats/types/boolean';
RegistryFieldFormatsProvider.register(() => UrlFormat);
RegistryFieldFormatsProvider.register(() => BytesFormat);
RegistryFieldFormatsProvider.register(() => DateFormat);
RegistryFieldFormatsProvider.register(() => DurationFormat);
RegistryFieldFormatsProvider.register(() => IpFormat);
RegistryFieldFormatsProvider.register(() => NumberFormat);
RegistryFieldFormatsProvider.register(() => PercentFormat);
RegistryFieldFormatsProvider.register(() => StringFormat);
RegistryFieldFormatsProvider.register(() => SourceFormat);
RegistryFieldFormatsProvider.register(() => ColorFormat);
RegistryFieldFormatsProvider.register(() => TruncateFormat);
RegistryFieldFormatsProvider.register(() => BoolFormat);
RegistryFieldFormatsProvider.register(createUrlFormat);
RegistryFieldFormatsProvider.register(createBytesFormat);
RegistryFieldFormatsProvider.register(createDateFormat);
RegistryFieldFormatsProvider.register(createDurationFormat);
RegistryFieldFormatsProvider.register(createIpFormat);
RegistryFieldFormatsProvider.register(createNumberFormat);
RegistryFieldFormatsProvider.register(createPercentFormat);
RegistryFieldFormatsProvider.register(createStringFormat);
RegistryFieldFormatsProvider.register(createSourceFormat);
RegistryFieldFormatsProvider.register(createColorFormat);
RegistryFieldFormatsProvider.register(createTruncateFormat);
RegistryFieldFormatsProvider.register(createBoolFormat);

View file

@ -1,27 +1,27 @@
import { UrlFormat } from '../../common/field_formats/types/url';
import { BytesFormat } from '../../common/field_formats/types/bytes';
import { DateFormat } from '../../common/field_formats/types/date';
import { DurationFormat } from '../../common/field_formats/types/duration';
import { IpFormat } from '../../common/field_formats/types/ip';
import { NumberFormat } from '../../common/field_formats/types/number';
import { PercentFormat } from '../../common/field_formats/types/percent';
import { StringFormat } from '../../common/field_formats/types/string';
import { SourceFormat } from '../../common/field_formats/types/source';
import { ColorFormat } from '../../common/field_formats/types/color';
import { TruncateFormat } from '../../common/field_formats/types/truncate';
import { BoolFormat } from '../../common/field_formats/types/boolean';
import { createUrlFormat } from '../../common/field_formats/types/url';
import { createBytesFormat } from '../../common/field_formats/types/bytes';
import { createDateFormat } from '../../common/field_formats/types/date';
import { createDurationFormat } from '../../common/field_formats/types/duration';
import { createIpFormat } from '../../common/field_formats/types/ip';
import { createNumberFormat } from '../../common/field_formats/types/number';
import { createPercentFormat } from '../../common/field_formats/types/percent';
import { createStringFormat } from '../../common/field_formats/types/string';
import { createSourceFormat } from '../../common/field_formats/types/source';
import { createColorFormat } from '../../common/field_formats/types/color';
import { createTruncateFormat } from '../../common/field_formats/types/truncate';
import { createBoolFormat } from '../../common/field_formats/types/boolean';
export function registerFieldFormats(server) {
server.registerFieldFormatClass(UrlFormat);
server.registerFieldFormatClass(BytesFormat);
server.registerFieldFormatClass(DateFormat);
server.registerFieldFormatClass(DurationFormat);
server.registerFieldFormatClass(IpFormat);
server.registerFieldFormatClass(NumberFormat);
server.registerFieldFormatClass(PercentFormat);
server.registerFieldFormatClass(StringFormat);
server.registerFieldFormatClass(SourceFormat);
server.registerFieldFormatClass(ColorFormat);
server.registerFieldFormatClass(TruncateFormat);
server.registerFieldFormatClass(BoolFormat);
server.registerFieldFormat(createUrlFormat);
server.registerFieldFormat(createBytesFormat);
server.registerFieldFormat(createDateFormat);
server.registerFieldFormat(createDurationFormat);
server.registerFieldFormat(createIpFormat);
server.registerFieldFormat(createNumberFormat);
server.registerFieldFormat(createPercentFormat);
server.registerFieldFormat(createStringFormat);
server.registerFieldFormat(createSourceFormat);
server.registerFieldFormat(createColorFormat);
server.registerFieldFormat(createTruncateFormat);
server.registerFieldFormat(createBoolFormat);
}

View file

@ -1,9 +1,13 @@
import numeral from '@elastic/numeral';
import handlebars from 'handlebars/dist/handlebars';
import { durationInputOptions } from './durations';
import { DurationFormat } from '../../../../kibana/common/field_formats/types/duration';
import { capitalize, isNumber } from 'lodash';
import { createDurationFormat } from '../../../../kibana/common/field_formats/types/duration';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const DurationFormat = createDurationFormat(FieldFormat);
const formatLookup = {
'bytes': '0.0b',
'number': '0,0.[00]',

View file

@ -0,0 +1,64 @@
import expect from 'expect.js';
import sinon from 'sinon';
import { FieldFormat } from '../field_format';
import * as FieldFormatsServiceNS from '../field_formats_service';
import { createServer } from '../../../test_utils/kbn_server';
describe('server.registerFieldFormat(createFormat)', () => {
const sandbox = sinon.sandbox.create();
let server;
beforeEach(async () => {
const kbnServer = createServer();
await kbnServer.ready();
server = kbnServer.server;
});
afterEach(() => {
sandbox.restore();
});
it('throws if createFormat is not a function', () => {
expect(() => server.registerFieldFormat()).to.throwError(error => {
expect(error.message).to.match(/createFormat is not a function/i);
});
});
it('calls the createFormat() function with the FieldFormat class', () => {
const createFormat = sinon.stub();
server.registerFieldFormat(createFormat);
sinon.assert.calledOnce(createFormat);
sinon.assert.calledWithExactly(createFormat, sinon.match.same(FieldFormat));
});
it('passes the returned class to the FieldFormatsService', async () => {
const { FieldFormatsService: ActualFFS } = FieldFormatsServiceNS;
sinon.stub(FieldFormatsServiceNS, 'FieldFormatsService', function (...args) {
return new ActualFFS(...args);
});
const { FieldFormatsService } = FieldFormatsServiceNS;
class FooFormat {
static id = 'foo'
}
server.registerFieldFormat(() => FooFormat);
const fieldFormats = await server.fieldFormatServiceFactory({
getAll: () => ({}),
getDefaults: () => ({})
});
sinon.assert.calledOnce(FieldFormatsService);
sinon.assert.calledWithExactly(
FieldFormatsService,
// array of fieldFormat classes
[sinon.match.same(FooFormat)],
// getConfig() function
sinon.match.func
);
const format = fieldFormats.getInstance({ id: 'foo' });
expect(format).to.be.a(FooFormat);
});
});

View file

@ -1,6 +1,7 @@
import expect from 'expect.js';
import { FieldFormat } from '../field_format';
import { FieldFormatsService } from '../field_formats_service';
import { NumberFormat } from '../../../core_plugins/kibana/common/field_formats/types/number';
import { createNumberFormat } from '../../../core_plugins/kibana/common/field_formats/types/number';
describe('FieldFormatsService', function () {
@ -11,7 +12,7 @@ describe('FieldFormatsService', function () {
};
config['format:number:defaultPattern'] = '0,0.[000]';
const getConfig = (key) => config[key];
const fieldFormatClasses = [NumberFormat];
const fieldFormatClasses = [createNumberFormat(FieldFormat)];
let fieldFormats;
beforeEach(function () {

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import { FieldFormatsService } from './field_formats/field_formats_service';
import { FieldFormatsService } from './field_formats_service';
import { FieldFormat } from './field_format';
export function fieldFormatsMixin(kbnServer, server) {
const fieldFormatClasses = [];
@ -22,7 +23,7 @@ export function fieldFormatsMixin(kbnServer, server) {
return new FieldFormatsService(fieldFormatClasses, getConfig);
});
server.decorate('server', 'registerFieldFormatClass', (FieldFormat) => {
fieldFormatClasses.push(FieldFormat);
server.decorate('server', 'registerFieldFormat', (createFormat) => {
fieldFormatClasses.push(createFormat(FieldFormat));
});
}

View file

@ -10,7 +10,7 @@ import UiBundlerEnv from './ui_bundler_env';
import { UiI18n } from './ui_i18n';
import { uiSettingsMixin } from './ui_settings';
import { fieldFormatsMixin } from './field_formats_mixin';
import { fieldFormatsMixin } from './field_formats/field_formats_mixin';
export default async (kbnServer, server, config) => {
const uiExports = kbnServer.uiExports = new UiExports({

View file

@ -95,6 +95,26 @@ describe('Registry', function () {
});
});
describe('spec.invokeProviders', () => {
it('is called with the registered providers and defines the initial set of values in the registry', () => {
const reg = uiRegistry({
invokeProviders(providers) {
return providers.map(i => i * 1000);
}
});
reg.register(1);
reg.register(2);
reg.register(3);
expect(Private(reg).toJSON()).to.eql([1000, 2000, 3000]);
});
it('does not get assigned as a property of the registry', () => {
expect(uiRegistry({
invokeProviders() {}
})).to.not.have.property('invokeProviders');
});
});
describe('spec[any]', function () {
it('mixes the extra properties into the module list', function () {
const reg = uiRegistry({

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import { IndexedArray } from 'ui/indexed_array';
const notPropsOptNames = IndexedArray.OPT_NAMES.concat('constructor');
const notPropsOptNames = IndexedArray.OPT_NAMES.concat('constructor', 'invokeProviders');
/**
* Create a registry, which is just a Private module provider.
@ -49,6 +49,7 @@ export function uiRegistry(spec) {
spec = spec || {};
const constructor = _.has(spec, 'constructor') && spec.constructor;
const invokeProviders = _.has(spec, 'invokeProviders') && spec.invokeProviders;
const iaOpts = _.defaults(_.pick(spec, IndexedArray.OPT_NAMES), { index: ['name'] });
const props = _.omit(spec, notPropsOptNames);
const providers = [];
@ -62,8 +63,12 @@ export function uiRegistry(spec) {
* defines how things will be indexed.
*/
const registry = function (Private, $injector) {
// call the registered providers to get their values
iaOpts.initialSet = invokeProviders
? $injector.invoke(invokeProviders, undefined, { providers })
: providers.map(Private);
// index all of the modules
iaOpts.initialSet = providers.map(Private);
let modules = new IndexedArray(iaOpts);
// mixin other props
@ -86,4 +91,3 @@ export function uiRegistry(spec) {
return registry;
}

View file

@ -1,11 +1,21 @@
import _ from 'lodash';
import { uiRegistry } from 'ui/registry/_registry';
import { FieldFormat } from '../../field_formats/field_format';
export const RegistryFieldFormatsProvider = uiRegistry({
name: 'fieldFormats',
index: ['id'],
group: ['fieldType'],
invokeProviders(providers) {
// in order to ensure that FieldFormats can be instantiated on the
// server and the browser we don't provide them access to the Angular
// injector, just the FieldFormat class.
return providers.map(createSomeFormat => (
createSomeFormat(FieldFormat)
));
},
constructor: function (config) {
const getConfig = (...args) => config.get(...args);
const self = this;