Expose field formatters in kibana server (#12625)

* move field formatters to common

* expose fieldFormats on server

* export Format class from field_formats/types

* remove use of window.atob in StringFormat

* fix test and move server register under directory field_formats

* cleanup

* expose uiSettingDefaults on server so fieldFormats knows how to parse uiSettings

* remove uiSettingDefaults decorator and clean up tests

* move field_formats_service out of kibana plugin

* make duration test more unstandable

* prefix internal member with underscore

* pass getConfig in constructor instead of methods

* move getParamsDefaults for DurationFormat

* add getInstance method to field_formats_server

* move FieldFormat class outside of kibana plugin
This commit is contained in:
Nathan Reese 2017-07-07 11:47:25 -06:00 committed by GitHub
parent 7610d25aa0
commit 363a06555c
84 changed files with 1212 additions and 694 deletions

View file

@ -0,0 +1,59 @@
import expect from 'expect.js';
import { BoolFormat } from '../boolean';
describe('Boolean Format', function () {
let boolean;
beforeEach(() => {
boolean = new BoolFormat();
});
[
{
input: 0,
expected: 'false'
},
{
input: 'no',
expected: 'false'
},
{
input: false,
expected: 'false'
},
{
input: 'false',
expected: 'false'
},
{
input: 1,
expected: 'true'
},
{
input: 'yes',
expected: 'true'
},
{
input: true,
expected: 'true'
},
{
input: 'true',
expected: 'true'
},
{
input: ' True ',//should handle trailing and mixed case
expected: 'true'
}
].forEach((test)=> {
it(`convert ${test.input} to boolean`, ()=> {
expect(boolean.convert(test.input)).to.be(test.expected);
});
});
it('does not convert non-boolean values, instead returning original value', ()=> {
const s = 'non-boolean value!!';
expect(boolean.convert(s)).to.be(s);
});
});

View file

@ -0,0 +1,20 @@
import expect from 'expect.js';
import { BytesFormat } from '../bytes';
describe('BytesFormat', function () {
const config = {};
config['format:bytes:defaultPattern'] = '0,0.[000]b';
const getConfig = (key) => config[key];
it('default pattern', ()=> {
const formatter = new BytesFormat({}, getConfig);
expect(formatter.convert(5150000)).to.be('4.911MB');
});
it('custom pattern', ()=> {
const formatter = new BytesFormat({ pattern: '0,0b' }, getConfig);
expect(formatter.convert('5150000')).to.be('5MB');
});
});

View file

@ -0,0 +1,70 @@
import expect from 'expect.js';
import { ColorFormat } from '../color';
describe('Color Format', function () {
describe('field is a number', () => {
it('should add colors if the value is in range', function () {
const colorer = new ColorFormat({
fieldType: 'number',
colors: [{
range: '100:150',
text: 'blue',
background: 'yellow'
}]
});
expect(colorer.convert(99, 'html')).to.eql('<span ng-non-bindable>99</span>');
expect(colorer.convert(100, 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">100</span></span>'
);
expect(colorer.convert(150, 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">150</span></span>'
);
expect(colorer.convert(151, 'html')).to.eql('<span ng-non-bindable>151</span>');
});
it('should not convert invalid ranges', function () {
const colorer = new ColorFormat({
fieldType: 'number',
colors: [{
range: '100150',
text: 'blue',
background: 'yellow'
}]
});
expect(colorer.convert(99, 'html')).to.eql('<span ng-non-bindable>99</span>');
});
});
describe('field is a string', () => {
it('should add colors if the regex matches', function () {
const colorer = new ColorFormat({
fieldType: 'string',
colors: [{
regex: 'A.*',
text: 'blue',
background: 'yellow'
}]
});
const converter = colorer.getConverterFor('html');
expect(converter('B', 'html')).to.eql('<span ng-non-bindable>B</span>');
expect(converter('AAA', 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">AAA</span></span>'
);
expect(converter('AB', 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">AB</span></span>'
);
expect(converter('a', 'html')).to.eql('<span ng-non-bindable>a</span>');
expect(converter('B', 'html')).to.eql('<span ng-non-bindable>B</span>');
expect(converter('AAA', 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">AAA</span></span>'
);
expect(converter('AB', 'html')).to.eql(
'<span ng-non-bindable><span style="color: blue;background-color: yellow;">AB</span></span>'
);
expect(converter('a', 'html')).to.eql('<span ng-non-bindable>a</span>');
});
});
});

View file

@ -0,0 +1,45 @@
import expect from 'expect.js';
import moment from 'moment-timezone';
import { DateFormat } from '../date';
describe('Date Format', function () {
let convert;
let mockConfig;
beforeEach(function () {
mockConfig = {};
mockConfig.dateFormat = 'MMMM Do YYYY, HH:mm:ss.SSS';
mockConfig['dateFormat:tz'] = 'Browser';
const getConfig = (key) => mockConfig[key];
const date = new DateFormat({}, getConfig);
convert = date.convert.bind(date);
});
it('decoding an undefined or null date should return an empty string', function () {
expect(convert(null)).to.be('-');
expect(convert(undefined)).to.be('-');
});
it('should clear the memoization cache after changing the date', function () {
function setDefaultTimezone() {
moment.tz.setDefault(mockConfig['dateFormat:tz']);
}
const time = 1445027693942;
mockConfig['dateFormat:tz'] = 'America/Chicago';
setDefaultTimezone();
const chicagoTime = convert(time);
mockConfig['dateFormat:tz'] = 'America/Phoenix';
setDefaultTimezone();
const phoenixTime = convert(time);
expect(chicagoTime).not.to.equal(phoenixTime);
});
it('should parse date math values', function () {
expect(convert('2015-01-01||+1M/d')).to.be('January 1st 2015, 00:00:00.000');
});
});

View file

@ -0,0 +1,117 @@
import expect from 'expect.js';
import { DurationFormat } from '../duration';
describe('Duration Format', function () {
test({
inputFormat: 'seconds',
outputFormat: 'humanize',
fixtures: [
{
input: -60,
output: 'minus a minute'
},
{
input: 60,
output: 'a minute'
},
{
input: 125,
output: '2 minutes'
}
]
});
test({
inputFormat: 'minutes',
outputFormat: 'humanize',
fixtures: [
{
input: -60,
output: 'minus an hour'
},
{
input: 60,
output: 'an hour'
},
{
input: 125,
output: '2 hours'
}
]
});
test({
inputFormat: 'minutes',
outputFormat: 'asHours',
fixtures: [
{
input: -60,
output: '-1.00'
},
{
input: 60,
output: '1.00'
},
{
input: 125,
output: '2.08'
}
]
});
test({
inputFormat: 'seconds',
outputFormat: 'asSeconds',
outputPrecision: 0,
fixtures: [
{
input: -60,
output: '-60'
},
{
input: 60,
output: '60'
},
{
input: 125,
output: '125'
}
]
});
test({
inputFormat: 'seconds',
outputFormat: 'asSeconds',
outputPrecision: 2,
fixtures: [
{
input: -60,
output: '-60.00'
},
{
input: -32.333,
output: '-32.33'
},
{
input: 60,
output: '60.00'
},
{
input: 125,
output: '125.00'
}
]
});
function test({ inputFormat, outputFormat, outputPrecision, fixtures }) {
fixtures.forEach((fixture) => {
const input = fixture.input;
const output = fixture.output;
it(`should format ${input} ${inputFormat} through ${outputFormat}${outputPrecision ? `, ${outputPrecision} decimals` : ''}`, () => {
const duration = new DurationFormat({ inputFormat, outputFormat, outputPrecision });
expect(duration.convert(input)).to.eql(output);
});
});
}
});

View file

@ -0,0 +1,19 @@
import expect from 'expect.js';
import { IpFormat } from '../ip';
describe('IP Address Format', function () {
let ip;
beforeEach(function () {
ip = new IpFormat();
});
it('converts a value from a decimal to a string', function () {
expect(ip.convert(1186489492)).to.be('70.184.100.148');
});
it('converts null and undefined to -', function () {
expect(ip.convert(null)).to.be('-');
expect(ip.convert(undefined)).to.be('-');
});
});

View file

@ -0,0 +1,20 @@
import expect from 'expect.js';
import { NumberFormat } from '../number';
describe('NumberFormat', function () {
const config = {};
config['format:number:defaultPattern'] = '0,0.[000]';
const getConfig = (key) => config[key];
it('default pattern', ()=> {
const formatter = new NumberFormat({}, getConfig);
expect(formatter.convert(12.345678)).to.be('12.346');
});
it('custom pattern', ()=> {
const formatter = new NumberFormat({ pattern: '0,0' }, getConfig);
expect(formatter.convert('12.345678')).to.be('12');
});
});

View file

@ -0,0 +1,20 @@
import expect from 'expect.js';
import { PercentFormat } from '../percent';
describe('PercentFormat', function () {
const config = {};
config['format:percent:defaultPattern'] = '0,0.[000]%';
const getConfig = (key) => config[key];
it('default pattern', ()=> {
const formatter = new PercentFormat({}, getConfig);
expect(formatter.convert(0.99999)).to.be('99.999%');
});
it('custom pattern', ()=> {
const formatter = new PercentFormat({ pattern: '0,0%' }, getConfig);
expect(formatter.convert('0.99999')).to.be('100%');
});
});

View file

@ -0,0 +1,51 @@
import expect from 'expect.js';
import { StringFormat } from '../string';
describe('String Format', function () {
it('convert a string to lower case', function () {
const string = new StringFormat({
transform: 'lower'
});
expect(string.convert('Kibana')).to.be('kibana');
});
it('convert a string to upper case', function () {
const string = new StringFormat({
transform: 'upper'
});
expect(string.convert('Kibana')).to.be('KIBANA');
});
it('decode a base64 string', function () {
const string = new StringFormat({
transform: 'base64'
});
expect(string.convert('Zm9vYmFy')).to.be('foobar');
});
it('convert a string to title case', function () {
const string = new StringFormat({
transform: 'title'
});
expect(string.convert('PLEASE DO NOT SHOUT')).to.be('Please Do Not Shout');
expect(string.convert('Mean, variance and standard_deviation.')).to.be('Mean, Variance And Standard_deviation.');
expect(string.convert('Stay CALM!')).to.be('Stay Calm!');
});
it('convert a string to short case', function () {
const string = new StringFormat({
transform: 'short'
});
expect(string.convert('dot.notated.string')).to.be('d.n.string');
});
it('convert a string to unknown transform case', function () {
const string = new StringFormat({
transform: 'unknown_transform'
});
const value = 'test test test';
expect(string.convert(value)).to.be(value);
});
});

View file

@ -0,0 +1,29 @@
import expect from 'expect.js';
import { TruncateFormat } from '../truncate';
describe('String TruncateFormat', function () {
it('truncate large string', function () {
const truncate = new TruncateFormat({ fieldLength: 4 });
expect(truncate.convert('This is some text')).to.be('This...');
});
it('does not truncate large string when field length is not a string', function () {
const truncate = new TruncateFormat({ fieldLength: 'not number' });
expect(truncate.convert('This is some text')).to.be('This is some text');
});
it('does not truncate large string when field length is null', function () {
const truncate = new TruncateFormat({ fieldLength: null });
expect(truncate.convert('This is some text')).to.be('This is some text');
});
it('does not truncate large string when field length larger than the text', function () {
const truncate = new TruncateFormat({ fieldLength: 100000 });
expect(truncate.convert('This is some text')).to.be('This is some text');
});
});

View file

@ -0,0 +1,79 @@
import expect from 'expect.js';
import { UrlFormat } from '../url';
describe('UrlFormat', function () {
it('ouputs a simple <a> tab by default', function () {
const url = new UrlFormat();
expect(url.convert('http://elastic.co', 'html'))
.to.be('<span ng-non-bindable><a href="http://elastic.co" target="_blank">http://elastic.co</a></span>');
});
it('outputs an <image> if type === "img"', function () {
const url = new UrlFormat({ type: 'img' });
expect(url.convert('http://elastic.co', 'html'))
.to.be('<span ng-non-bindable><img src="http://elastic.co" alt="A dynamically-specified image located at http://elastic.co"></span>');
});
describe('url template', function () {
it('accepts a template', function () {
const url = new UrlFormat({ urlTemplate: 'url: {{ value }}' });
expect(url.convert('url', 'html'))
.to.be('<span ng-non-bindable><a href="url: url" target="_blank">url: url</a></span>');
});
it('only outputs the url if the contentType === "text"', function () {
const url = new UrlFormat();
expect(url.convert('url', 'text')).to.be('url');
});
});
describe('label template', function () {
it('accepts a template', function () {
const url = new UrlFormat({ labelTemplate: 'extension: {{ value }}' });
expect(url.convert('php', 'html'))
.to.be('<span ng-non-bindable><a href="php" target="_blank">extension: php</a></span>');
});
it('uses the label template for text formating', function () {
const url = new UrlFormat({ labelTemplate: 'external {{value }}' });
expect(url.convert('url', 'text')).to.be('external url');
});
it('can use the raw value', function () {
const url = new UrlFormat({
labelTemplate: 'external {{value}}'
});
expect(url.convert('url?', 'text')).to.be('external url?');
});
it('can use the url', function () {
const url = new UrlFormat({
urlTemplate: 'http://google.com/{{value}}',
labelTemplate: 'external {{url}}'
});
expect(url.convert('url?', 'text')).to.be('external http://google.com/url%3F');
});
});
describe('templating', function () {
it('ignores unknown variables', function () {
const url = new UrlFormat({ urlTemplate: '{{ not really a var }}' });
expect(url.convert('url', 'text')).to.be('');
});
it('does not allow executing code in variable expressions', function () {
const url = new UrlFormat({ urlTemplate: '{{ (__dirname = true) && value }}' });
expect(url.convert('url', 'text')).to.be('');
});
describe('', function () {
it('does not get values from the prototype chain', function () {
const url = new UrlFormat({ urlTemplate: '{{ toString }}' });
expect(url.convert('url', 'text')).to.be('');
});
});
});
});

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import numeral from 'numeral';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import numeral from '@spalger/numeral';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const numeralInst = numeral();

View file

@ -0,0 +1,29 @@
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();
}
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'];
}

View file

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

View file

@ -0,0 +1,53 @@
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;
}
}
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 });
}
};

