Merge pull request #2731 from spalger/fieldFormatting

Field formatting
This commit is contained in:
Spencer 2015-05-04 14:33:51 -07:00
commit 5924d844e6
130 changed files with 3303 additions and 1283 deletions

View file

@ -1,5 +1,4 @@
define(function (require) {
var _ = require('lodash');
define(function () {
return function (leaf) {
// walk up the branch for each parent
function walk(item, memo) {

View file

@ -1,4 +1,4 @@
define(function (require) {
define(function () {
return function PointSeriesInitX() {
return function initXAxis(chart) {
var x = chart.aspects.x;
@ -14,4 +14,4 @@ define(function (require) {
}
};
};
});
});

View file

@ -19,4 +19,4 @@ define(function (require) {
chart.yScale = xAggOutput.metricScale || null;
};
};
});
});

View file

@ -1,6 +1,5 @@
define(function (require) {
return function PointSeriesTooltipFormatter($compile, $rootScope) {
var _ = require('lodash');
var $ = require('jquery');
var $tooltipScope = $rootScope.$new();

View file

@ -8,8 +8,6 @@ define(function (require) {
.directive('kbnAggTable', function ($filter, config, Private, compileRecursiveDirective) {
var _ = require('lodash');
var orderBy = $filter('orderBy');
return {
restrict: 'E',
template: require('text!components/agg_table/agg_table.html'),
@ -54,7 +52,7 @@ define(function (require) {
}
// escape each cell in each row
var csvRows = rows.map(function (row, i) {
var csvRows = rows.map(function (row) {
return row.map(escape);
});
@ -72,16 +70,13 @@ define(function (require) {
var table = $scope.table;
if (!table) {
$scope.formattedRows = null;
$scope.rows = null;
$scope.formattedColumns = null;
return;
}
setFormattedRows(table);
setFormattedColumns(table);
});
function setFormattedColumns(table) {
self.csv.filename = (table.title() || 'table') + '.csv';
$scope.rows = table.rows;
$scope.formattedColumns = table.columns.map(function (col, i) {
var agg = $scope.table.aggConfig(col);
var field = agg.field();
@ -98,14 +93,7 @@ define(function (require) {
return formattedColumn;
});
}
function setFormattedRows(table) {
$scope.rows = table.rows;
// update the csv file's title
self.csv.filename = (table.title() || 'table') + '.csv';
}
});
}
};
});

View file

@ -1,15 +1,19 @@
define(function (require) {
return function MetricAggTypeProvider(Private, indexPatterns) {
return function MetricAggTypeProvider(Private) {
var _ = require('lodash');
var AggType = Private(require('components/agg_types/_agg_type'));
var fieldFormats = Private(require('registry/field_formats'));
_(MetricAggType).inherits(AggType);
function MetricAggType(config) {
MetricAggType.Super.call(this, config);
if (_.isFunction(config.getValue)) {
this.getValue = config.getValue;
}
// allow overriding any value on the prototype
_.forOwn(config, function (val, key) {
if (_.has(MetricAggType.prototype, key)) {
this[key] = val;
}
}, this);
}
/**
@ -21,15 +25,19 @@ define(function (require) {
return bucket[agg.id].value;
};
/**
* Pick a format for the values produced by this agg type,
* overriden by several metrics that always output a simple
* number
*
* @param {agg} agg - the agg to pick a format for
* @return {FieldFromat}
*/
MetricAggType.prototype.getFormat = function (agg) {
var field = agg.field();
if (field && field.type === 'date' && field.format) {
return field.format;
} else {
return indexPatterns.fieldFormats.byName.number;
}
return field ? field.format : fieldFormats.getDefaultInstance('number');
};
return MetricAggType;
};
});
});

View file

@ -16,4 +16,4 @@ define(function (require) {
]
});
};
});
});

View file

@ -1,6 +1,7 @@
define(function (require) {
return function AggTypeMetricCardinalityProvider(Private) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var fieldFormats = Private(require('registry/field_formats'));
return new MetricAggType({
name: 'cardinality',
@ -8,6 +9,9 @@ define(function (require) {
makeLabel: function (aggConfig) {
return 'Unique count of ' + aggConfig.params.field.displayName;
},
getFormat: function () {
return fieldFormats.getDefaultInstance('number');
},
params: [
{
name: 'field'
@ -15,4 +19,4 @@ define(function (require) {
]
});
};
});
});

View file

@ -1,17 +1,21 @@
define(function (require) {
return function AggTypeMetricCountProvider(Private) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var fieldFormats = Private(require('registry/field_formats'));
return new MetricAggType({
name: 'count',
title: 'Count',
hasNoDsl: true,
makeLabel: function (aggConfig) {
makeLabel: function () {
return 'Count';
},
getFormat: function () {
return fieldFormats.getDefaultInstance('number');
},
getValue: function (agg, bucket) {
return bucket.doc_count;
}
});
};
});
});

View file

@ -4,6 +4,7 @@ define(function (require) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var fieldFormats = Private(require('registry/field_formats'));
var valuesEditor = require('text!components/agg_types/controls/percentile_ranks.html');
// required by the values editor
@ -39,6 +40,9 @@ define(function (require) {
return new ValueAggConfig(value);
});
},
getFormat: function () {
return fieldFormats.getInstance('percent') || fieldFormats.getDefaultInstance('number');
},
getValue: function (agg, bucket) {
// values for 1, 5, and 10 will come back as 1.0, 5.0, and 10.0 so we
// parse the keys and respond with the value that matches

View file

@ -5,6 +5,7 @@ define(function (require) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var ordinalSuffix = require('utils/ordinal_suffix');
var fieldFormats = Private(require('registry/field_formats'));
var percentsEditor = require('text!components/agg_types/controls/percentiles.html');
// required by the percentiles editor
@ -40,6 +41,9 @@ define(function (require) {
return new ValueAggConfig(percent);
});
},
getFormat: function () {
return fieldFormats.getInstance('percent') || fieldFormats.getDefaultInstance('number');
},
getValue: function (agg, bucket) {
// percentiles for 1, 5, and 10 will come back as 1.0, 5.0, and 10.0 so we
// parse the keys and respond with the value that matches

View file

@ -1,9 +1,20 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
require('modules').get('kibana')
.config(function ($provide) {
function strictEquality(a, b) {
// are the values equal? or, are they both NaN?
return a === b || (a !== a && b !== b);
}
function errorNotAssignable(source, target) {
throw Error('Unable to accept change to bound $scope property "' + source + '"' +
' because source expression "' + target + '" is not assignable!');
}
$provide.decorator('$rootScope', function ($delegate, $parse) {
/**
* Two-way bind a value from scope to another property on scope. This
@ -12,23 +23,58 @@ define(function (require) {
*
* @param {expression} to - the location on scope to bind to
* @param {expression} from - the location on scope to bind from
* @param {Scope} $sourceScope - the scope to read "from" expression from
* @return {undefined}
*/
$delegate.constructor.prototype.$bind = function (to, from) {
var $source = this.$parent;
$delegate.constructor.prototype.$bind = function (to, from, $sourceScope) {
var $source = $sourceScope || this.$parent;
var $target = this;
var getter = $parse(from);
var setter = $parse(to).assign;
// parse expressions
var $to = $parse(to);
if (!$to.assign) errorNotAssignable(to, from);
var $from = $parse(from);
$from.assignOrFail = $from.assign || function () {
// revert the change and throw an error, child writes aren't supported
$to($target, lastSourceVal = $from($source));
errorNotAssignable(from, to);
};
setter($target, getter($source));
this.$watch(
function () { return getter($source); },
function (val) { setter($target, val); }
);
// bind scopes to expressions
var getTarget = function () { return $to($target); };
var setTarget = function (v) { return $to.assign($target, v); };
var getSource = function () { return $from($source); };
var setSource = function (v) { return $from.assignOrFail($source, v); };
// if we are syncing down a literal, then we use loose equality check
var strict = !$from.literal;
var compare = strict ? strictEquality : angular.equals;
// to support writing from the child to the parent we need to know
// which source has changed. Track the source value and anytime it
// changes (even if the target value changed too) push from source
// to target. If the source hasn't changed then the change is from
// the target and push accordingly
var lastSourceVal = getSource();
// push the initial value down, start off in sync
setTarget(lastSourceVal);
$target.$watch(function () {
var sourceVal = getSource();
var targetVal = getTarget();
var outOfSync = !compare(sourceVal, targetVal);
var sourceChanged = outOfSync && !compare(sourceVal, lastSourceVal);
if (sourceChanged) setTarget(sourceVal);
else if (outOfSync) setSource(targetVal);
return lastSourceVal = sourceVal;
}, null, !strict);
};
return $delegate;
});
});
});
});

View file

@ -0,0 +1,43 @@
define(function (require) {
return function BoundToConfigObjProvider($rootScope, config) {
var _ = require('lodash');
/**
* Create an object with properties that may be bound to config values.
* The input object is basically cloned unless one of it's own properties
* resolved to a string value that starts with an equal sign. When that is
* found, that property is forever bound to the corresponding config key.
*
* example:
*
* // name is cloned, height is bound to the defaultHeight config key
* { name: 'john', height: '=defaultHeight' };
*
* @param {Object} input
* @return {Object}
*/
function BoundToConfigObj(input) {
var self = this;
_.forOwn(input, function (val, prop) {
if (!_.isString(val) || val.charAt(0) !== '=') {
self[prop] = val;
return;
}
var configKey = val.substr(1);
update();
$rootScope.$on('init:config', update);
$rootScope.$on('change:config.' + configKey, update);
function update() {
self[prop] = config.get(configKey);
}
});
}
return BoundToConfigObj;
};
});

View file

@ -1,6 +1,6 @@
define(function (require) {
return function () {
var _ = require('lodash');
define(function () {
return function configDefaultsProvider() {
// wraped in provider so that a new instance is given to each app/test
return {
'query:queryString:options': {
@ -87,6 +87,36 @@ define(function (require) {
value: 5,
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
'patterns from which to query the field mapping.'
},
'format:defaultTypeMap': {
type: 'json',
value: [
'{',
' "ip": { "id": "ip", "params": {} },',
' "date": { "id": "date", "params": {} },',
' "number": { "id": "number", "params": {} },',
' "_source": { "id": "_source", "params": {} },',
' "_default_": { "id": "string", "params": {} }',
'}',
].join('\n'),
description: 'Map of the format name to use by default for each field type. ' +
'"_default_" is used if the field type is not mentioned explicitly.'
},
'format:number:defaultPattern': {
type: 'string',
value: '0,0.[000]'
},
'format:bytes:defaultPattern': {
type: 'string',
value: '0,0.[000]b'
},
'format:percent:defaultPattern': {
type: 'string',
value: '0,0.[000]%'
},
'format:currency:defaultPattern': {
type: 'string',
value: '($0,0.[00])'
}
};
};

View file

@ -2,7 +2,6 @@ define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var addWordBreaks = require('utils/add_word_breaks');
var noWhiteSpace = require('utils/no_white_space');
var module = require('modules').get('app/discover');
require('components/highlight/highlight');
@ -23,12 +22,11 @@ define(function (require) {
* <tr ng-repeat="row in rows" kbn-table-row="row"></tr>
* ```
*/
module.directive('kbnTableRow', function ($compile, config, highlightFilter, highlightTags, shortDotsFilter, courier) {
module.directive('kbnTableRow', function ($compile) {
var openRowHtml = require('text!components/doc_table/components/table_row/open.html');
var detailsHtml = require('text!components/doc_table/components/table_row/details.html');
var cellTemplate = _.template(require('text!components/doc_table/components/table_row/cell.html'));
var truncateByHeightTemplate = _.template(require('text!partials/truncate_by_height.html'));
var sourceTemplate = _.template(noWhiteSpace(require('text!components/doc_table/components/table_row/_source.html')));
return {
restrict: 'A',
@ -38,12 +36,11 @@ define(function (require) {
indexPattern: '=',
row: '=kbnTableRow'
},
link: function ($scope, $el, attrs) {
link: function ($scope, $el) {
$el.after('<tr>');
$el.empty();
var init = function () {
_formatRow($scope.row);
createSummaryRow($scope.row, $scope.row._id);
};
@ -92,37 +89,24 @@ define(function (require) {
// create a tr element that lists the value for each *column*
function createSummaryRow(row) {
var indexPattern = $scope.indexPattern;
// We just create a string here because its faster.
var newHtmls = [
openRowHtml
];
if ($scope.indexPattern.timeFieldName) {
if (indexPattern.timeFieldName) {
newHtmls.push(cellTemplate({
timefield: true,
formatted: _displayField(row, $scope.indexPattern.timeFieldName)
formatted: _displayField(row, indexPattern.timeFieldName)
}));
}
$scope.columns.forEach(function (column) {
var formatted;
var sources = _.extend({}, row.$$_formatted, row.highlight);
if (column === '_source') {
var sourceConfig = {
source: _.mapValues(sources, function (val, field) {
return _displayField(row, field, false);
}),
highlight: row.highlight,
shortDotsFilter: shortDotsFilter
};
formatted = sourceTemplate(sourceConfig);
} else {
formatted = _displayField(row, column, true);
}
newHtmls.push(cellTemplate({
timefield: false,
formatted: formatted
formatted: _displayField(row, column, true)
}));
});
@ -164,9 +148,9 @@ define(function (require) {
/**
* Fill an element with the value of a field
*/
function _displayField(row, field, breakWords) {
var text = _getValForField(row, field);
text = highlightFilter(text, row.highlight && row.highlight[field]);
function _displayField(row, fieldName, breakWords) {
var indexPattern = $scope.indexPattern;
var text = indexPattern.formatField(row, fieldName);
if (breakWords) {
text = addWordBreaks(text, MIN_LINE_LENGTH);
@ -181,56 +165,6 @@ define(function (require) {
return text;
}
/**
* get the value of a field from a row, serialize it to a string
* and truncate it if necessary
*
* @param {object} row - the row to pull the value from
* @param {string} field - the name of the field (dot-seperated paths are accepted)
* @return {[type]} a string, which should be inserted as text, or an element
*/
function _getValForField(row, field) {
var val;
if (row.highlight && row.highlight[field]) {
// Strip out the highlight tags so we have the "original" value
var untagged = _.map(row.highlight[field], function (value) {
return value
.split(highlightTags.pre).join('')
.split(highlightTags.post).join('');
});
return _formatField(untagged, field);
}
// discover formats all of the values and puts them in $$_formatted for display
val = (row.$$_formatted || _formatRow(row))[field];
// undefined and null should just be an empty string
val = (val == null) ? '' : val;
return val;
}
/*
* Format a field with the index pattern on scope.
*/
function _formatField(value, name) {
var defaultFormat = courier.indexPatterns.fieldFormats.defaultByType.string;
var field = $scope.indexPattern.fields.byName[name];
var formatter = (field && field.format) ? field.format : defaultFormat;
return formatter.convert(value);
}
/*
* Create the $$_formatted key on a row
*/
function _formatRow(row) {
$scope.indexPattern.flattenHit(row);
row.$$_formatted = row.$$_formatted || _.mapValues(row.$$_flattened, _formatField);
return row.$$_formatted;
}
init();
}
};

View file

@ -1,4 +1,5 @@
define(function (require) {
var _ = require('lodash');
require('modules').get('kibana')
.run(function ($rootScope, docTitle) {
@ -27,7 +28,7 @@ define(function (require) {
parts.push(baseTitle);
}
return parts.filter(Boolean).join(' - ');
return _(parts).flatten().compact().join(' - ');
}
self.change = function (title, complete) {
@ -48,4 +49,4 @@ define(function (require) {
return function DoctitleProvider(docTitle) {
return docTitle;
};
});
});

View file

@ -49,12 +49,27 @@
tooltip-placement="top"
tooltip="Objects in arrays are not well supported."
class="fa fa-warning text-color-warning ng-scope doc-viewer-object-array"></i>
<div class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></div>
<div class="doc-viewer-value" ng-bind-html="typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>
</table>
<div id="json-ace" ng-show="mode == 'json'" readonly ui-ace="{ useWrapMode: true, advanced: { highlightActiveLine: false }, rendererOptions: { showPrintMargin: false, maxLines: 4294967296 }, mode: 'json' }" ng-model="hit_json"></div>
<div
id="json-ace"
ng-show="mode == 'json'"
ng-model="hitJson"
readonly
ui-ace="{
useWrapMode: true,
advanced: {
highlightActiveLine: false
},
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}"></div>
</div>
</div>

View file

@ -8,8 +8,6 @@ define(function (require) {
require('modules').get('kibana')
.directive('docViewer', function (config, Private) {
var formats = Private(require('components/index_patterns/_field_formats'));
return {
restrict: 'E',
template: html,
@ -21,21 +19,11 @@ define(function (require) {
},
link: function ($scope, $el, attr) {
// If a field isn't in the mapping, use this
var defaultFormat = formats.defaultByType.string;
$scope.mode = 'table';
$scope.mapping = $scope.indexPattern.fields.byName;
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
$scope.hit_json = angular.toJson($scope.hit, true);
$scope.formatted = _.mapValues($scope.flattened, function (value, name) {
var mapping = $scope.mapping[name];
var formatter = (mapping && mapping.format) ? mapping.format : defaultFormat;
if (_.isArray(value) && typeof value[0] === 'object') {
value = JSON.stringify(value, null, ' ');
}
return formatter.convert(value);
});
$scope.hitJson = angular.toJson($scope.hit, true);
$scope.formatted = $scope.indexPattern.formatHit($scope.hit);
$scope.fields = _.keys($scope.flattened).sort();
$scope.toggleColumn = function (fieldName) {

View file

@ -0,0 +1,26 @@
define(function (require) {
var _ = require('lodash');
var NL_RE = /\n/g;
var events = 'keydown keypress keyup change';
require('modules').get('kibana')
.directive('elasticTextarea', function () {
return {
restrict: 'A',
link: function ($scope, $el) {
function resize() {
$el.attr('rows', _.size($el.val().match(NL_RE)) + 1);
}
$el.on(events, resize);
$scope.$evalAsync(resize);
$scope.$on('$destroy', function () {
$el.off(events, resize);
});
}
};
});
});

View file

@ -0,0 +1,127 @@
<form ng-submit="editor.save()" name="form">
<div ng-if="editor.creating" class="form-group">
<label>Name</label>
<input
ng-model="editor.field.name"
required
placeholder="New Scripted Field"
input-focus
class="form-control">
</div>
<div ng-if="editor.creating && editor.indexPattern.fields.byName[editor.field.name]" class="hintbox">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Mapping Conflict:</strong>
You already have a field with the name {{ editor.field.name }}. Naming your scripted
field with the same name means you won't be able to query both fields at the same time.
</p>
</div>
<div class="form-group">
<label>Type</label>
<input
ng-model="editor.field.type"
readonly
class="form-control">
</div>
<div class="form-group">
<span class="pull-right text-warning hintbox-label" ng-click="editor.showFormatHelp = !editor.showFormatHelp">
<i class="fa fa-warning"></i> Warning
</span>
<label>Format <small>(Default: <i>{{editor.defFormatType.resolvedTitle}}</i>)</small></label>
<div class="hintbox" ng-if="editor.showFormatHelp">
<h4 class="hintbox-heading">
<i class="fa fa-warning text-warning"></i> Format Warning
</h4>
<p>
Formatting allows you to control the way that specific values are displayed. It can also cause values to be completely changed and prevent highlighting in Discover from working.
</p>
</div>
<select
ng-model="editor.selectedFormatId"
ng-options="format.id as format.title for format in editor.fieldFormatTypes"
class="form-control">
</select>
<fieldset
field-format-editor
ng-if="editor.selectedFormatId"
field="editor.field"
format-params="editor.formatParams">
</fieldset>
</div>
<div class="form-group">
<label for="editor.field.count">Popularity</label>
<div class="input-group">
<input
ng-model="editor.field.count"
type="number"
class="form-control">
<span class="input-group-btn">
<button
type="button"
ng-click="editor.field.count = editor.field.count + 1"
aria-label="Plus"
class="btn btn-default">
<i aria-hidden="true" class="fa fa-plus"></i>
</button>
<button
type="button"
ng-click="editor.field.count = editor.field.count - 1"
aria-label="Minus"
class="btn btn-default">
<i aria-hidden="true" class="fa fa-minus"></i>
</button>
</span>
</div>
</div>
<div ng-if="editor.field.scripted">
<div class="form-group">
<label>Script</label>
<textarea required class="form-control text-monospace" ng-model="editor.field.script"></textarea>
</div>
<div class="form-group">
<div ng-bind-html="editor.scriptingWarning" class="hintbox"></div>
</div>
<div class="form-group">
<div ng-bind-html="editor.scriptingInfo" class="hintbox"></div>
</div>
</div>
<div class="form-group">
<button
type="button"
ng-click="editor.cancel()"
aria-label="Cancel"
class="btn btn-primary">
Cancel
</button>
<button
type="button"
ng-if="editor.field.scripted && !editor.creating"
confirm-click="editor.delete()"
confirmation="Are you sure want to delete '{{ editor.field.name }}'? This action is irreversible!"
aria-label="Delete"
class="btn btn-danger">
Delete Field
</button>
<button
ng-disabled="form.$invalid"
type="submit"
aria-label="{{ editor.creating ? 'Create' : 'Update' }} Field"
class="btn btn-success">
{{ editor.creating ? 'Create' : 'Update' }} Field
</button>
</div>
</form>

View file

@ -0,0 +1,141 @@
define(function (require) {
require('components/field_format_editor/field_format_editor');
require('modules')
.get('kibana')
.directive('fieldEditor', function (Private, $sce) {
var _ = require('lodash');
var fieldFormats = Private(require('registry/field_formats'));
var Field = Private(require('components/index_patterns/_field'));
var scriptingInfo = $sce.trustAsHtml(require('text!components/field_editor/scripting_info.html'));
var scriptingWarning = $sce.trustAsHtml(require('text!components/field_editor/scripting_warning.html'));
return {
restrict: 'E',
template: require('text!components/field_editor/field_editor.html'),
scope: {
getIndexPattern: '&indexPattern',
getField: '&field'
},
controllerAs: 'editor',
controller: function ($scope, Notifier, kbnUrl) {
var self = this;
var notify = new Notifier({ location: 'Field Editor' });
self.scriptingInfo = scriptingInfo;
self.scriptingWarning = scriptingWarning;
self.indexPattern = $scope.getIndexPattern();
self.field = shadowCopy($scope.getField());
self.formatParams = self.field.format.params();
// only init on first create
self.creating = !self.indexPattern.fields.byName[self.field.name];
self.selectedFormatId = _.get(self.indexPattern, ['fieldFormatMap', self.field.name, 'type', 'id']);
self.defFormatType = initDefaultFormat();
self.fieldFormatTypes = [self.defFormatType].concat(fieldFormats.byFieldType[self.field.type] || []);
self.cancel = redirectAway;
self.save = function () {
var indexPattern = self.indexPattern;
var fields = indexPattern.fields;
var field = self.field.toActualField();
_.remove(fields, { name: field.name });
fields.push(field);
if (!self.selectedFormatId) {
delete indexPattern.fieldFormatMap[field.name];
} else {
indexPattern.fieldFormatMap[field.name] = self.field.format;
}
return indexPattern.save()
.then(function () {
notify.info('Saved Field "' + self.field.name + '"');
redirectAway();
});
};
self.delete = function () {
var indexPattern = self.indexPattern;
var field = self.field;
_.remove(indexPattern.fields, { name: field.name });
return indexPattern.save()
.then(function () {
notify.info('Deleted Field "' + field.name + '"');
redirectAway();
});
};
$scope.$watch('editor.selectedFormatId', function (cur, prev) {
var format = self.field.format;
var changedFormat = cur !== prev;
var missingFormat = cur && (!format || format.type.id !== cur);
if (!changedFormat || !missingFormat) return;
// reset to the defaults, but make sure it's an object
self.formatParams = _.assign({}, getFieldFormatType().paramDefaults);
});
$scope.$watch('editor.formatParams', function () {
var FieldFormat = getFieldFormatType();
self.field.format = new FieldFormat(self.formatParams);
}, true);
// copy the defined properties of the field to a plain object
// which is mutable, and capture the changed seperately.
function shadowCopy(field) {
var changes = {};
var shadowProps = {
toActualField: {
// bring the shadow copy out of the shadows
value: function toActualField() {
return new Field(self.indexPattern, _.defaults({}, changes, field.$$spec));
}
}
};
Object.getOwnPropertyNames(field).forEach(function (prop) {
var desc = Object.getOwnPropertyDescriptor(field, prop);
shadowProps[prop] = {
enumerable: desc.enumerable,
get: function () {
return _.has(changes, prop) ? changes[prop] : field[prop];
},
set: function (v) {
changes[prop] = v;
}
};
});
return Object.create(null, shadowProps);
}
function redirectAway() {
kbnUrl.changeToRoute(self.indexPattern, self.field.scripted ? 'scriptedFields' : 'indexedFields');
}
function getFieldFormatType() {
if (self.selectedFormatId) return fieldFormats.getType(self.selectedFormatId);
else return fieldFormats.getDefaultType(self.field.type);
}
function initDefaultFormat() {
var def = Object.create(fieldFormats.getDefaultType(self.field.type));
// explicitly set to undefined to prevent inheritting the prototypes id
def.id = undefined;
def.resolvedTitle = def.title;
def.title = '- default - ';
return def;
}
}
};
});
});

View file

@ -0,0 +1,32 @@
<h4>
<i class="fa fa-question-circle text-info"></i> Scripting Help
</h4>
<p>
By default, Elasticsearch scripts use <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions <i class="fa-link fa"></i></a>, which is a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations. We'll let you do some reading on <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions<i class="fa-link fa"></i></a> To access values in the document use the following format:
</p>
<p><code>doc['some_field'].value</code></p>
<p>
There are a few limitations when using Lucene Expressions:
</p>
<ul>
<li>Only numeric fields may be accessed</li>
<li> Stored fields are not available </li>
<li> If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0 </li>
</ul>
<p>
Here are all the operations available to scripted fields:
</p>
<ul>
<li> Arithmetic operators: + - * / % </li>
<li> Bitwise operators: | & ^ ~ << >> >>> </li>
<li> Boolean operators (including the ternary operator): && || ! ?: </li>
<li> Comparison operators: < <= == >= > </li>
<li> Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow </li>
<li> Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan </li>
<li> Distance functions: haversin </li>
<li> Miscellaneous functions: min, max </li>
</ul>

View file

@ -0,0 +1,11 @@
<h4>
<i class="fa fa-warning text-warning"></i> Proceed with caution
</h4>
<p>
Please familiarize yourself with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a> before using scripted fields.
</p>
<p>
Scripted fields can be used to display and aggregate calculated values. As such, they can be very slow, and if done incorrectly, can cause Kibana to be unusable. There's no safety net here. If you make a typo, unexpected exceptions will be thrown all over the place!
</p>

View file

@ -0,0 +1,122 @@
define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
require('modules')
.get('app/settings')
.directive('fieldFormatEditor', function (Private, $compile) {
return {
restrict: 'A',
scope: {
getField: '&field',
getFormatParams: '&formatParams'
},
controllerAs: 'editor',
controller: function ($scope) {
var self = this;
// bind the scope values to the controller, down with $scope.values
$scope.editor = this;
$scope.$bind('editor.field', 'getField()', $scope);
$scope.$bind('editor.formatParams', 'getFormatParams()', $scope);
/**
* Read the FieldFormat's editor property and convert it into
* a "pseudoDirective". For clarity I'm reusing the directive def
* object api, but for simplicity not implementing the entire thing.
*
* possible configs:
* string:
* - used as an angular template
* directive def object, with support for the following opts:
* - template
* - compile or link
* - scope (creates isolate, reads from parent scope, not attributes)
* - controller
* - controllerAs
*
* @param {angular.element} $el - template
* @param {object} directiveDef - the directive definition object
* @return {undefined}
*/
$scope.$watch('editor.field.format.type', function (FieldFormat) {
var opts = FieldFormat && FieldFormat.editor;
if (!opts) {
delete self.$$pseudoDirective;
return;
}
if (typeof opts === 'string') {
self.$$pseudoDirective = {
template: opts
};
return;
}
self.$$pseudoDirective = {
template: opts.template,
compile: opts.compile || function () {
return opts.link;
},
scope: opts.scope || false,
controller: opts.controller,
controllerAs: opts.controllerAs
};
});
},
link: function ($scope, $el) {
var scopesToTeardown = [];
function setupScope(opts) {
if (typeof opts !== 'object') {
return scopesToTeardown[scopesToTeardown.push($scope.$new()) - 1];
}
var isolate = scopesToTeardown[scopesToTeardown.push($scope.$new(true)) - 1];
_.forOwn(opts, function (from, to) {
isolate.$bind(to, from, $scope);
});
return isolate;
}
$scope.$watch('editor.$$pseudoDirective', function (directive) {
$el.empty();
_.invoke(scopesToTeardown.splice(0), '$destroy');
if (!directive) return $el.hide();
else $el.show();
var askedForChild = !!directive.scope;
var reuseScope = !askedForChild && !directive.controller;
var $formatEditor = $('<div>').html(directive.template);
var $formatEditorScope = reuseScope ? $scope : setupScope(directive.scope);
if (directive.controller) {
// bind the controller to the injected element
var cntrlAs = (directive.controllerAs ? ' as ' + directive.controllerAs : '');
$formatEditorScope.Controller = directive.controller;
$formatEditor.attr('ng-controller', 'Controller' + cntrlAs);
}
var attrs = {};
var linkFns = directive.compile && directive.compile($el, attrs);
if (!linkFns || _.isFunction(linkFns)) {
linkFns = {
pre: _.noop,
post: linkFns || _.noop
};
}
$el.html($formatEditor);
linkFns.pre($formatEditorScope, $formatEditor, attrs);
$compile($formatEditor)($formatEditorScope);
linkFns.post($formatEditorScope, $formatEditor, attrs);
});
}
};
});
});

View file

@ -0,0 +1,21 @@
<div class="form-group">
<small class="pull-right">
<a ng-href="https://adamwdraper.github.io/Numeral-js/" target="_blank">
Docs <i class="fa fa-link"></i>
</a>
</small>
<label>
Numeral.js format pattern
<small>
(Default: "{{ editor.field.format.type.paramDefaults.pattern }}")
</small>
</label>
</div>
<field-format-editor-pattern
ng-model="editor.formatParams.pattern"
placeholder="editor.field.format.type.paramDefaults.pattern"
inputs="cntrl.sampleInputs">
</field-format-editor-pattern>

View file

@ -0,0 +1,12 @@
define(function (require) {
require('components/field_format_editor/pattern/pattern');
require('modules')
.get('kibana')
.directive('fieldEditorNumeral', function () {
return {
restrict: 'E',
template: require('text!components/field_format_editor/numeral/numeral.html')
};
});
});

View file

@ -0,0 +1,11 @@
<div class="form-group">
<input
ng-model="model"
placeholder="{{ placeholder }}"
class="form-control">
</div>
<field-format-editor-samples
ng-model="model"
inputs="inputs">
</field-format-editor-samples>

View file

@ -0,0 +1,26 @@
define(function (require) {
require('components/field_format_editor/samples/samples');
require('modules')
.get('kibana')
.directive('fieldFormatEditorPattern', function () {
return {
restrict: 'E',
template: require('text!components/field_format_editor/pattern/pattern.html'),
require: ['ngModel', '^fieldEditor'],
scope: true,
link: function ($scope, $el, attrs, cntrls) {
var ngModelCntrl = cntrls[0];
$scope.$bind('inputs', attrs.inputs);
$scope.$bind('placeholder', attrs.placeholder);
// bind our local model with the outside ngModel
$scope.$watch('model', ngModelCntrl.$setViewValue);
ngModelCntrl.$render = function () {
$scope.model = ngModelCntrl.$viewValue;
};
}
};
});
});

View file

@ -0,0 +1,32 @@
<div class="form-group hintbox" ng-if="error">
<h4 class="hintbox-heading">
<i class="fa fa-danger text-danger"></i> Format error
</h4>
<p>
An error occured while trying to use this format configuration.
</p>
<pre>{{ error.message }}</pre>
</div>
<div class="form-group" ng-if="samples">
<hr>
<label>Samples</label>
<table class="table">
<thead>
<tr>
<th>
Input
</th>
<th>
Formatted
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="sample in samples">
<td ng-bind="sample[0]"></td>
<td ng-bind-html="sample[1]"></td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,45 @@
define(function (require) {
var _ = require('lodash');
require('modules')
.get('kibana')
.directive('fieldFormatEditorSamples', function ($sce, Promise) {
return {
restrict: 'E',
template: require('text!components/field_format_editor/samples/samples.html'),
require: ['?^ngModel', '^fieldEditor'],
scope: true,
link: function ($scope, $el, attrs, cntrls) {
var ngModelCntrl = cntrls[0];
$scope.samples = null;
$scope.$bind('inputs', attrs.inputs);
$scope.$watchMulti([
'editor.field.format',
'[]inputs'
], function () {
$scope.samples = null;
var field = $scope.editor.field;
if (!field || !field.format) {
return;
}
Promise.try(function () {
var converter = field.format.getConverterFor('html');
$scope.samples = _.map($scope.inputs, function (input) {
return [input, $sce.trustAsHtml(converter(input))];
});
})
.then(validity, validity);
});
function validity(err) {
$scope.error = err;
ngModelCntrl && ngModelCntrl.$setValidity('patternExecutes', !err);
}
}
};
});
});

View file

@ -1,5 +1,4 @@
define(function (require) {
var _ = require('lodash');
define(function () {
return function mapScriptProvider(Promise, courier) {
return function (filter) {
var key, value, field;

View file

@ -9,8 +9,6 @@ define(function (require) {
return function (formatted, highlight) {
if (typeof formatted === 'object') formatted = angular.toJson(formatted);
formatted = _.escape(formatted);
_.each(highlight, function (section) {
section = _.escape(section);
@ -31,4 +29,4 @@ define(function (require) {
return formatted;
};
});
});
});

View file

@ -0,0 +1,79 @@
define(function (require) {
return function FieldObjectProvider(Private, shortDotsFilter, $rootScope, Notifier) {
var notify = new Notifier({ location: 'IndexPattern Field' });
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var fieldFormats = Private(require('registry/field_formats'));
var ObjDefine = require('utils/obj_define');
function Field(indexPattern, spec) {
// unwrap old instances of Field
if (spec instanceof Field) spec = spec.$$spec;
// constuct this object using ObjDefine class, which
// extends the Field.prototype but gets it's properties
// defined using the logic below
var obj = new ObjDefine(spec, Field.prototype);
if (spec.name === '_source') {
spec.type = '_source';
}
// find the type for this field, fallback to unkown type
var type = fieldTypes.byName[spec.type];
if (spec.type && !type) {
notify.error(
'Unkown field type "' + spec.type + '"' +
' for field "' + spec.name + '"' +
' in indexPattern "' + indexPattern.id + '"'
);
}
if (!type) type = fieldTypes.byName.unknown;
var format = spec.format;
if (!format || !(format instanceof FieldFormat)) {
format = indexPattern.fieldFormatMap[spec.name] || fieldFormats.getDefaultInstance(spec.type);
}
var indexed = !!spec.indexed;
var scripted = !!spec.scripted;
var sortable = indexed && type.sortable;
var bucketable = indexed || scripted;
var filterable = spec.name === '_id' || scripted || (indexed && type.filterable);
obj.fact('name');
obj.fact('type');
obj.writ('count', spec.count || 0);
// scripted objs
obj.fact('scripted', scripted);
obj.writ('script', scripted ? spec.script : null);
obj.writ('lang', scripted ? (spec.lang || 'expression') : null);
// mapping info
obj.fact('indexed', indexed);
obj.fact('analyzed', !!spec.analyzed);
obj.fact('doc_values', !!spec.doc_values);
// usage flags, read-only and won't be saved
obj.comp('format', format);
obj.comp('sortable', sortable);
obj.comp('bucketable', bucketable);
obj.comp('filterable', filterable);
// computed values
obj.comp('indexPattern', indexPattern);
obj.comp('displayName', shortDotsFilter(spec.name));
obj.comp('$$spec', spec);
return obj.create();
}
Field.prototype.routes = {
edit: '/settings/indices/{{indexPattern.id}}/field/{{name}}'
};
return Field;
};
});

View file

@ -0,0 +1,99 @@
define(function (require) {
return function FieldFormatClassProvider(config, $rootScope, Private) {
var _ = require('lodash');
var contentTypes = Private(require('components/index_patterns/_field_format/contentTypes'));
function FieldFormat(params) {
var self = this;
// give the constructor a more appropriate name
self.type = self.constructor;
// keep the params and defaults seperate
self._params = params || {};
self._paramDefaults = self.type.paramDefaults || {};
// one content type, so assume text
if (_.isFunction(self._convert)) {
self._convert = { text: self._convert };
}
contentTypes.setup(self);
}
/**
* Convert a raw value to a formated string
* @param {any} value
* @param {string} [contentType=text] - optional content type, the only two contentTypes
* currently supported are "html" and "text", which helps
* formatters adjust to different contexts
* @return {string} - the formatted string, which is assumed to be html, safe for
* injecting into the DOM or a DOM attribute
*/
FieldFormat.prototype.convert = function (value, contentType) {
return this.getConverterFor(contentType)(value);
};
/**
* Get a convert function that is bound to a specific contentType
* @param {string} [contentType=text]
* @return {function} - a bound converter function
*/
FieldFormat.prototype.getConverterFor = function (contentType) {
return this._convert[contentType || 'text'];
};
/**
* Get the value of a param. This value may be a default value.
*
* @param {string} name - the param name to fetch
* @return {any}
*/
FieldFormat.prototype.param = function (name) {
var val = this._params[name];
if (val || val === false || val === 0) {
// truthy, false, or 0 are fine
// '', NaN, null, undefined, etc are not
return val;
}
return this._paramDefaults[name];
};
/**
* Get all of the params in a single object
* @return {object}
*/
FieldFormat.prototype.params = function () {
return _.cloneDeep(_.defaults({}, this._params, this._paramDefaults));
};
/**
* serialize this format to a simple POJO, with only the params
* that are not default
*
* @return {object}
*/
FieldFormat.prototype.toJSON = function () {
var type = this.type;
var defaults = this._paramDefaults;
var params = _.transform(this._params, function (uniqParams, val, param) {
if (val !== defaults[param]) {
uniqParams[param] = val;
}
}, {});
if (!_.size(params)) {
params = undefined;
}
return {
id: type.id,
params: params
};
};
return FieldFormat;
};
});

View file

@ -0,0 +1,66 @@
define(function (require) {
return function contentTypesProvider(highlightFilter) {
var _ = require('lodash');
var angular = require('angular');
var types = {
html: function (format, convert) {
return function recurse(value, field, hit) {
var type = typeof value;
if (type === 'object' && typeof value.map === 'function') {
if (value.$$_formattedField) return value.$$_formattedField;
var subVals = value.map(recurse);
var useMultiLine = subVals.some(function (sub) {
return sub.indexOf('\n') > -1;
});
return value.$$_formattedField = subVals.join(',' + (useMultiLine ? '\n' : ' '));
}
return convert.call(format, value, field, hit);
};
},
text: function (format, convert) {
return function recurse(value) {
if (value && typeof value.map === 'function') {
return angular.toJson(value.map(recurse), true);
}
return convert.call(format, value);
};
}
};
function fallbackText(value) {
return _.asPrettyString(value);
}
function fallbackHtml(value, field, hit) {
var formatted = _.escape(this.convert(value, 'text'));
if (!hit || !hit.highlight || !hit.highlight[field.name]) {
return formatted;
} else {
return highlightFilter(formatted, hit.highlight[field.name]);
}
}
function setup(format) {
var src = format._convert || {};
var converters = format._convert = {};
converters.text = types.text(format, src.text || fallbackText);
converters.html = types.html(format, src.html || fallbackHtml);
return format._convert;
}
return {
types: types,
setup: setup
};
};
});

View file

@ -1,175 +0,0 @@
/**
### Formatting a value
To format a response value, you need to get ahold of the field list, which is usually available at `indexPattern.fields`. Each field object has a `format` property*, which is an object detailed in [_field_formats.js](https://github.com/elastic/kibana4/blob/master/src/kibana/components/index_patterns/_field_formats.js).
Once you have the field that a response value came from, pass the value to `field.format.convert(value)` and a formatted string representation of the field will be returned.
\* the `format` property on field object's is a non-enumerable getter, meaning that if you itterate/clone/stringify the field object the format property will not be present.
### Changing a field's format
Currently only one field format exists, `"string"`, which just [flattens any value down to a string](https://github.com/elastic/kibana4/blob/master/src/kibana/components/index_patterns/_field_formats.js#L18-L24).
To change the format for a specific field you can either change the default for a field type modify the [default format mapping here](https://github.com/elastic/kibana4/blob/master/src/kibana/components/index_patterns/_field_formats.js#L37-L46).
To change the format for a specific indexPattern's field, add the field and format name to `indexPattern.customFormats` object property.
```js
$scope.onChangeFormat = function (field, format) {
indexPattern.customFormats[field.name] = format.name;
};
```
### Passing the formats to a chart
Currently, the [histogram formatter](https://github.com/elastic/kibana4/blob/master/src/plugins/visualize/saved_visualizations/resp_converters/histogram.js) passes the formatting function as the `xAxisFormatter` and `yAxisFormatter` function.
*/
define(function (require) {
return function FieldFormattingService($rootScope, config) {
var _ = require('lodash');
var angular = require('angular');
var moment = require('moment');
function stringConverter(val) {
return formatField(val, function (val) {
if (_.isObject(val)) {
return angular.toJson(val);
}
else if (val == null) {
return '';
}
else {
return '' + val;
}
});
}
var formats = [
{
types: [
'number',
'boolean',
'date',
'ip',
'attachment',
'geo_point',
'geo_shape',
'string',
'conflict'
],
name: 'string',
convert: stringConverter
},
{
types: [
'date'
],
name: 'date',
convert: function (val) {
return formatField(val, function (val) {
if (_.isNumber(val) || _.isDate(val)) {
return moment(val).format(config.get('dateFormat'));
} else {
return val;
}
});
}
},
{
types: [
'ip'
],
name: 'ip',
convert: function (val) {
return formatField(val, function (val) {
if (!isFinite(val)) return val;
return [val >>> 24, val >>> 16 & 0xFF, val >>> 8 & 0xFF, val & 0xFF].join('.');
});
}
},
{
types: [
'number'
],
name: 'kilobytes',
convert: function (val) {
return formatField(val, function (val) {
return (val / 1024).toFixed(config.get('format:numberPrecision')) + ' kb';
});
}
},
{
types: [
'number',
'murmur3'
],
name: 'number',
convert: function (val) {
return formatField(val, function (val) {
if (_.isNumber(val)) {
return +val.toFixed(config.get('format:numberPrecision'));
} else {
return stringConverter(val);
}
});
}
}
];
function formatField(value, fn) {
if (_.isArray(value)) {
if (value.length === 1) {
return fn(value[0]);
} else {
return angular.toJson(_.map(value, fn));
}
} else {
return fn(value);
}
}
formats.byType = _.transform(formats, function (byType, formatter) {
formatter.types.forEach(function (type) {
var list = byType[type] || (byType[type] = []);
list.push(formatter);
});
}, {});
formats.byName = _.indexBy(formats, 'name');
formats.defaultByType = {
number: formats.byName.number,
murmur3: formats.byName.number,
date: formats.byName.date,
boolean: formats.byName.string,
ip: formats.byName.ip,
attachment: formats.byName.string,
geo_point: formats.byName.string,
geo_shape: formats.byName.string,
string: formats.byName.string,
conflict: formats.byName.string
};
/**
* Wrap the dateFormat.convert function in memoize,
* as moment is a huge performance issue if not memoized.
*
* @return {void}
*/
function memoizeDateFormat() {
var format = formats.byName.date;
if (!format._origConvert) {
format._origConvert = format.convert;
}
format.convert = _.memoize(format._origConvert);
}
// memoize once config is ready, and every time the date format changes
$rootScope.$on('init:config', memoizeDateFormat);
$rootScope.$on('change:config.dateFormat', memoizeDateFormat);
return formats;
};
});

View file

@ -16,8 +16,10 @@ define(function (require) {
{ name: 'geo_point', sortable: false, filterable: false },
{ name: 'geo_shape', sortable: false, filterable: false },
{ name: 'attachment', sortable: false, filterable: false },
{ name: 'murmur3', sortable: false, filterable: false }
{ name: 'murmur3', sortable: false, filterable: false },
{ name: 'unknown', sortable: false, filterable: false },
{ name: '_source', sortable: false, filterable: false },
]
});
};
});
});

View file

@ -2,7 +2,6 @@
// returns a flattened version
define(function (require) {
return function FlattenHitProvider(config, $rootScope) {
var _ = require('lodash');
var metaFields = config.get('metaFields');
@ -49,13 +48,15 @@ define(function (require) {
return flat;
}
function cachedFlatten(indexPattern, hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
}
return function (indexPattern) {
function cachedFlatten(hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
}
cachedFlatten.uncached = flattenHit;
cachedFlatten.uncached = _.partial(flattenHit, indexPattern);
return cachedFlatten;
return cachedFlatten;
};
};
});

View file

@ -0,0 +1,48 @@
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a formated version
define(function (require) {
var _ = require('lodash');
return function (indexPattern, defaultFormat) {
function convert(hit, val, fieldName) {
var field = indexPattern.fields.byName[fieldName];
if (!field) return defaultFormat.convert(val, 'html');
return field.format.getConverterFor('html')(val, field, hit);
}
function formatHit(hit) {
if (hit.$$_formatted) return hit.$$_formatted;
// use and update the partial cache, but don't rewrite it. _source is stored in partials
// but not $$_formatted
var partials = hit.$$_partialFormatted || (hit.$$_partialFormatted = {});
var cache = hit.$$_formatted = {};
_.forOwn(indexPattern.flattenHit(hit), function (val, fieldName) {
// sync the formatted and partial cache
var formatted = partials[fieldName] == null ? convert(hit, val, fieldName) : partials[fieldName];
cache[fieldName] = partials[fieldName] = formatted;
});
return cache;
}
formatHit.formatField = function (hit, fieldName) {
var partials = hit.$$_partialFormatted;
if (partials && partials[fieldName] != null) {
return partials[fieldName];
}
if (!partials) {
partials = hit.$$_partialFormatted = {};
}
var val = fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName];
return partials[fieldName] = convert(hit, val, fieldName);
};
return formatHit;
};
});

View file

@ -9,7 +9,7 @@ define(function (require) {
fielddataFields = _.pluck(self.fields.byType.date, 'name');
_.each(self.getFields('scripted'), function (field) {
_.each(self.getScriptedFields(), function (field) {
scriptFields[field.name] = { script: field.script, lang: field.lang };
});

View file

@ -1,23 +1,22 @@
define(function (require) {
return function IndexPatternFactory(Private, timefilter, Notifier, config, Promise) {
return function IndexPatternFactory(Private, timefilter, Notifier, config, Promise, $rootScope) {
var _ = require('lodash');
var angular = require('angular');
var errors = require('errors');
var angular = require('angular');
var fieldformats = Private(require('registry/field_formats'));
var getIds = Private(require('components/index_patterns/_get_ids'));
var mapper = Private(require('components/index_patterns/_mapper'));
var fieldFormats = Private(require('components/index_patterns/_field_formats'));
var intervals = Private(require('components/index_patterns/_intervals'));
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var flattenHit = Private(require('components/index_patterns/_flatten_hit'));
var Field = Private(require('components/index_patterns/_field'));
var getComputedFields = require('components/index_patterns/_get_computed_fields');
var shortDotsFilter = Private(require('filters/short_dots'));
var DocSource = Private(require('components/courier/data_source/doc_source'));
var mappingSetup = Private(require('utils/mapping_setup'));
var IndexedArray = require('utils/indexed_array/index');
var flattenHit = Private(require('components/index_patterns/_flatten_hit'));
var formatHit = require('components/index_patterns/_format_hit');
var type = 'index-pattern';
var notify = new Notifier();
@ -26,17 +25,35 @@ define(function (require) {
title: 'string',
timeFieldName: 'string',
intervalName: 'string',
customFormats: 'json',
fields: 'json'
fields: 'json',
fieldFormatMap: {
type: 'string',
_serialize: function (map) {
if (map == null) return;
var count = 0;
var serialized = _.transform(map, function (flat, format, field) {
if (!format) return;
count++;
flat[field] = format;
});
if (count) return angular.toJson(serialized);
},
_deserialize: function (map) {
if (map == null) return {};
return _.mapValues(angular.fromJson(map), function (mapping) {
var FieldFormat = fieldformats.byId[mapping.id];
return FieldFormat && new FieldFormat(mapping.params);
});
}
}
});
function IndexPattern(id) {
var self = this;
// set defaults
self.id = id;
self.title = id;
self.customFormats = {};
setId(id);
var docSource = new DocSource();
@ -47,6 +64,11 @@ define(function (require) {
.type(type)
.id(self.id);
// listen for config changes and update field list
$rootScope.$on('change:config', function () {
initFields();
});
return mappingSetup.isDefined(type)
.then(function (defined) {
// create mapping for this type if one does not exist
@ -69,7 +91,7 @@ define(function (require) {
}
});
// Give obj all of the values in _source.fields
// Give obj all of the values in _source
_.assign(self, resp._source);
self._indexFields();
@ -84,49 +106,13 @@ define(function (require) {
});
};
function setIndexedValue(key, value) {
value = value || self[key];
self[key] = new IndexedArray({
function initFields(fields) {
fields = fields || self.fields || [];
self.fields = new IndexedArray({
index: ['name'],
group: ['type'],
initialSet: value.map(function (field) {
field.count = field.count || 0;
if (field.hasOwnProperty('format')) return field;
var type = fieldTypes.byName[field.type];
Object.defineProperties(field, {
bucketable: {
value: field.indexed || field.scripted
},
displayName: {
get: function () {
return shortDotsFilter(field.name);
}
},
filterable: {
value: field.name === '_id' || ((field.indexed && type && type.filterable) || field.scripted)
},
format: {
get: function () {
var formatName = self.customFormats && self.customFormats[field.name];
return formatName ? fieldFormats.byName[formatName] : fieldFormats.defaultByType[field.type];
}
},
sortable: {
value: field.indexed && type && type.sortable
},
scripted: {
// enumerable properties end up in the JSON
enumerable: true,
value: !!field.scripted
},
lang: {
enumerable: true,
value: field.scripted ? field.lang || 'expression' : undefined
}
});
return field;
initialSet: fields.map(function (field) {
return new Field(self, field);
})
});
}
@ -136,7 +122,7 @@ define(function (require) {
if (!self.fields) {
return self.refreshFields();
} else {
setIndexedValue('fields');
initFields();
}
}
};
@ -144,13 +130,13 @@ define(function (require) {
self.addScriptedField = function (name, script, type, lang) {
type = type || 'string';
var scriptFields = _.pluck(self.getFields('scripted'), 'name');
var scriptFields = _.pluck(self.getScriptedFields(), 'name');
if (_.contains(scriptFields, name)) {
throw new errors.DuplicateField(name);
}
var scriptedField = self.fields.push({
self.fields.push({
name: name,
script: script,
type: type,
@ -185,11 +171,12 @@ define(function (require) {
}
};
self.getFields = function (type) {
var getScripted = (type === 'scripted');
return _.where(self.fields, function (field) {
return field.scripted ? getScripted : !getScripted;
});
self.getNonScriptedFields = function () {
return _.where(self.fields, { scripted: false });
};
self.getScriptedFields = function () {
return _.where(self.fields, { scripted: true });
};
self.getInterval = function () {
@ -223,47 +210,44 @@ define(function (require) {
// clear the indexPattern list cache
getIds.clearCache();
return body;
};
// index the document
var finish = function (id) {
self.id = id;
return self.id;
};
function setId(id) {
return self.id = id;
}
self.create = function () {
var body = self.prepBody();
return docSource.doCreate(body)
.then(finish).catch(function (err) {
.then(setId)
.catch(function (err) {
var confirmMessage = 'Are you sure you want to overwrite this?';
if (_.deepGet(err, 'origError.status') === 409 && window.confirm(confirmMessage)) {
return docSource.doIndex(body).then(finish);
return docSource.doIndex(body).then(setId);
}
return Promise.resolve(false);
});
};
self.save = function () {
var body = self.prepBody();
return docSource.doIndex(body).then(finish);
return docSource.doIndex(body).then(setId);
};
self.refreshFields = function () {
return mapper.clearCache(self)
.then(function () {
return self._fetchFields()
.then(self.save);
});
.then(self._fetchFields)
.then(self.save);
};
self._fetchFields = function () {
return mapper.getFieldsForIndexPattern(self, true)
.then(function (fields) {
// append existing scripted fields
fields = fields.concat(self.getFields('scripted'));
setIndexedValue('fields', fields);
fields = fields.concat(self.getScriptedFields());
// initialize self.field with this field list
initFields(fields);
});
};
@ -275,10 +259,21 @@ define(function (require) {
return '' + self.toJSON();
};
self.flattenHit = _.partial(flattenHit, self);
self.metaFields = config.get('metaFields');
self.getComputedFields = getComputedFields.bind(self);
self.flattenHit = flattenHit(self);
self.formatHit = formatHit(self, fieldformats.getDefaultInstance('string'));
self.formatField = self.formatHit.formatField;
}
IndexPattern.prototype.routes = {
edit: '/settings/indices/{{id}}',
addField: '/settings/indices/{{id}}/create-field',
indexedFields: '/settings/indices/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/settings/indices/{{id}}?_a=(tab:scriptedFields)'
};
return IndexPattern;
};
});

View file

@ -21,6 +21,9 @@ define(function (require) {
_timestamp: {
indexed: true,
type: 'date'
},
_source: {
type: '_source'
}
};
@ -40,4 +43,4 @@ define(function (require) {
return mapping;
};
};
});
});

View file

@ -1,12 +1,12 @@
<dl class="source truncate-by-height">
<% _.each(highlight, function (value, field) { /* show fields that match the query first */ %>
<dt><%= shortDotsFilter(field) %>:</dt>
<dt><%- shortDotsFilter(field) %>:</dt>
<dd><%= source[field] %></dd>
<%= ' ' %>
<% }); %>
<% _.each(source, function (value, field) { %>
<% if (_.has(highlight, field)) return; %>
<dt><%= shortDotsFilter(field) %>:</dt>
<dt><%- shortDotsFilter(field) %>:</dt>
<dd><%= value %></dd>
<%= ' ' %>
<% }); %>

View file

@ -1,8 +1,6 @@
define(function (require) {
return function transformMappingIntoFields(Private, configFile, config) {
var _ = require('lodash');
var MappingConflict = require('errors').MappingConflict;
var castMappingType = Private(require('components/index_patterns/_cast_mapping_type'));
var mapField = Private(require('components/index_patterns/_map_field'));
@ -19,7 +17,7 @@ define(function (require) {
var fields = {};
_.each(response, function (index, indexName) {
if (indexName === configFile.kibana_index) return;
_.each(index.mappings, function (mappings, typeName) {
_.each(index.mappings, function (mappings) {
_.each(mappings, function (field, name) {
var keys = Object.keys(field.mapping);
if (keys.length === 0 || (name[0] === '_') && !_.contains(config.get('metaFields'), name)) return;

View file

@ -42,7 +42,7 @@ define(function (require) {
self.intervals = Private(require('components/index_patterns/_intervals'));
self.mapper = Private(require('components/index_patterns/_mapper'));
self.patternToWildcard = Private(require('components/index_patterns/_pattern_to_wildcard'));
self.fieldFormats = Private(require('components/index_patterns/_field_formats'));
self.fieldFormats = Private(require('registry/field_formats'));
self.IndexPattern = IndexPattern;
});
});

View file

@ -1,7 +1,7 @@
define(function (require) {
require('modules')
.get('kibana')
.directive('paginatedTable', function ($filter, config, Private) {
.directive('paginatedTable', function ($filter) {
var _ = require('lodash');
var orderBy = $filter('orderBy');
@ -63,17 +63,23 @@ define(function (require) {
};
// update the sordedRows result
$scope.$watch('rows', rowSorter);
$scope.$watchCollection('paginatedTable.sort', rowSorter);
function rowSorter() {
if (self.sort.direction == null) {
$scope.sortedRows = $scope.rows.slice(0);
$scope.$watchMulti([
'rows',
'columns',
'[]paginatedTable.sort'
], function resortRows() {
if (!$scope.rows || !$scope.columns) {
$scope.sortedRows = false;
return;
}
$scope.sortedRows = orderBy($scope.rows, self.sort.getter, self.sort.direction === 'desc');
}
var sort = self.sort;
if (sort.direction == null) {
$scope.sortedRows = $scope.rows.slice(0);
} else {
$scope.sortedRows = orderBy($scope.rows, sort.getter, sort.direction === 'desc');
}
});
}
};
});

View file

@ -0,0 +1,4 @@
{
"extends": "../../.jshintrc",
"unused": true
}

View file

@ -0,0 +1 @@
<field-format-editor-numeral></field-format-editor-numeral>

View file

@ -0,0 +1,20 @@
<div class="form-group">
<small class="pull-right">
<a ng-href="http://momentjs.com/" target="_blank">
Docs <i class="fa fa-link"></i>
</a>
</small>
<label>
moment.js format pattern
<small>
(Default: "{{ editor.field.format.type.paramDefaults.pattern }}")
</small>
</label>
<field-format-editor-pattern
ng-model="editor.formatParams.pattern"
inputs="cntrl.sampleInputs"
></field-format-editor-pattern>
</div>

View file

@ -0,0 +1,10 @@
<div class="form-group">
<label>Transform</label>
<select
ng-model="editor.formatParams.transform"
ng-options="opt.id as opt.name for opt in editor.field.format.type.transformOpts"
class="form-control">
</select>
</div>
<field-format-editor-samples inputs="editor.field.format.type.sampleInputs"></field-format-editor-samples>

View file

@ -0,0 +1,40 @@
<div class="form-group">
<label>Type</label>
<select
ng-model="editor.formatParams.type"
ng-options="type.id as type.name for type in editor.field.format.type.urlTypes"
class="form-control">
</select>
</div>
<div class="form-group">
<span class="pull-right text-info hintbox-label" ng-click="editor.showUrlTemplateHelp = !editor.showUrlTemplateHelp">
<i class="fa fa-info"></i> Url Template Help
</span>
<label>Template</label>
<text ng-model="editor.formatParams.format">
<div class="hintbox" ng-if="editor.showUrlTemplateHelp">
<h4 class="hintbox-heading">
<i class="fa fa-question-circle text-info"></i> Url Template Help
</h4>
<p>
If a field only contains part of a url then a "Url Template" can be used to format the value as a complete url. The format is a string which uses double curly brace notation <code ng-bind="'\{\{ \}\}'"></code> to inject values. The following values can be accessed:
</p>
<ul>
<li>
<strong>value</strong> &mdash; The uri-escaped value
</li>
<li>
<strong>rawValue</strong> &mdash; The unescaped value
</li>
</ul>
</div>
<field-format-editor-pattern
ng-model="editor.formatParams.template"
inputs="url.sampleInputs">
</field-format-editor-pattern>
</div>

View file

@ -0,0 +1,11 @@
define(function (require) {
var fieldFormats = require('registry/field_formats');
fieldFormats.register(require('components/stringify/types/Url'));
fieldFormats.register(require('components/stringify/types/Bytes'));
fieldFormats.register(require('components/stringify/types/Date'));
fieldFormats.register(require('components/stringify/types/Ip'));
fieldFormats.register(require('components/stringify/types/Number'));
fieldFormats.register(require('components/stringify/types/Percent'));
fieldFormats.register(require('components/stringify/types/String'));
fieldFormats.register(require('components/stringify/types/Source'));
});

View file

@ -0,0 +1,10 @@
define(function (require) {
return function BytesFormatProvider(Private) {
var Numeral = Private(require('components/stringify/types/_Numeral'));
return Numeral.factory({
id: 'bytes',
title: 'Bytes',
sampleInputs: [1024, 5150000, 1990000000]
});
};
});

View file

@ -0,0 +1,56 @@
define(function (require) {
return function DateTimeFormatProvider(Private) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
var BoundToConfigObj = Private(require('components/bound_to_config_obj'));
var moment = require('moment');
require('components/field_format_editor/pattern/pattern');
_(DateTime).inherits(FieldFormat);
function DateTime(params) {
DateTime.Super.call(this, params);
}
DateTime.id = 'date';
DateTime.title = 'Date';
DateTime.fieldType = 'date';
DateTime.paramDefaults = new BoundToConfigObj({
pattern: '=dateFormat'
});
DateTime.editor = {
template: require('text!components/stringify/editors/date.html'),
controllerAs: 'cntrl',
controller: function ($interval, $scope) {
var self = this;
self.sampleInputs = [
Date.now(),
+moment().startOf('year'),
+moment().endOf('year')
];
$scope.$on('$destroy', $interval(function () {
self.sampleInputs[0] = Date.now();
}, 1000));
}
};
DateTime.prototype._convert = function (val) {
// don't give away our ref to converter so
// we can hot-swap when config changes
var pattern = this.param('pattern');
if (this._memoizedPattern !== pattern) {
this._memoizedPattern = pattern;
this._memoizedConverter = _.memoize(function converter(val) {
return moment(val).format(pattern);
});
}
return this._memoizedConverter(val);
};
return DateTime;
};
});

View file

@ -0,0 +1,24 @@
define(function (require) {
return function IpFormatProvider(Private) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
_(Ip).inherits(FieldFormat);
function Ip(params) {
Ip.Super.call(this, params);
}
Ip.id = 'ip';
Ip.title = 'IP Address';
Ip.fieldType = 'ip';
Ip.prototype._convert = function (val) {
if (!isFinite(val)) return val;
// shazzam!
return [val >>> 24, val >>> 16 & 0xFF, val >>> 8 & 0xFF, val & 0xFF].join('.');
};
return Ip;
};
});

View file

@ -0,0 +1,12 @@
define(function (require) {
return function NumberFormatProvider(Private) {
var Numeral = Private(require('components/stringify/types/_Numeral'));
return Numeral.factory({
id: 'number',
title: 'Number',
sampleInputs: [
10000, 12.345678, -1, -999, 0.52
]
});
};
});

View file

@ -0,0 +1,25 @@
define(function (require) {
return function NumberFormatProvider(Private) {
var _ = require('lodash');
var BoundToConfigObj = Private(require('components/bound_to_config_obj'));
var Numeral = Private(require('components/stringify/types/_Numeral'));
return Numeral.factory({
id: 'percent',
title: 'Percentage',
editorTemplate: require('text!components/stringify/editors/_numeral.html'),
paramDefaults: new BoundToConfigObj({
pattern: '=format:percent:defaultPattern',
fractional: true
}),
sampleInputs: [
0.10, 0.99999, 1, 100, 1000
],
prototype: {
_convert: _.compose(Numeral.prototype._convert, function (val) {
return this.param('fractional') ? val : val / 100;
})
}
});
};
});

View file

@ -0,0 +1,41 @@
define(function (require) {
return function _SourceProvider(Private, shortDotsFilter) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
var noWhiteSpace = require('utils/no_white_space');
var template = _.template(noWhiteSpace(require('text!components/stringify/types/_source.html')));
var angular = require('angular');
_(Source).inherits(FieldFormat);
function Source(params) {
Source.Super.call(this, params);
}
Source.id = '_source';
Source.title = '_source';
Source.fieldType = '_source';
Source.prototype._convert = {
text: angular.toJson,
html: function sourceToHtml(source, field, hit) {
if (!field) return this.getConverter('text')(source, field, hit);
var highlights = (hit && hit.highlight) || {};
var formatted = field.indexPattern.formatHit(hit);
var highlightPairs = [];
var sourcePairs = [];
_.keys(formatted).forEach(function (key) {
var pairs = highlights[key] ? highlightPairs : sourcePairs;
var field = shortDotsFilter(key);
var val = formatted[key];
pairs.push([field, val]);
}, []);
return template({ defPairs: highlightPairs.concat(sourcePairs) });
}
};
return Source;
};
});

View file

@ -0,0 +1,59 @@
define(function (require) {
return function _StringProvider(Private) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
require('components/field_format_editor/samples/samples');
_(_String).inherits(FieldFormat);
function _String(params) {
_String.Super.call(this, params);
}
_String.id = 'string';
_String.title = 'String';
_String.fieldType = [
'number',
'boolean',
'date',
'ip',
'attachment',
'geo_point',
'geo_shape',
'string',
'murmur3',
'unknown',
'conflict'
];
_String.paramDefaults = {
transform: false
};
_String.editor = require('text!components/stringify/editors/string.html');
_String.transformOpts = [
{ id: false, name: '- none -' },
{ id: 'lower', name: 'Lower Case' },
{ id: 'upper', name: 'Upper Case' },
{ id: 'short', name: 'Short Dots' }
];
_String.sampleInputs = [
'A Quick Brown Fox.',
'com.organizations.project.ClassName',
'hostname.net'
];
_String.prototype._convert = function (val) {
switch (this.param('transform')) {
case 'lower': return String(val).toLowerCase();
case 'upper': return String(val).toUpperCase();
case 'short': return _.shortenDottedString(val);
default: return _.asPrettyString(val);
}
};
return _String;
};
});

View file

@ -0,0 +1,101 @@
define(function (require) {
return function UrlFormatProvider(Private, highlightFilter) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
require('components/field_format_editor/pattern/pattern');
_(Url).inherits(FieldFormat);
function Url(params) {
Url.Super.call(this, params);
this._compileTemplate = _.memoize(this._compileTemplate);
}
Url.id = 'url';
Url.title = 'Url';
Url.fieldType = [
'number',
'boolean',
'date',
'ip',
'string',
'murmur3',
'unknown',
'conflict'
];
Url.editor = {
template: require('text!components/stringify/editors/url.html'),
controllerAs: 'url',
controller: function () {
this.sampleInputs = [ 'john', '/some/pathname/asset.png', 1234 ];
}
};
Url.templateMatchRE = /{{([\s\S]+?)}}/g;
Url.paramDefaults = {
type: 'a',
template: null
};
Url.urlTypes = [
{ id: 'a', name: 'Link' },
{ id: 'img', name: 'Image' }
];
Url.prototype._convert = {
text: function (value) {
var template = this.param('template');
return !template ? value : this._compileTemplate(template)(value);
},
html: function (rawValue, field, hit) {
var url = _.escape(this.convert(rawValue, 'text'));
var value = _.escape(rawValue);
switch (this.param('type')) {
case 'img': return '<img src="' + url + '" alt="' + value + '" title="' + value + '">';
default:
var urlDisplay = url;
if (hit && hit.highlight && hit.highlight[field.name]) {
urlDisplay = highlightFilter(url, hit.highlight[field.name]);
}
return '<a href="' + url + '" target="_blank">' + urlDisplay + '</a>';
}
}
};
Url.prototype._compileTemplate = function (template) {
var parts = template.split(Url.templateMatchRE).map(function (part, i) {
// trim all the odd bits, the variable names
return (i % 2) ? part.trim() : part;
});
return function (val) {
var locals = {
value: encodeURIComponent(val),
rawValue: val
};
// replace all the odd bits with their local var
var output = '';
var i = -1;
while (++i < parts.length) {
if (i % 2) {
if (locals.hasOwnProperty(parts[i])) {
var local = locals[parts[i]];
output += local == null ? '' : local;
}
} else {
output += parts[i];
}
}
return output;
};
};
return Url;
};
});

View file

@ -0,0 +1,56 @@
define(function (require) {
return function AbstractNumeralFormatProvider(Private) {
var _ = require('lodash');
var FieldFormat = Private(require('components/index_patterns/_field_format/FieldFormat'));
var BoundToConfigObj = Private(require('components/bound_to_config_obj'));
var numeral = require('numeral')();
require('components/field_format_editor/numeral/numeral');
_(Numeral).inherits(FieldFormat);
function Numeral(params) {
Numeral.Super.call(this, params);
}
Numeral.prototype._convert = function (val) {
if (typeof val !== 'number') {
val = parseFloat(val);
}
if (isNaN(val)) return '';
return numeral.set(val).format(this.param('pattern'));
};
Numeral.factory = function (opts) {
_(Class).inherits(Numeral);
function Class(params) {
Class.Super.call(this, params);
}
Class.id = opts.id;
Class.title = opts.title;
Class.fieldType = 'number';
Class.paramDefaults = opts.paramDefaults || new BoundToConfigObj({
pattern: '=format:' + opts.id + ':defaultPattern',
});
Class.editor = {
template: opts.editorTemplate || require('text!components/field_format_editor/numeral/numeral.html'),
controllerAs: 'cntrl',
controller: opts.controller || function () {
this.sampleInputs = opts.sampleInputs;
}
};
if (opts.prototype) {
_.assign(Class.prototype, opts.prototype);
}
return Class;
};
return Numeral;
};
});

View file

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

View file

@ -1,86 +1,74 @@
define(function (require) {
var _ = require('lodash');
require('filters/uriescape');
require('filters/rison');
var _ = require('lodash');
var rison = require('utils/rison');
var location = require('modules').get('kibana/url');
location.service('kbnUrl', function ($route, $location, $rootScope, globalState, $parse, getAppState) {
require('modules').get('kibana/url')
.service('kbnUrl', function (Private) { return Private(KbnUrlProvider); });
function KbnUrlProvider($route, $location, $rootScope, globalState, $parse, getAppState) {
var self = this;
var reloading;
var unbindListener;
/**
* Navigate to a url
*
* @param {String} url - the new url, can be a template. See #eval
* @param {Object} [paramObj] - optional set of parameters for the url template
* @return {undefined}
*/
self.change = function (url, paramObj) {
self._changeLocation('url', url, paramObj);
};
/**
* Same as #change except only changes the url's path,
* leaving the search string and such intact
*
* @param {String} path - the new path, can be a template. See #eval
* @param {Object} [paramObj] - optional set of parameters for the path template
* @return {undefined}
*/
self.changePath = function (path, paramObj) {
self._changeLocation('path', path, paramObj);
};
/**
* Same as #change except that it removes the current url from history
*
* @param {String} url - the new url, can be a template. See #eval
* @param {Object} [paramObj] - optional set of parameters for the url template
* @return {undefined}
*/
self.redirect = function (url, paramObj) {
self._changeLocation('url', url, paramObj, true);
};
/**
* Same as #redirect except only changes the url's path,
* leaving the search string and such intact
*
* @param {String} path - the new path, can be a template. See #eval
* @param {Object} [paramObj] - optional set of parameters for the path template
* @return {undefined}
*/
self.redirectPath = function (path, paramObj) {
self._changeLocation('path', path, paramObj, true);
};
self._changeLocation = function (type, url, paramObj, replace) {
var prev = {
path: $location.path(),
search: $location.search()
};
url = self.eval(url, paramObj);
$location[type](url);
if (replace) $location.replace();
var next = {
path: $location.path(),
search: $location.search()
};
if (self.shouldAutoReload(next, prev)) {
var appState = getAppState();
if (appState) appState.destroy();
reloading = $rootScope.$on('$locationChangeSuccess', function () {
// call the "unlisten" function returned by $on
reloading();
reloading = false;
$route.reload();
});
}
};
self.eval = function (url, paramObj) {
/**
* Evaluate a url template. templates can contain double-curly wrapped
* expressions that are evaluated in the context of the paramObj
*
* @param {String} template - the url template to evaluate
* @param {Object} [paramObj] - the variables to expose to the template
* @return {String} - the evaluated result
* @throws {Error} If any of the expressions can't be parsed.
*/
self.eval = function (template, paramObj) {
paramObj = paramObj || {};
return parseUrlPrams(url, paramObj);
};
self.getRoute = function () {
return $route.current && $route.current.$$route;
};
self.shouldAutoReload = function (next, prev) {
if (reloading) return false;
var route = self.getRoute();
if (!route) return false;
if (next.path !== prev.path) return false;
var reloadOnSearch = route.reloadOnSearch;
var searchSame = _.isEqual(next.search, prev.search);
return (reloadOnSearch && searchSame) || !reloadOnSearch;
};
function parseUrlPrams(url, paramObj) {
return url.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) {
return template.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) {
// remove filters
var key = expr.split('|')[0].trim();
@ -99,7 +87,104 @@ define(function (require) {
return $parse(expr)(paramObj);
});
}
};
});
/**
* convert an object's route to an href, compatible with
* window.location.href= and <a href="">
*
* @param {Object} obj - any object that list's it's routes at obj.routes{}
* @param {string} route - the route name
* @return {string} - the computed href
*/
self.getRouteHref = function (obj, route) {
return '#' + self.getRouteUrl(obj, route);
};
/**
* convert an object's route to a url, compatible with url.change() or $location.url()
*
* @param {Object} obj - any object that list's it's routes at obj.routes{}
* @param {string} route - the route name
* @return {string} - the computed url
*/
self.getRouteUrl = function (obj, route) {
var template = obj && obj.routes && obj.routes[route];
if (template) return self.eval(template, obj);
};
/**
* Similar to getRouteUrl, supports objects which list their routes,
* and redirects to the named route. See #redirect
*
* @param {Object} obj - any object that list's it's routes at obj.routes{}
* @param {string} route - the route name
* @return {undefined}
*/
self.redirectToRoute = function (obj, route) {
self.redirect(self.getRouteUrl(obj, route));
};
/**
* Similar to getRouteUrl, supports objects which list their routes,
* and changes the url to the named route. See #change
*
* @param {Object} obj - any object that list's it's routes at obj.routes{}
* @param {string} route - the route name
* @return {undefined}
*/
self.changeToRoute = function (obj, route) {
self.change(self.getRouteUrl(obj, route));
};
/////
// private api
/////
var reloading;
self._changeLocation = function (type, url, paramObj, replace) {
var prev = {
path: $location.path(),
search: $location.search()
};
url = self.eval(url, paramObj);
$location[type](url);
if (replace) $location.replace();
var next = {
path: $location.path(),
search: $location.search()
};
if (self._shouldAutoReload(next, prev)) {
var appState = getAppState();
if (appState) appState.destroy();
reloading = $rootScope.$on('$locationChangeSuccess', function () {
// call the "unlisten" function returned by $on
reloading();
reloading = false;
$route.reload();
});
}
};
self._shouldAutoReload = function (next, prev) {
if (reloading) return false;
var route = $route.current && $route.current.$$route;
if (!route) return false;
if (next.path !== prev.path) return false;
var reloadOnSearch = route.reloadOnSearch;
var searchSame = _.isEqual(next.search, prev.search);
return (reloadOnSearch && searchSame) || !reloadOnSearch;
};
}
return KbnUrlProvider;
});

View file

@ -1,7 +1,7 @@
define(function (require) {
return function AggConfigFactory(Private, fieldTypeFilter) {
var _ = require('lodash');
var fieldFormats = Private(require('components/index_patterns/_field_formats'));
var fieldFormats = Private(require('registry/field_formats'));
function AggConfig(vis, opts) {
var self = this;
@ -267,16 +267,16 @@ define(function (require) {
return this.params.field;
};
AggConfig.prototype.fieldFormatter = function () {
AggConfig.prototype.fieldFormatter = function (contentType) {
var field = this.field();
var format = field && field.format;
var strFormat = fieldFormats.defaultByType.string;
var strFormat = fieldFormats.getDefaultInstance('string');
if (this.type.getFormat) {
if (this.type && this.type.getFormat) {
format = this.type.getFormat(this) || format;
}
return (format || strFormat).convert;
return (format || strFormat).getConverterFor(contentType);
};
AggConfig.prototype.fieldName = function () {

View file

@ -1,4 +1,4 @@
define(function (require) {
define(function () {
function AggConfigResult(aggConfig, parent, value, key) {
this.$parent = parent;
this.key = key;
@ -32,8 +32,8 @@ define(function (require) {
return this.aggConfig.createFilter(this.key);
};
AggConfigResult.prototype.toString = function () {
return this.aggConfig.fieldFormatter()(this.value);
AggConfigResult.prototype.toString = function (contentType) {
return this.aggConfig.fieldFormatter(contentType)(this.value);
};
AggConfigResult.prototype.valueOf = function () {

View file

@ -44,7 +44,7 @@ define(function (require) {
yMin : data.getYMin(),
yMax : data.getYMax(),
_attr: vis._attr,
tickFormat: data.get('yAxisFormatter')
yAxisFormatter: data.get('yAxisFormatter')
})
});
};

View file

@ -12,4 +12,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -23,31 +23,38 @@ define(function (require) {
$cell.scope = $scope.$new();
$cell.addClass('cell-hover');
$cell.attr('ng-click', 'clickHandler()');
$cell.scope.clickHandler = function (negate) {
$cell.scope.clickHandler = function () {
clickHandler({ point: { aggConfigResult: aggConfigResult } });
};
return $compile($cell)($cell.scope);
};
if (contents instanceof AggConfigResult) {
if (contents.type === 'bucket' && contents.aggConfig.field() && contents.aggConfig.field().filterable) {
$cell = createAggConfigResultCell(contents);
}
contents = contents.toString();
contents = contents.toString('html');
}
if (_.isObject(contents)) {
if (contents.attr) {
$cell.attr(contents.attr);
}
if (contents.class) {
$cell.addClass(contents.class);
}
if (contents.scope) {
$cell.html($compile(contents.markup)(contents.scope));
$cell = $compile($cell.html(contents.markup))(contents.scope);
} else {
$cell.html($(contents.markup));
$cell.html(contents.markup);
}
} else {
if (contents === '') {
$cell.html('&nbsp;');
} else {
$cell.text(contents);
$cell.html(contents);
}
}

View file

@ -10,12 +10,19 @@ define(function (require) {
return Private(shortDotsFilterProvider);
});
function shortDotsFilterProvider(config) {
function shortDotsFilterProvider(config, $rootScope) {
var filter;
function updateFilter() {
filter = config.get('shortDots:enable') ? _.shortenDottedString : _.identity;
}
updateFilter();
$rootScope.$on('change:config.shortDots:enable', updateFilter);
$rootScope.$on('init:config', updateFilter);
return function (str) {
if (!_.isString(str) || config.get('shortDots:enable') !== true) {
return str;
}
return str.replace(/(.+?\.)/g, function (v) { return v[0] + '.'; });
return filter(str);
};
}

View file

@ -348,38 +348,37 @@ define(function (require) {
}
var rows = $scope.rows;
var counts = rows.fieldCounts;
var counts = rows.fieldCounts || (rows.fieldCounts = {});
var indexPattern = $scope.searchSource.get('index');
// merge the rows and the hits, use a new array to help watchers
rows = $scope.rows = rows.concat(resp.hits.hits);
rows.fieldCounts = counts;
if (sortFn) {
rows.sort(sortFn);
rows = $scope.rows = rows.slice(0, totalSize);
counts = rows.fieldCounts = {};
notify.event('resort rows', function () {
rows.sort(sortFn);
rows = $scope.rows = rows.slice(0, totalSize);
counts = rows.fieldCounts = {};
});
}
$scope.rows.forEach(function (hit) {
// skip this work if we have already done it and we are NOT sorting.
// ---
// when we are sorting results, we need to redo the counts each time because the
// "top 500" may change with each response
if (hit.$$_formatted && !sortFn) return;
notify.event('flatten hit and count fields', function () {
$scope.rows.forEach(function (hit) {
// skip this work if we have already done it and we are NOT sorting.
// ---
// when we are sorting results, we need to redo the counts each time because the
// "top 500" may change with each response
if (hit.$$_counted && !sortFn) return;
hit.$$_counted = true;
var formatAndCount = function (value, name) {
// add up the counts for each field name
counts[name] = counts[name] ? counts[name] + 1 : 1;
var defaultFormat = courier.indexPatterns.fieldFormats.defaultByType.string;
var field = $scope.indexPattern.fields.byName[name];
var formatter = (field && field.format) ? field.format : defaultFormat;
return formatter.convert(value);
};
var flatHit = $scope.indexPattern.flattenHit(hit);
hit.$$_formatted = _.mapValues(flatHit, formatAndCount);
var fields = _.keys(indexPattern.flattenHit(hit));
var n = fields.length;
var field;
while (field = fields[--n]) {
if (counts[field]) counts[field] += 1;
else counts[field] = 1;
}
});
});
}));

View file

@ -17,6 +17,7 @@ define(function (require) {
require('components/bind');
require('components/listen');
require('components/fancy_forms/fancy_forms');
require('components/stringify/register');
require('directives/click_focus');
require('directives/info');
require('directives/spinner');

View file

@ -5,7 +5,3 @@
padding: 1em;
width: 100%;
}
.markdown-vis-options textarea {
font-family: monospace;
}

View file

@ -31,6 +31,7 @@
class="form-control"
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
elastic-textarea
validate-json
></textarea>
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>

View file

@ -1,5 +1,6 @@
define(function (require) {
var _ = require('lodash');
require('components/elastic_textarea');
require('modules').get('apps/settings')
.directive('advancedRow', function (config, Notifier, Private) {
@ -63,6 +64,7 @@ define(function (require) {
return config.clear(conf.name);
});
};
}
};
});

View file

@ -2,38 +2,12 @@
<kbn-settings-indices>
<div ng-controller="settingsIndicesEdit" bindonce>
<div class="page-header">
<div class="index-pattern-name">
<h1 class="title" css-truncate>
<i aria-hidden="true" ng-if="defaultIndex === indexPattern.id" class="fa fa-star"></i>
{{indexPattern.id}}
</h1>
<div class="controls">
<button
ng-click="setDefaultPattern()"
tooltip="Set as default index"
class="btn btn-success">
<span class="sr-only">Set as default index</span>
<i aria-hidden="true" class="fa fa-star"></i>
</button>
<button
confirm-click="indexPattern.refreshFields()"
confirmation="This will reset the field popularity counters. Are you sure you want to reload your fields?"
tooltip="Reload field list"
class="btn btn-warning">
<span class="sr-only">Reload field list</span>
<i aria-hidden="true" class="fa fa-refresh"></i>
</button>
<button
aria-label="Remove index pattern"
confirm-click="removePattern()"
confirmation="Are you sure you want to remove this index pattern?"
tooltip="Remove index pattern"
class="btn btn-danger">
<span class="sr-only">Remove index pattern</span>
<i aria-hidden="true" class="fa fa-trash"></i>
</button>
</div>
</div>
<kbn-settings-index-header
index-pattern="indexPattern"
set-default="setDefaultPattern()"
refresh-fields="indexPattern.refreshFields()"
delete="removePattern()">
</kbn-settings-index-header>
<p>
This page lists every field in the <strong>{{indexPattern.id}}</strong>
@ -62,9 +36,8 @@
</li>
</ul>
<indexed-fields ng-show="state.tab == 'indexedFields'" class="fields"></indexed-fields>
<scripted-fields ng-show="state.tab == 'scriptedFields'" class="scripted-fields"></scripted-fields>
<indexed-fields ng-show="state.tab == 'indexedFields'" class="fields indexed-fields"></indexed-fields>
<scripted-fields ng-show="state.tab == 'scriptedFields'" class="fields scripted-fields"></scripted-fields>
</div>
</kbn-settings-indices>

View file

@ -2,26 +2,29 @@ define(function (require) {
var _ = require('lodash');
require('plugins/settings/sections/indices/_indexed_fields');
require('plugins/settings/sections/indices/_scripted_fields');
require('plugins/settings/sections/indices/_index_header');
require('routes')
.when('/settings/indices/:id', {
.when('/settings/indices/:indexPatternId', {
template: require('text!plugins/settings/sections/indices/_edit.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.id)
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
}
});
require('modules').get('apps/settings')
.controller('settingsIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState) {
.controller('settingsIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState, docTitle) {
var notify = new Notifier();
var $state = $scope.state = new AppState();
var refreshKibanaIndex = Private(require('plugins/settings/sections/indices/_refresh_kibana_index'));
$scope.kbnUrl = Private(require('components/url/url'));
$scope.indexPattern = $route.current.locals.indexPattern;
docTitle.change($scope.indexPattern.id);
var otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id);
var fieldTypes = Private(require('plugins/settings/sections/indices/_field_types'));

View file

@ -1,10 +1,11 @@
<div class="actions">
<button ng-click="edit(field)" aria-label="Edit" class="btn btn-xs btn-default">
<a ng-href="{{ kbnUrl.getRouteHref(field, 'edit') }}" aria-label="Edit" class="btn btn-xs btn-default">
<span class="sr-only">Edit</span>
<i aria-hidden="true" class="fa fa-pencil"></i>
</button>
</a>
<button
ng-if="field.scripted"
confirm-click="remove(field)"
confirmation="Are you sure want to delete '{{field.name}}'? This action is irreversible!"
class="btn btn-xs btn-danger"

View file

@ -0,0 +1,18 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div class="page-header">
<kbn-settings-index-header index-pattern="fieldSettings.indexPattern"></kbn-settings-index-header>
<h2 ng-if="fieldSettings.mode === 'create'">
Create {{ fieldSettings.field.scripted ? 'Scripted ' : '' }}Field
</h2>
<h2 ng-if="fieldSettings.mode === 'edit'">
{{ fieldSettings.field.name }}
</h2>
</div>
<field-editor index-pattern="fieldSettings.indexPattern" field="fieldSettings.field"></field-editor>
</kbn-settings-indices>
</kbn-settings-app>

View file

@ -0,0 +1,55 @@
define(function (require) {
require('components/field_editor/field_editor');
require('plugins/settings/sections/indices/_index_header');
require('routes')
.when('/settings/indices/:indexPatternId/field/:fieldName', { mode: 'edit' })
.when('/settings/indices/:indexPatternId/create-field/', { mode: 'create' })
.defaults(/settings\/indices\/[^\/]+\/(field|create-field)(\/|$)/, {
template: require('text!plugins/settings/sections/indices/_field_editor.html'),
resolve: {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.indexPatternId)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
},
controllerAs: 'fieldSettings',
controller: function FieldEditorPageController($route, Private, Notifier, docTitle) {
var Field = Private(require('components/index_patterns/_field'));
var notify = new Notifier({ location: 'Field Editor' });
var kbnUrl = Private(require('components/url/url'));
this.mode = $route.current.mode;
this.indexPattern = $route.current.locals.indexPattern;
if (this.mode === 'edit') {
var fieldName = $route.current.params.fieldName;
this.field = this.indexPattern.fields.byName[fieldName];
if (!this.field) {
notify.error(this.indexPattern + ' does not have a "' + fieldName + '" field.');
kbnUrl.redirectToRoute(this.indexPattern, 'edit');
return;
}
}
else if (this.mode === 'create') {
this.field = new Field(this.indexPattern, {
scripted: true,
type: 'number'
});
}
else {
throw new Error('unknown fieldSettings mode ' + this.mode);
}
docTitle.change([this.field.name || 'New Scripted Field', this.indexPattern.id]);
this.goBack = function () {
kbnUrl.changeToRoute(this.indexPattern, 'edit');
};
}
});
});

View file

@ -1,8 +1,8 @@
<span>{{field.displayName}}</span>
&nbsp;
<span
ng-if="indexPattern.timeFieldName === field.name"
tooltip="This field represents the time that events occurred."
class="label label-default">
<i aria-hidden="true" class="fa fa-clock-o"></i>
</span>
ng-if="indexPattern.timeFieldName === field.name"
tooltip="This field represents the time that events occurred."
class="label label-default">
<i aria-hidden="true" class="fa fa-clock-o"></i>
</span>

View file

@ -6,4 +6,4 @@
<span aria-label="Minus" ng-click="indexPattern.popularizeField(field.name, -1)"
class="label label-default"><i aria-hidden="true" class="fa fa-minus"></i></span>
</span>
</div>'
</div>

View file

@ -1,7 +1,7 @@
<span>{{field.type}}</span>
<i
aria-label="The type of this field changes across indices. It is unavailable for many analysis functions."
ng-if="field.type == 'conflict'"
tooltip="The type of this field changes across indices. It is unavailable for many analysis functions."
class="fa fa-warning text-color-warning">
</i>
aria-label="The type of this field changes across indices. It is unavailable for many analysis functions."
ng-if="field.type == 'conflict'"
tooltip="The type of this field changes across indices. It is unavailable for many analysis functions."
class="fa fa-warning text-color-warning">
</i>

View file

@ -0,0 +1,35 @@
<div class="index-pattern-name">
<h1 class="title" css-truncate>
<i aria-hidden="true" ng-if="defaultIndex === indexPattern.id" class="fa fa-star"></i>
{{indexPattern.id}}
</h1>
<div class="controls">
<button
ng-if="setDefault"
ng-click="setDefault()"
tooltip="Set as default index"
class="btn btn-success">
<span class="sr-only">Set as default index</span>
<i aria-hidden="true" class="fa fa-star"></i>
</button>
<button
ng-if="refreshFields"
confirm-click="refreshFields()"
confirmation="This will reset the field popularity counters. Are you sure you want to reload your fields?"
tooltip="Reload field list"
class="btn btn-warning">
<span class="sr-only">Reload field list</span>
<i aria-hidden="true" class="fa fa-refresh"></i>
</button>
<button
ng-if="delete"
confirm-click="delete()"
aria-label="Remove index pattern"
confirmation="Are you sure you want to remove this index pattern?"
tooltip="Remove index pattern"
class="btn btn-danger">
<span class="sr-only">Remove index pattern</span>
<i aria-hidden="true" class="fa fa-trash"></i>
</button>
</div>
</div>

View file

@ -0,0 +1,22 @@
define(function (require) {
require('modules')
.get('apps/settings')
.directive('kbnSettingsIndexHeader', function (config) {
return {
restrict: 'E',
template: require('text!plugins/settings/sections/indices/_index_header.html'),
scope: {
indexPattern: '=',
setDefault: '&',
refreshFields: '&',
delete: '&'
},
link: function ($scope, $el, attrs) {
$scope.delete = attrs.delete ? $scope.delete : null;
$scope.setDefault = attrs.setDefault ? $scope.setDefault : null;
$scope.refreshFields = attrs.refreshFields ? $scope.refreshFields : null;
config.$bind($scope, 'defaultIndex');
}
};
});
});

View file

@ -4,34 +4,35 @@ define(function (require) {
require('modules').get('apps/settings')
.directive('indexedFields', function () {
var yesTemplate = '<i class="fa fa-check" aria-label="yes"></i>';
var noTemplate = '';
var nameHtml = require('text!plugins/settings/sections/indices/_field_name.html');
var typeHtml = require('text!plugins/settings/sections/indices/_field_type.html');
var popularityHtml = require('text!plugins/settings/sections/indices/_field_popularity.html');
var controlsHtml = require('text!plugins/settings/sections/indices/_field_controls.html');
return {
restrict: 'E',
template: require('text!plugins/settings/sections/indices/_indexed_fields.html'),
scope: true,
link: function ($scope, $el, attr) {
link: function ($scope) {
var rowScopes = []; // track row scopes, so they can be destroyed as needed
$scope.perPage = 25;
$scope.popularityField = {name: null};
$scope.columns = [
{ title: 'name' },
{ title: 'type' },
{ title: 'format' },
{ title: 'analyzed', info: 'Analyzed fields may require extra memory to visualize' },
{ title: 'indexed', info: 'Fields that are not indexed are unavailable for search' },
{ title: 'popularity', info: 'A gauge of how often this field is used' }
{ title: 'controls', sortable: false }
];
$scope.$watchCollection('indexPattern.fields', function () {
_.invoke(rowScopes, '$destroy');
// clear and destroy row scopes
_.invoke(rowScopes.splice(0), '$destroy');
$scope.rows = $scope.indexPattern.getFields().map(function (field) {
var childScope = $scope.$new();
$scope.rows = $scope.indexPattern.getNonScriptedFields().map(function (field) {
var childScope = _.assign($scope.$new(), { field: field });
rowScopes.push(childScope);
childScope.field = field;
return [
{
@ -44,12 +45,18 @@ define(function (require) {
scope: childScope,
value: field.type
},
field.analyzed,
field.indexed,
_.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']),
{
markup: popularityHtml,
scope: childScope,
value: field.count
markup: field.analyzed ? yesTemplate : noTemplate,
value: field.analyzed
},
{
markup: field.indexed ? yesTemplate : noTemplate,
value: field.indexed
},
{
markup: controlsHtml,
scope: childScope
}
];
});
@ -57,4 +64,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -2,10 +2,13 @@
<p>These scripted fields are computed on the fly from your data. They can be used in visualizations and displayed in your documents, however they can not be searched. You can manage them here and add new ones as you see fit, but be careful, scripts can be tricky! <!-- If you need some examples, why not let Kibana <a ng-click="addDateScripts()"><strong>create a few examples from your date fields.</strong></a --></p>
<header>
<button ng-click="create()" class="btn btn-info" aria-label="Add Scripted Field">
<a
ng-href="{{ kbnUrl.getRouteHref(indexPattern, 'addField') }}"
class="btn btn-info"
aria-label="Add Scripted Field">
<i aria-hidden="true" class="fa fa-plus"></i>
Add Scripted Field
</button>
</a>
</header>
<paginated-table

View file

@ -5,8 +5,7 @@ define(function (require) {
require('modules').get('apps/settings')
.directive('scriptedFields', function (kbnUrl, Notifier) {
var rowScopes = []; // track row scopes, so they can be destroyed as needed
var popularityHtml = require('text!plugins/settings/sections/indices/_field_popularity.html');
var controlsHtml = require('text!plugins/settings/sections/indices/_scripted_field_controls.html');
var controlsHtml = require('text!plugins/settings/sections/indices/_field_controls.html');
var notify = new Notifier();
@ -14,20 +13,17 @@ define(function (require) {
restrict: 'E',
template: require('text!plugins/settings/sections/indices/_scripted_fields.html'),
scope: true,
link: function ($scope, $el, attr) {
link: function ($scope) {
var dateScripts = require('plugins/settings/sections/indices/_date_scripts');
var fieldCreatorPath = '/settings/indices/{{ indexPattern }}/scriptedField';
var fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}';
$scope.perPage = 25;
$scope.popularityField = {name: null};
$scope.columns = [
{ title: 'name' },
{ title: 'script' },
{ title: 'type' },
{ title: 'popularity', info: 'A gauge of how often this field is used' },
{ title: 'format' },
{ title: 'controls', sortable: false }
];
@ -35,21 +31,20 @@ define(function (require) {
_.invoke(rowScopes, '$destroy');
rowScopes.length = 0;
$scope.rows = $scope.indexPattern.getFields('scripted').map(function (field) {
$scope.rows = $scope.indexPattern.getScriptedFields().map(function (field) {
var rowScope = $scope.$new();
var columns = [field.name, field.script, field.type];
rowScope.field = field;
rowScopes.push(rowScope);
columns.push({
markup: popularityHtml,
scope: rowScope
}, {
markup: controlsHtml,
scope: rowScope
});
return columns;
return [
field.name,
field.script,
_.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']),
{
markup: controlsHtml,
scope: rowScope
}
];
});
});
@ -97,4 +92,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -35,4 +35,4 @@
</div>
</div>
<div class="col-md-10" ng-transclude></div>
<div class="col-md-10" ng-transclude></div>

View file

@ -1,30 +1,32 @@
define(function (require) {
var _ = require('lodash');
require('plugins/settings/sections/indices/scripted_fields/index');
require('plugins/settings/sections/indices/_create');
require('plugins/settings/sections/indices/_edit');
require('plugins/settings/sections/indices/_field_editor');
// add a dependency to all of the subsection routes
require('routes')
.addResolves(/settings\/indices/, {
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
.defaults(/settings\/indices/, {
resolve: {
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
}
}
});
// wrapper directive, which sets some global stuff up like the left nav
require('modules').get('apps/settings')
.directive('kbnSettingsIndices', function ($route, config, kbnUrl, $rootScope) {
.directive('kbnSettingsIndices', function ($route, config, kbnUrl) {
return {
restrict: 'E',
transclude: true,
template: require('text!plugins/settings/sections/indices/index.html'),
link: function ($scope) {
$scope.edittingId = $route.current.params.id;
$scope.edittingId = $route.current.params.indexPatternId;
config.$bind($scope, 'defaultIndex');
$scope.$watch('defaultIndex', function (defaultIndex) {
$scope.$watch('defaultIndex', function () {
$scope.indexPatternList = _($route.current.locals.indexPatternIds)
.map(function (id) {
return {

View file

@ -1,70 +0,0 @@
<kbn-settings-app section="indices">
<kbn-settings-indices>
<div ng-controller="scriptedFieldsEdit">
<h1>{{ action }} Scripted Field</h1>
<div>
<p>
By default, Elasticsearch scripts use <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions <i class="fa-link fa"></i></a>, which is a lot like JavaScript, but limited to basic arithmetic, bitwise and comparison operations. We'll let you do some reading on <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts">Lucene Expressions<i class="fa-link fa"></i></a> To access values in the document use the following format:
<h4><code>doc['some_field'].value</code></h4>
</p>
<p>There are a few limitations when using Lucene Expressions:
<ul>
<li>Only numeric fields may be accessed</li>
<li> Stored fields are not available </li>
<li> If a field is sparse (only some documents contain a value), documents missing the field will have a value of 0 </li>
</ul>
</p>
<p>
Here are all the operations available to scripted fields:
<ul>
<li> Arithmetic operators: + - * / % </li>
<li> Bitwise operators: | & ^ ~ << >> >>> </li>
<li> Boolean operators (including the ternary operator): && || ! ?: </li>
<li> Comparison operators: < <= == >= > </li>
<li> Common mathematic functions: abs ceil exp floor ln log10 logn max min sqrt pow </li>
<li> Trigonometric library functions: acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan </li>
<li> Distance functions: haversin </li>
<li> Miscellaneous functions: min, max </li>
</ul>
</div>
<div class="bs-callout bs-callout-warning">
<h4>Proceed with caution</h4>
<p>Scripted fields can be used to display and aggregate calculated values. As such,
they can be very slow, and if done incorrectly, can cause Kibana to be unusable. There's no safety net here. If you make a typo, unexpected exceptions will be thrown all over the place!</p>
<p></p>
</div>
<form name="scriptedFieldForm" ng-submit="submit()">
<div class="form-group">
<label>Name</label>
<input required type="text" ng-model="scriptedField.name" class="form-control span12">
</div>
<div class="form-group">
<label>Script <small>Please familiarize yourself with <a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html#search-request-script-fields">script fields <i class="fa-link fa"></i></a> and with
<a target="_window" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-script">scripts in aggregations <i class="fa-link fa"></i></a>
before using scripted fields.</small></label>
<textarea required class="scripted-field-script form-control span12" ng-model="scriptedField.script"></textarea>
</div>
<div ng-if="namingConflict" class="alert alert-danger">
You already have a field with the name {{ scriptedField.name }}. Naming your scripted
field with the same name means you won't be able to query both fields at the same time.
</div>
<div class="form-group">
<button class="btn btn-primary" type="button" ng-click="goBack()">Cancel</button>
<button class="btn btn-success" type="submit">
Save Scripted Field
</button>
</div>
</form>
</div>
</kbn-settings-indices>
</kbn-settings-app>

View file

@ -1,84 +0,0 @@
define(function (require) {
var _ = require('lodash');
require('plugins/settings/sections/indices/_indexed_fields');
require('plugins/settings/sections/indices/_scripted_fields');
require('routes')
.addResolves(/settings\/indices\/(.+)\/scriptedField/, {
indexPattern: function ($route, courier) {
return courier.indexPatterns.get($route.current.params.id)
.catch(courier.redirectWhenMissing('/settings/indices'));
}
})
.when('/settings/indices/:id/scriptedField', {
template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'),
})
.when('/settings/indices/:id/scriptedField/:field', {
template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'),
});
require('modules').get('apps/settings')
.controller('scriptedFieldsEdit', function ($scope, $route, $window, Notifier, Private, kbnUrl) {
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var indexPatternPath = '/settings/indices/{{ indexPattern }}?_a=(tab:scriptedFields)';
var notify = new Notifier();
var createMode = (!$route.current.params.field);
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.fieldTypes = fieldTypes;
if (createMode) {
$scope.action = 'Create';
} else {
var scriptName = $route.current.params.field;
$scope.action = 'Edit';
$scope.scriptedField = _.find($scope.indexPattern.fields, {
name: scriptName,
scripted: true
});
}
$scope.goBack = function () {
kbnUrl.change(indexPatternPath, {
indexPattern: $scope.indexPattern.id
});
};
$scope.submit = function () {
var field = _.defaults($scope.scriptedField, {
type: 'number',
lang: 'expression'
});
try {
if (createMode) {
$scope.indexPattern.addScriptedField(field.name, field.script, field.type, field.lang);
} else {
$scope.indexPattern.save();
}
notify.info('Scripted field \'' + $scope.scriptedField.name + '\' successfully saved');
$scope.goBack();
} catch (e) {
notify.error(e.message);
}
};
$scope.$watch('scriptedField.name', function (name) {
checkConflict(name);
});
function checkConflict(name) {
var match = _.find($scope.indexPattern.getFields(), {
name: name
});
if (match) {
$scope.namingConflict = true;
} else {
$scope.namingConflict = false;
}
}
});
});

View file

@ -155,29 +155,31 @@ kbn-settings-objects-view {
.flex(4, 0, auto);
}
}
.scripted-field-script {
font-family: @font-family-monospace;
}
}
kbn-settings-indices .fields {
& th:first-child,
& td:first-child {
width: 35%;
}
}
kbn-settings-indices {
.fields {
table {
.table-striped()
}
kbn-settings-indices .scripted-fields {
& header {
th:last-child,
td:last-child {
text-align: right;
}
}
.indexed-fields {
th:first-child,
td:first-child {
width: 35%;
}
}
.scripted-fields header {
margin: 5px 0;
text-align: right;
}
& th:last-child,
& td:last-child {
text-align: right;
}
}

View file

@ -110,7 +110,7 @@
font-weight: bold;
border: inherit !important;
background-color: @gray-lighter;
margin-bottom: 5px;
margin-bottom: @vis-editor-agg-editor-spacing;
padding: 2px 5px !important;
}
@ -118,13 +118,18 @@
color: @text-color !important;
background-color: @gray-lighter !important;
}
.hintbox {
padding: @vis-editor-agg-editor-spacing;
margin-bottom: @vis-editor-agg-editor-spacing;
}
}
.visualization-options {
padding: @vis-editor-agg-editor-spacing;
.form-group {
margin-bottom: 5px;
margin-bottom: @vis-editor-agg-editor-spacing;
}
.form-horizontal .control-label {
text-align: left;

View file

@ -0,0 +1,89 @@
define(function (require) {
var _ = require('lodash');
return require('registry/_registry')({
name: 'fieldFormats',
index: ['id'],
group: ['fieldType'],
constructor: function (config, $rootScope) {
var self = this;
var defaultMap;
function init() {
parseDefaultTypeMap();
$rootScope.$on('init:config', parseDefaultTypeMap);
$rootScope.$on('change:config.format:defaultTypeMap', parseDefaultTypeMap);
}
/**
* 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}
*/
self.getDefaultConfig = function (fieldType) {
return defaultMap[fieldType] || defaultMap._default_;
};
/**
* Get a FieldFormat type (class) by it's id.
*
* @param {String} formatId - the format id
* @return {Function}
*/
self.getType = function (formatId) {
return self.byId[formatId];
};
/**
* Get the default FieldFormat type (class) for
* a field type, using the format:defaultTypeMap.
*
* @param {String} fieldType
* @return {Function}
*/
self.getDefaultType = function (fieldType) {
return self.byId[self.getDefaultConfig(fieldType).id];
};
/**
* Get the singleton instance of the FieldFormat type by it's id.
*
* @param {String} formatId
* @return {FieldFormat}
*/
self.getInstance = _.memoize(function (formatId) {
var FieldFormat = self.byId[formatId];
return new FieldFormat();
});
/**
* Get the default fieldFormat instance for a field format.
*
* @param {String} fieldType
* @return {FieldFormat}
*/
self.getDefaultInstance = _.memoize(function (fieldType) {
var conf = self.getDefaultConfig(fieldType);
var FieldFormat = self.byId[conf.id];
return new FieldFormat(conf.params);
});
function parseDefaultTypeMap() {
defaultMap = config.get('format:defaultTypeMap');
_.forOwn(self, function (fn) {
if (_.isFunction(fn) && fn.cache) {
// clear all memoize caches
fn.cache = {};
}
});
}
init();
}
});
});

View file

@ -1,13 +1,12 @@
.hintbox-label,
.hintbox-label[ng-click] {
cursor: help;
}
@hintbox-background-color: @gray-lighter;
@hintbox-spacing-vertical: 10px;
@hintbox-spacing-horizontal: 12px;
.hintbox {
padding: @hintbox-spacing-vertical @hintbox-spacing-horizontal;
border-radius: 5px;
background-color: @gray-lighter;
padding: 5px;
margin-bottom: 5px;
margin-bottom: @hintbox-spacing-vertical;
background-color: @hintbox-background-color;
a {
color: @link-color !important;
@ -16,11 +15,27 @@
color: @text-color !important;
}
}
}
.hintbox p {
margin-bottom: 0;
pre {
background-color: white;
}
&-label,
&-label[ng-click] {
cursor: help;
}
ul, ol {
padding-left: 25px;
}
// inspired by Bootstrap alerts component
// https://github.com/twbs/bootstrap/blob/063c1b0780ea0240e4adce4c88d57fc23e099475/less/alerts.less#L27-L35
> * {
margin: 0;
}
> * + * {
margin-top: @hintbox-spacing-vertical;
}
}
.hintbox p + p {
margin-top: 5px;
}

View file

@ -79,6 +79,15 @@ button {
color: @brand-danger;
}
.text-monospace {
font-family: @font-family-monospace;
}
code {
word-break: break-all;
word-wrap: break-word;
}
// alias for alert types - allows class="fa fa-{{alertType}}"
.fa-success:before { content: @fa-var-check; }
.fa-danger:before { content: @fa-var-exclamation-circle; }
@ -464,10 +473,29 @@ style-compile {
@import '../components/filter_bar/filter_bar.less';
.cell-hover {
background-color: white;
&-show {
// so that the cell doesn't change size on hover
visibility: hidden;
}
}
.cell-hover:hover {
background-color: @gray-lighter;
.cell-hover-show {
visibility: visible;
}
}
mark, .mark {
background-color: rgba(252, 229, 113, 1);
}
fieldset {
margin: @form-group-margin-bottom;
padding: @form-group-margin-bottom;
border: 1px solid @input-border;
border-radius: @input-border-radius;
}

View file

@ -44,15 +44,28 @@ define(function (require) {
},
/**
* Remove an element at a specific index from an array
* Patched version of _.remove that supports IndexedArrays
*
* @param {array} arr
* @param {number} index
* @return {array} arr
* @param {array} array
* @param {object|function|str} - a lodash selector/predicate
* @return {array} the elements that were removed
*/
remove: function (arr, index) {
arr.splice(index, 1);
return arr;
remove: function (array, where) {
var index = -1;
var length = array ? array.length : 0;
var result = [];
var callback = _.createCallback(where, this, 3);
while (++index < length) {
var value = array[index];
if (callback(value, index, array)) {
result.push(value);
array.splice(index--, 1);
length--;
}
}
return result;
},
/**
@ -90,7 +103,6 @@ define(function (require) {
* @return {object}
*/
flattenWith: function (dot, nestedObj, flattenArrays) {
var key; // original key
var stack = []; // track key stack
var flatObj = {};
(function flattenObj(obj) {

View file

@ -10,6 +10,7 @@ define(function (require) {
* of lodash.
*/
var _ = require('lodash_src');
var DOT_PREFIX_RE = /(.).+?\./g;
return {
/**
@ -151,25 +152,18 @@ define(function (require) {
* @param {number} count - the number of args to accept
* @return {Function}
*/
limit: function (context, fn, count) {
// syntax without context limit(fn, 1)
if (count == null && _.isNumeric(fn)) {
count = fn;
fn = context;
context = null;
}
ary: function (fn, count) {
count = count || 0;
// shortcuts for common paths
// !!!! PLEASE don't use more than two arg
if (count === 0) return function () { return fn.call(context); };
if (count === 1) return function (a) { return fn.call(context, a); };
if (count === 2) return function (a, b) { return fn.call(context, a, b); };
if (count === 0) return function () { return fn.call(this); };
if (count === 1) return function (a) { return fn.call(this, a); };
if (count === 2) return function (a, b) { return fn.call(this, a, b); };
// catch all version
return function () {
return fn.apply(context, [].slice.call(arguments, 0, count));
return fn.apply(this, _.first(arguments, count));
};
},
@ -181,6 +175,31 @@ define(function (require) {
*/
callEach: function (arr) {
_.invoke(arr, 'call');
},
/**
* Convert a value to a presentable string
* @param {any} val - the value to transform
* @return {string}
*/
asPrettyString: function (val) {
if (val === null || val === undefined) return ' - ';
switch (typeof val) {
case 'string': return val;
case 'object': return JSON.stringify(val, null, ' ');
default: return '' + val;
}
},
/**
* Convert a dot.notated.string into a short
* version (d.n.string)
*
* @param {string} str - the long string to convert
* @return {string}
*/
shortenDottedString: function (input) {
return typeof input !== 'string' ? input : input.replace(DOT_PREFIX_RE, '$1.');
}
};
});

View file

@ -1,5 +1,6 @@
define(function () {
return function addWordBreaks(text, minLineLength) {
text = text || '';
var lineSize = 0;
var newText = '';
var inHtmlTag = false;

View file

@ -1,17 +0,0 @@
define(function (require) {
var _ = require('lodash');
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'\'': '&#39;',
'"': '&quot;',
};
var regex = new RegExp('[' + _.keys(map).join('') + ']', 'g');
return function htmlEscape(text) {
return text.replace(regex, function (c) {
return map[c];
});
};
});

Some files were not shown because too many files have changed in this diff Show more