View file

@ -0,0 +1,51 @@
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);
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;
}
});
}
return this._memoizedConverter(val);
}
static id = 'date';
static title = 'Date';
static fieldType = 'date';
}

View file

@ -0,0 +1,73 @@
import moment from 'moment';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
const ratioToSeconds = {
picoseconds: 0.000000000001,
nanoseconds: 0.000000001,
microseconds: 0.000001
};
const HUMAN_FRIENDLY = 'humanize';
const DEFAULT_OUTPUT_PRECISION = 2;
const DEFAULT_INPUT_FORMAT = { text: 'Seconds', kind: 'seconds' };
const inputFormats = [
{ text: 'Picoseconds', kind: 'picoseconds' },
{ text: 'Nanoseconds', kind: 'nanoseconds' },
{ text: 'Microseconds', kind: 'microseconds' },
{ text: 'Milliseconds', kind: 'milliseconds' },
DEFAULT_INPUT_FORMAT,
{ text: 'Minutes', kind: 'minutes' },
{ text: 'Hours', kind: 'hours' },
{ text: 'Days', kind: 'days' },
{ text: 'Weeks', kind: 'weeks' },
{ text: 'Months', kind: 'months' },
{ text: 'Years', kind: 'years' }
];
const DEFAULT_OUTPUT_FORMAT = { text: 'Human Readable', method: 'humanize' };
const outputFormats = [
DEFAULT_OUTPUT_FORMAT,
{ text: 'Milliseconds', method: 'asMilliseconds' },
{ text: 'Seconds', method: 'asSeconds' },
{ text: 'Minutes', method: 'asMinutes' },
{ text: 'Hours', method: 'asHours' },
{ text: 'Days', method: 'asDays' },
{ text: 'Weeks', method: 'asWeeks' },
{ text: 'Months', method: 'asMonths' },
{ 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);
}

View file

@ -0,0 +1,15 @@
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
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('.');
}
static id = 'ip';
static title = 'IP Address';
static fieldType = 'ip';
}

View file

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

View file

@ -0,0 +1,18 @@
import _ from 'lodash';
import { Numeral } 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) {
return this.param('fractional') ? val : val / 100;
})
}
});

View file

@ -0,0 +1,49 @@
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 = `
<dl class="source truncate-by-height">
<% defPairs.forEach(function (def) { %>
<dt><%- def[0] %>:</dt>
<dd><%= def[1] %></dd>
<%= ' ' %>
<% }); %>
</dl>`;
const template = _.template(noWhiteSpace(templateHtml));
export class SourceFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
}
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) });
}
};

View file

@ -0,0 +1,50 @@
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);
}
}
_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

@ -0,0 +1,22 @@
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
});
}
return val;
}
static id = 'truncate';
static title = 'Truncated String';
static fieldType = ['string'];
}

View file

@ -0,0 +1,112 @@
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>`;
}
}
};

View file

@ -1,11 +1,10 @@
import _ from 'lodash';
import angular from 'angular';
import { highlightTags } from './highlight_tags';
import { htmlTags } from './html_tags';
export function getHighlightHtml(fieldValue, highlights) {
let highlightHtml = (typeof fieldValue === 'object')
? angular.toJson(fieldValue)
? JSON.stringify(fieldValue)
: fieldValue;
_.each(highlights, function (highlight) {

View file

@ -1,7 +1,8 @@
import _ from 'lodash';
import expect from 'expect.js';
import sinon from 'sinon';
import * as aggressiveParse from 'ui/utils/aggressive_parse';
import * as aggressiveParse from '../aggressive_parse';
describe('aggressiveParse', () => {
let object;

View file

@ -1,5 +1,5 @@
import expect from 'expect.js';
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { asPrettyString } from '../as_pretty_string';
describe('asPrettyString', () => {

View file

@ -1,5 +1,5 @@
import expect from 'expect.js';
import { shortenDottedString } from 'ui/utils/shorten_dotted_string';
import { shortenDottedString } from '../shorten_dotted_string';
describe('shortenDottedString', () => {

View file

@ -10,6 +10,7 @@ import { importApi } from './server/routes/api/import';
import { exportApi } from './server/routes/api/export';
import scripts from './server/routes/api/scripts';
import { registerSuggestionsApi } from './server/routes/api/suggestions';
import { registerFieldFormats } from './server/field_formats/register';
import * as systemApi from './server/lib/system_api';
import handleEsError from './server/lib/handle_es_error';
import mappings from './mappings.json';
@ -33,6 +34,7 @@ export default function (kibana) {
uiExports: {
hacks: ['plugins/kibana/dev_tools/hacks/hide_empty_tools'],
fieldFormats: ['plugins/kibana/field_formats/register'],
app: {
id: 'kibana',
title: 'Kibana',
@ -135,10 +137,12 @@ export default function (kibana) {
importApi(server);
exportApi(server);
registerSuggestionsApi(server);
registerFieldFormats(server);
server.expose('systemApi', systemApi);
server.expose('handleEsError', handleEsError);
server.expose('injectVars', injectVars);
}
});
}

View file

@ -2,7 +2,7 @@ import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { FieldFormat } from '../../../../../ui/field_formats/field_format';
let fieldFormats;
let config;

View file

@ -0,0 +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';
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);

View file

@ -0,0 +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';
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);
}

View file

@ -1,23 +1,20 @@
import _ from 'lodash';
import expect from 'expect.js';
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { asPrettyString } from '../../../core_plugins/kibana/common/utils/as_pretty_string';
import { FieldFormat } from '../field_format';
describe('FieldFormat class', function () {
let TestFormat;
beforeEach(function () {
TestFormat = function (params) {
TestFormat.Super.call(this, params);
TestFormat = class _TestFormat extends FieldFormat {
static id = 'test-format';
static title = 'Test Format';
_convert(val) {
return asPrettyString(val);
}
};
TestFormat.id = 'test-format';
TestFormat.title = 'Test Format';
TestFormat.prototype._convert = asPrettyString;
_.class(TestFormat).inherits(FieldFormat);
});
describe('params', function () {

View file

@ -0,0 +1,32 @@
import expect from 'expect.js';
import { FieldFormatsService } from '../field_formats_service';
import { NumberFormat } from '../../../core_plugins/kibana/common/field_formats/types/number';
describe('FieldFormatsService', function () {
const config = {};
config['format:defaultTypeMap'] = {
'number': { 'id': 'number', 'params': {} },
'_default_': { 'id': 'string', 'params': {} }
};
config['format:number:defaultPattern'] = '0,0.[000]';
const getConfig = (key) => config[key];
const fieldFormatClasses = [NumberFormat];
let fieldFormats;
beforeEach(function () {
fieldFormats = new FieldFormatsService(fieldFormatClasses, getConfig);
});
it('FieldFormats are accessible via getType method', function () {
const Type = fieldFormats.getType('number');
expect(Type.id).to.be('number');
});
it('getDefaultInstance returns default FieldFormat instance for fieldType', function () {
const instance = fieldFormats.getDefaultInstance('number', getConfig);
expect(instance.type.id).to.be('number');
expect(instance.convert('0.33333')).to.be('0.333');
});
});

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { getHighlightHtml } from 'ui/highlight';
import { asPrettyString } from '../../core_plugins/kibana/common/utils/as_pretty_string';
import { getHighlightHtml } from '../../core_plugins/kibana/common/highlight/highlight_html';
const types = {
html: function (format, convert) {

View file

@ -1,5 +1,5 @@
import _ from 'lodash';
import { contentTypesSetup } from 'ui/index_patterns/_field_format/content_types';
import { contentTypesSetup } from './content_types';
export function FieldFormat(params) {
// give the constructor a more appropriate name

View file

@ -0,0 +1,53 @@
import _ from 'lodash';
export class FieldFormatsService {
constructor(fieldFormatClasses, getConfig) {
this._fieldFormats = _.indexBy(fieldFormatClasses, 'id');
this.getConfig = getConfig;
}
/**
* Get the id of the default type for this field type
* using the format:defaultTypeMap config map
*
* @param {String} fieldType - the field type
* @return {String}
*/
getDefaultConfig(fieldType) {
const defaultMap = this.getConfig('format:defaultTypeMap');
return defaultMap[fieldType] || defaultMap._default_;
}
/**
* Get the default fieldFormat instance for a field type.
*
* @param {String} fieldType
* @return {FieldFormat}
*/
getDefaultInstance(fieldType) {
const conf = this.getDefaultConfig(fieldType);
const FieldFormat = this._fieldFormats[conf.id];
return new FieldFormat(conf.params, this.getConfig);
}
/**
* Get the fieldFormat instance for a field format configuration.
*
* @param {Object} conf:id, conf:params
* @return {FieldFormat}
*/
getInstance(conf) {
const FieldFormat = this._fieldFormats[conf.id];
return new FieldFormat(conf.params, this.getConfig);
}
/**
* Get a FieldFormat type (class) by it's id.
*
* @param {String} fieldFormatId - the FieldFormat id
* @return {FieldFormat}
*/
getType(fieldFormatId) {
return this._fieldFormats[fieldFormatId];
}
}

View file

@ -0,0 +1,28 @@
import _ from 'lodash';
import { FieldFormatsService } from './field_formats/field_formats_service';
export function fieldFormatsMixin(kbnServer, server) {
const fieldFormatClasses = [];
// for use in the context of a request, the default context
server.decorate('request', 'getFieldFormatService', async function () {
return await server.fieldFormatServiceFactory(this.getUiSettingsService());
});
// for use outside of the request context, for special cases
server.decorate('server', 'fieldFormatServiceFactory', async function (uiSettings) {
const uiConfigs = await uiSettings.getAll();
const uiSettingDefaults = await uiSettings.getDefaults();
Object.keys(uiSettingDefaults).forEach((key) => {
if (_.has(uiConfigs, key) && uiSettingDefaults[key].type === 'json') {
uiConfigs[key] = JSON.parse(uiConfigs[key]);
}
});
const getConfig = (key) => uiConfigs[key];
return new FieldFormatsService(fieldFormatClasses, getConfig);
});
server.decorate('server', 'registerFieldFormatClass', (FieldFormat) => {
fieldFormatClasses.push(FieldFormat);
});
}

View file

@ -10,6 +10,7 @@ import UiBundlerEnv from './ui_bundler_env';
import { UiI18n } from './ui_i18n';
import { uiSettingsMixin } from './ui_settings';
import { fieldFormatsMixin } from './field_formats_mixin';
export default async (kbnServer, server, config) => {
const uiExports = kbnServer.uiExports = new UiExports({
@ -18,6 +19,8 @@ export default async (kbnServer, server, config) => {
await kbnServer.mixin(uiSettingsMixin);
await kbnServer.mixin(fieldFormatsMixin);
const uiI18n = kbnServer.uiI18n = new UiI18n(config.get('i18n.defaultLocale'));
uiI18n.addUiExportConsumer(uiExports);

View file

@ -1,6 +1,6 @@
import { AggTypesBucketsBucketAggTypeProvider } from 'ui/agg_types/buckets/_bucket_agg_type';
import { AggTypesBucketsCreateFilterRangeProvider } from 'ui/agg_types/buckets/create_filter/range';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { FieldFormat } from '../../../field_formats/field_format';
import { RangeKeyProvider } from './range_key';
import rangesTemplate from 'ui/agg_types/controls/ranges.html';

View file

@ -24,7 +24,6 @@ import 'ui/modals';
import 'ui/state_management/app_state';
import 'ui/state_management/global_state';
import 'ui/storage';
import 'ui/stringify/register';
import 'ui/style_compile';
import 'ui/timefilter';
import 'ui/timepicker';

View file

@ -8,7 +8,7 @@ import { ErrorHandlersProvider } from '../_error_handlers';
import { FetchProvider } from '../fetch';
import { DecorateQueryProvider } from './_decorate_query';
import { FieldWildcardProvider } from '../../field_wildcard';
import { getHighlightRequest } from '../../highlight';
import { getHighlightRequest } from '../../../../core_plugins/kibana/common/highlight';
import { migrateFilter } from './_migrate_filter';
export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, config) {

View file

@ -1,7 +1,6 @@
import _ from 'lodash';
import angular from 'angular';
import { toJson } from 'ui/utils/aggressive_parse';
import { toJson } from '../../../../../core_plugins/kibana/common/utils/aggressive_parse';
export function SearchStrategyProvider(Private, Promise, timefilter, kbnIndex, sessionId) {

View file

@ -1,13 +1,11 @@
import _ from 'lodash';
import $ from 'jquery';
import rison from 'rison-node';
import 'ui/highlight';
import 'ui/highlight/highlight_tags';
import 'ui/doc_viewer';
import 'ui/filters/trust_as_html';
import 'ui/filters/short_dots';
import './table_row.less';
import { noWhiteSpace } from 'ui/utils/no_white_space';
import { noWhiteSpace } from '../../../../core_plugins/kibana/common/utils/no_white_space';
import openRowHtml from 'ui/doc_table/components/table_row/open.html';
import detailsHtml from 'ui/doc_table/components/table_row/details.html';
import { uiModules } from 'ui/modules';

View file

@ -1,6 +1,6 @@
import './color.less';
import colorTemplate from './color.html';
import { DEFAULT_COLOR } from 'ui/stringify/types/color_default';
import { DEFAULT_COLOR } from '../../../../../core_plugins/kibana/common/field_formats/types/color_default';
export function colorEditor() {
return {

View file

@ -1,5 +1,5 @@
import _ from 'lodash';
import { shortenDottedString } from 'ui/utils/shorten_dotted_string';
import { shortenDottedString } from '../../../core_plugins/kibana/common/utils/shorten_dotted_string';
import { uiModules } from 'ui/modules';
// Shorts dot notated strings
// eg: foo.bar.baz becomes f.b.baz

View file

@ -1,5 +1,4 @@
import './_index_pattern';
import './_get_computed_fields';
import './_field_format';
describe('Index Patterns', function () {
});

View file

@ -1,5 +1,5 @@
import { ObjDefine } from 'ui/utils/obj_define';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { FieldFormat } from '../../field_formats/field_format';
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
import { getKbnFieldType } from '../../../utils';

View file

@ -1,98 +0,0 @@
import angular from 'angular';
import $ from 'jquery';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import expect from 'expect.js';
import { escape } from 'lodash';
import { contentTypesSetup } from '../content_types';
describe('index_patterns/_field_format/content_types', () => {
let render;
const callMe = sinon.stub();
afterEach(() => callMe.reset());
function getAllContents(node) {
return [...node.childNodes].reduce((acc, child) => {
return acc.concat(child, getAllContents(child));
}, []);
}
angular.module('testApp', [])
.directive('testDirective', () => ({
restrict: 'EACM',
link: callMe
}));
beforeEach(ngMock.module('testApp'));
beforeEach(ngMock.inject(($injector) => {
const $rootScope = $injector.get('$rootScope');
const $compile = $injector.get('$compile');
$rootScope.callMe = callMe;
render = (convert) => {
const $el = $('<div>');
const { html } = contentTypesSetup({ _convert: { html: convert } });
$compile($el.html(html(`
<!-- directive: test-directive -->
<div></div>
<test-directive>{{callMe()}}</test-directive>
<span test-directive></span>
<marquee class="test-directive"></marquee>
`)))($rootScope);
return $el;
};
}));
it('no element directive', () => {
const $el = render(value => `
<test-directive>${escape(value)}</test-directive>
`);
expect($el.find('test-directive')).to.have.length(1);
sinon.assert.notCalled(callMe);
});
it('no attribute directive', () => {
const $el = render(value => `
<div test-directive>${escape(value)}</div>
`);
expect($el.find('[test-directive]')).to.have.length(1);
sinon.assert.notCalled(callMe);
});
it('no comment directive', () => {
const $el = render(value => `
<!-- directive: test-directive -->
<div>${escape(value)}</div>
`);
const comments = getAllContents($el.get(0))
.filter(node => node.nodeType === 8);
expect(comments).to.have.length(1);
expect(comments[0].textContent).to.contain('test-directive');
sinon.assert.notCalled(callMe);
});
it('no class directive', () => {
const $el = render(value => `
<div class="test-directive">${escape(value)}</div>
`);
expect($el.find('.test-directive')).to.have.length(1);
sinon.assert.notCalled(callMe);
});
it('no interpolation', () => {
const $el = render(value => `
<div class="foo {{callMe()}}">${escape(value)}</div>
`);
expect($el.find('.foo')).to.have.length(1);
sinon.assert.notCalled(callMe);
});
});

View file

@ -1,26 +0,0 @@
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
import { stringifyUrl } from 'ui/stringify/types/url';
import { stringifyBytes } from 'ui/stringify/types/bytes';
import { stringifyDate } from 'ui/stringify/types/date';
import { stringifyDuration } from 'ui/stringify/types/duration';
import { stringifyIp } from 'ui/stringify/types/ip';
import { stringifyNumber } from 'ui/stringify/types/number';
import { stringifyPercent } from 'ui/stringify/types/percent';
import { stringifyString } from 'ui/stringify/types/string';
import { stringifySource } from 'ui/stringify/types/source';
import { stringifyColor } from 'ui/stringify/types/color';
import { stringifyTruncate } from 'ui/stringify/types/truncate';
import { stringifyBoolean } from 'ui/stringify/types/boolean';
RegistryFieldFormatsProvider.register(stringifyUrl);
RegistryFieldFormatsProvider.register(stringifyBytes);
RegistryFieldFormatsProvider.register(stringifyDate);
RegistryFieldFormatsProvider.register(stringifyDuration);
RegistryFieldFormatsProvider.register(stringifyIp);
RegistryFieldFormatsProvider.register(stringifyNumber);
RegistryFieldFormatsProvider.register(stringifyPercent);
RegistryFieldFormatsProvider.register(stringifyString);
RegistryFieldFormatsProvider.register(stringifySource);
RegistryFieldFormatsProvider.register(stringifyColor);
RegistryFieldFormatsProvider.register(stringifyTruncate);
RegistryFieldFormatsProvider.register(stringifyBoolean);

View file

@ -1,7 +0,0 @@
<dl class="source truncate-by-height">
<% defPairs.forEach(function (def) { %>
<dt><%- def[0] %>:</dt>
<dd><%= def[1] %></dd>
<%= ' ' %>
<% }); %>
</dl>

View file

@ -1,35 +0,0 @@
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyBoolean() {
class Bool 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);
}
}
static id = 'boolean';
static title = 'Boolean';
static fieldType = ['boolean', 'number', 'string'];
}
return Bool;
}

View file

@ -1,8 +0,0 @@
import { Numeral } from 'ui/stringify/types/_numeral';
export function stringifyBytes() {
return Numeral.factory({
id: 'bytes',
title: 'Bytes'
});
}

View file

@ -1,60 +0,0 @@
import _ from 'lodash';
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { DEFAULT_COLOR } from './color_default';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyColor() {
const convertTemplate = _.template('<span style="<%- style %>"><%- val %></span>');
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'
];
}
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;
}

View file

@ -1,56 +0,0 @@
import _ from 'lodash';
import moment from 'moment';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyDate() {
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;
}
});
}
return this._memoizedConverter(val);
}
static id = 'date';
static title = 'Date';
static fieldType = 'date';
}
return DateFormat;
}

View file

@ -1,78 +0,0 @@
import moment from 'moment';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyDuration() {
const ratioToSeconds = {
picoseconds: 0.000000000001,
nanoseconds: 0.000000001,
microseconds: 0.000001
};
const HUMAN_FRIENDLY = 'humanize';
const DEFAULT_OUTPUT_PRECISION = 2;
const DEFAULT_INPUT_FORMAT = { text: 'Seconds', kind: 'seconds' };
const inputFormats = [
{ text: 'Picoseconds', kind: 'picoseconds' },
{ text: 'Nanoseconds', kind: 'nanoseconds' },
{ text: 'Microseconds', kind: 'microseconds' },
{ text: 'Milliseconds', kind: 'milliseconds' },
DEFAULT_INPUT_FORMAT,
{ text: 'Minutes', kind: 'minutes' },
{ text: 'Hours', kind: 'hours' },
{ text: 'Days', kind: 'days' },
{ text: 'Weeks', kind: 'weeks' },
{ text: 'Months', kind: 'months' },
{ text: 'Years', kind: 'years' }
];
const DEFAULT_OUTPUT_FORMAT = { text: 'Human Readable', method: 'humanize' };
const outputFormats = [
DEFAULT_OUTPUT_FORMAT,
{ text: 'Milliseconds', method: 'asMilliseconds' },
{ text: 'Seconds', method: 'asSeconds' },
{ text: 'Minutes', method: 'asMinutes' },
{ text: 'Hours', method: 'asHours' },
{ text: 'Days', method: 'asDays' },
{ text: 'Weeks', method: 'asWeeks' },
{ text: 'Months', method: 'asMonths' },
{ text: 'Years', method: 'asYears' }
];
class Duration extends FieldFormat {
isHuman() {
return this.param('outputFormat') === HUMAN_FRIENDLY;
}
_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;
}
Duration.prototype.getParamDefaults = function () {
return {
inputFormat: DEFAULT_INPUT_FORMAT.kind,
outputFormat: DEFAULT_OUTPUT_FORMAT.method,
outputPrecision: DEFAULT_OUTPUT_PRECISION
};
};
return Duration;
function parseInputAsDuration(val, inputFormat) {
const ratio = ratioToSeconds[inputFormat] || 1;
const kind = inputFormat in ratioToSeconds ? 'seconds' : inputFormat;
return moment.duration(val * ratio, kind);
}
}

View file

@ -1,20 +0,0 @@
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyIp() {
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('.');
}
static id = 'ip';
static title = 'IP Address';
static fieldType = 'ip';
}
return IpFormat;
}

View file

@ -1,8 +0,0 @@
import { Numeral } from 'ui/stringify/types/_numeral';
export function stringifyNumber() {
return Numeral.factory({
id: 'number',
title: 'Number'
});
}

View file

@ -1,20 +0,0 @@
import _ from 'lodash';
import { Numeral } from 'ui/stringify/types/_numeral';
export function stringifyPercent() {
return Numeral.factory({
id: 'percent',
title: 'Percentage',
getParamDefaults: (getConfig) => {
return {
pattern: getConfig('format:percent:defaultPattern'),
fractional: true
};
},
prototype: {
_convert: _.compose(Numeral.prototype._convert, function (val) {
return this.param('fractional') ? val : val / 100;
})
}
});
}

View file

@ -1,45 +0,0 @@
import _ from 'lodash';
import { noWhiteSpace } from 'ui/utils/no_white_space';
import { toJson } from 'ui/utils/aggressive_parse';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { shortenDottedString } from 'ui/utils/shorten_dotted_string';
export function stringifySource() {
const template = _.template(noWhiteSpace(require('ui/stringify/types/_source.html')));
class SourceFormat extends FieldFormat {
constructor(params, getConfig) {
super(params);
this.getConfig = getConfig;
}
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;
}

View file

@ -1,55 +0,0 @@
import { asPrettyString } from 'ui/utils/as_pretty_string';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { shortenDottedString } from 'ui/utils/shorten_dotted_string';
export function stringifyString() {
class StringFormat extends FieldFormat {
getParamDefaults() {
return {
transform: false
};
}
_base64Decode(val) {
try {
return window.atob(val);
} catch (e) {
return asPrettyString(val);
}
}
_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'
];
}
return StringFormat;
}

View file

@ -1,27 +0,0 @@
import _ from 'lodash';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
export function stringifyTruncate() {
const omission = '...';
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;
}
static id = 'truncate';
static title = 'Truncated String';
static fieldType = ['string'];
}
return TruncateFormat;
}

View file

@ -1,117 +0,0 @@
import _ from 'lodash';
import { FieldFormat } from 'ui/index_patterns/_field_format/field_format';
import { getHighlightHtml } from 'ui/highlight';
export function stringifyUrl() {
const templateMatchRE = /{{([\s\S]+?)}}/g;
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;
}