Merge branch 'master' into pinned-filters

Conflicts:
	test/unit/specs/services/url.js
This commit is contained in:
Joe Fleming 2015-05-04 15:31:07 -07:00
commit 026911b86a
159 changed files with 4151 additions and 1633 deletions

View file

@ -49,7 +49,8 @@
"requirejs-text": "~2.0.10",
"lodash-deep": "spenceralger/lodash-deep#compat",
"marked": "~0.3.2",
"numeral": "~1.5.3"
"numeral": "~1.5.3",
"leaflet-draw": "~0.2.4"
},
"devDependencies": {}
}

View file

@ -2,6 +2,7 @@
= Kibana User Guide
:ref: http://www.elastic.co/guide/en/elasticsearch/reference/current
:shield: https://www.elastic.co/guide/en/shield/current
include::introduction.asciidoc[]

View file

@ -24,19 +24,26 @@ If you are using Shield to authenticate Elasticsearch users, you need to provide
the Kibana server with credentials so it can access the `.kibana` index and monitor
the cluster.
To configure credentials for the Kibana server, set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml`:
To configure credentials for the Kibana server:
----
# If your Elasticsearch is protected with basic auth:
kibana_elasticsearch_username: kibana4
kibana_elasticsearch_password: kibana4
----
For information about assigning the Kibana server the necessary permissions in Shield,
see https://www.elastic.co/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4]
. Assign the `kibana4_server` role to a user in Shield. For more information, see
{shield}/_shield_with_kibana_4.html[Configuring a Role for the Kibana 4 Server]
in the Shield documentation.
. Set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml` to specify the credentials
of the user you assigned the `kibana4_server`
role:
+
[source,text]
----
kibana_elasticsearch_username: kibana4-user
kibana_elasticsearch_password: kibana4-password
----
Kibana 4 users also need access to the `.kibana` index so they can save and load searches, visualizations, and dashboards.
For more information, see {shield}/_shield_with_kibana_4.html#kibana4-roles[Configuring Roles for Kibana 4 Users] in the Shield documentation.
[float]
[[enabling-ssl]]
=== Enabling SSL
@ -45,6 +52,7 @@ sends to Elasticsearch.
To encrypt communications between the browser and the Kibana server, you configure the `ssl_key_file `and `ssl_cert_file` properties in `kibana.yml`:
[source,text]
----
# SSL for outgoing requests from the Kibana Server (PEM formatted)
ssl_key_file: /path/to/your/server.key
@ -58,12 +66,15 @@ the Kibana server and Elasticsearch are encrypted.
To do this, you specify the HTTPS
protocol when you configure the Elasticsearch URL in `kibana.yml`:
[source,text]
----
elasticsearch: "https://<your_elasticsearch_host>.com:9200"
----
If you are using a self-signed certificate for Elasticsearch, set the `ca` property in
`kibana.yml` to specify the location of the PEM file. Setting the `ca` property lets you leave the `verify_ssl` option enabled.
[source,text]
----
# If you need to provide a CA certificate for your Elasticsarech instance, put
# the path of the pem file here.

View file

@ -1,5 +1,6 @@
define(function (require) {
var decodeGeoHash = require('utils/decode_geo_hash');
var _ = require('lodash');
function readRows(table, agg, index, chart) {
var geoJson = chart.geoJson;
@ -8,6 +9,7 @@ define(function (require) {
props.length = table.rows.length;
props.min = null;
props.max = null;
props.agg = agg;
table.rows.forEach(function (row) {
var geohash = row[index.geo].value;

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

@ -26,6 +26,7 @@ define(function (require) {
Private(require('./mapExists')),
Private(require('./mapMissing')),
Private(require('./mapQueryString')),
Private(require('./mapGeoBoundingBox')),
Private(require('./mapScript')),
Private(require('./mapDefault')) // ProTip: last one to get applied
];

View file

@ -0,0 +1,21 @@
define(function (require) {
var _ = require('lodash');
return function mapGeoBoundBoxProvider(Promise, courier) {
return function (filter) {
var key, value, topLeft, bottomRight, field;
if (filter.geo_bounding_box) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
key = _.keys(filter.geo_bounding_box)[0];
field = indexPattern.fields.byName[key];
topLeft = field.format.convert(filter.geo_bounding_box[field.name].top_left);
bottomRight = field.format.convert(filter.geo_bounding_box[field.name].bottom_right);
value = topLeft + ' to ' + bottomRight;
return { key: key, value: value };
});
}
return Promise.reject(filter);
};
};
});

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

@ -0,0 +1,19 @@
define(function (require) {
var _ = require('lodash');
return function () {
return function ($state) {
if (!_.isObject($state)) throw new Error ('pushFilters requires a state object');
return function (filter, negate, index) {
// Hierarchical and tabular data set their aggConfigResult parameter
// differently because of how the point is rewritten between the two. So
// we need to check if the point.orig is set, if not use try the point.aggConfigResult
var filters = _.clone($state.filters || []);
var pendingFilter = { meta: { negate: negate, index: index }};
_.extend(pendingFilter, filter);
filters.push(pendingFilter);
$state.filters = filters;
};
};
};
});

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

@ -1,46 +1,62 @@
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a flattened version
define(function (require) {
var _ = require('lodash');
return function FlattenHitProvider(config, $rootScope) {
var _ = require('lodash');
function flattenHit(indexPattern, hit) {
var flat = {};
var metaFields = config.get('metaFields');
$rootScope.$on('change:config.metaFields', function () {
metaFields = config.get('metaFields');
});
// recursively merge _source
var fields = indexPattern.fields.byName;
(function flatten(obj, keyPrefix) {
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
_.forOwn(obj, function (val, key) {
key = keyPrefix + key;
function flattenHit(indexPattern, hit) {
var flat = {};
if (flat[key] !== void 0) return;
// recursively merge _source
var fields = indexPattern.fields.byName;
(function flatten(obj, keyPrefix) {
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
_.forOwn(obj, function (val, key) {
key = keyPrefix + key;
var hasValidMapping = (fields[key] && fields[key].type !== 'conflict');
var isValue = !_.isPlainObject(val);
if (flat[key] !== void 0) return;
if (hasValidMapping || isValue) {
flat[key] = val;
return;
}
var hasValidMapping = (fields[key] && fields[key].type !== 'conflict');
var isValue = !_.isPlainObject(val);
flatten(val, key);
if (hasValidMapping || isValue) {
flat[key] = val;
return;
}
flatten(val, key);
});
}(hit._source));
// assign the meta fields
_.each(metaFields, function (meta) {
if (meta === '_source') return;
flat[meta] = hit[meta];
});
}(hit._source));
// assign the meta fields
_.each(indexPattern.metaFields, function (meta) {
flat[meta] = hit[meta];
});
// unwrap computed fields
_.forOwn(hit.fields, function (val, key) {
if (key[0] === '_' && !_.contains(metaFields, key)) return;
flat[key] = _.isArray(val) && val.length === 1 ? val[0] : val;
});
// unwrap computed fields
_.forOwn(hit.fields, function (val, key) {
flat[key] = val[0];
});
return flat;
}
return flat;
}
return function (indexPattern) {
function cachedFlatten(hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
}
return function cachedFlatten(indexPattern, hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
cachedFlatten.uncached = _.partial(flattenHit, indexPattern);
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,22 +1,22 @@
define(function (require) {
return function IndexPatternFactory(Private, timefilter, configFile, Notifier, shortDotsFilter, 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 = require('components/index_patterns/_flatten_hit');
var Field = Private(require('components/index_patterns/_field'));
var getComputedFields = require('components/index_patterns/_get_computed_fields');
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();
@ -25,27 +25,50 @@ 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();
self.init = function () {
// tell the docSource where to find the doc
docSource
.index(configFile.kibana_index)
.index(config.file.kibana_index)
.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
@ -68,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();
@ -83,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);
})
});
}
@ -135,7 +122,7 @@ define(function (require) {
if (!self.fields) {
return self.refreshFields();
} else {
setIndexedValue('fields');
initFields();
}
}
};
@ -143,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,
@ -184,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 () {
@ -222,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,11 +260,20 @@ define(function (require) {
};
self.metaFields = config.get('metaFields');
self.flattenHit = _.partial(flattenHit, self);
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,5 +1,5 @@
define(function (require) {
return function MapperService(Private, Promise, es, configFile, config) {
return function MapperService(Private, Promise, es, config) {
var _ = require('lodash');
var moment = require('moment');
@ -33,7 +33,7 @@ define(function (require) {
if (!skipIndexPatternCache) {
return es.get({
index: configFile.kibana_index,
index: config.file.kibana_index,
type: 'index-pattern',
id: id,
_sourceInclude: ['fields']

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,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var nextTick = require('utils/next_tick');
var $ = require('jquery');
var modules = require('modules');
var module = modules.get('kibana/notify');

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

@ -265,18 +265,9 @@ define(function (require) {
* @param thing {String} Data object key
* @returns {*} Data object value
*/
Data.prototype.get = function (thing) {
var data;
if (this.data.rows) {
data = this.data.rows;
} else if (this.data.columns) {
data = this.data.columns;
} else {
data = [this.data];
}
return _.pluck(data, thing)[0];
Data.prototype.get = function (thing, def) {
var source = (this.data.rows || this.data.columns || [this.data])[0];
return _.get(source, thing, def);
};
/**
@ -410,7 +401,7 @@ define(function (require) {
* @returns {*} Array of data objects with x, y, y0 keys
*/
Data.prototype.stackData = function (series) {
// SHould not stack values on line chart
// Should not stack values on line chart
if (this._attr.type === 'line') return series;
return this._attr.stack(series);
};

View file

@ -13,13 +13,17 @@ define(function (require) {
*/
function Dispatch(handler) {
var stockEvents = ['brush', 'click', 'hover', 'mouseup', 'mousedown', 'mouseover', 'mouseout'];
var customEvents = _.deepGet(handler, 'vis.eventTypes.enabled');
var eventTypes = customEvents ? stockEvents.concat(customEvents) : stockEvents;
if (!(this instanceof Dispatch)) {
return new Dispatch(handler);
}
this.handler = handler;
this.dispatch = d3.dispatch('brush', 'click', 'hover', 'mouseup',
'mousedown', 'mouseover', 'mouseout');
this.dispatch = d3.dispatch.apply(this, eventTypes);
}
/**

View file

@ -1,7 +1,5 @@
define(function (require) {
return function ColumnHandler(d3, Private) {
var $ = require('jquery');
var injectZeros = Private(require('components/vislib/components/zero_injection/inject_zeros'));
var Handler = Private(require('components/vislib/lib/handler/handler'));
var Data = Private(require('components/vislib/lib/data'));
@ -45,7 +43,8 @@ define(function (require) {
el : vis.el,
yMin : data.getYMin(),
yMax : data.getYMax(),
_attr: vis._attr
_attr: vis._attr,
yAxisFormatter: data.get('yAxisFormatter')
})
});
};

View file

@ -2,7 +2,6 @@ define(function (require) {
return function YAxisFactory(d3, Private) {
var _ = require('lodash');
var $ = require('jquery');
var numeral = require('numeral');
var errors = require('errors');
var ErrorHandler = Private(require('components/vislib/lib/_error_handler'));
@ -18,6 +17,7 @@ define(function (require) {
this.el = args.el;
this.yMin = args.yMin;
this.yMax = args.yMax;
this.yAxisFormatter = args.yAxisFormatter;
this._attr = args._attr || {};
}
@ -74,6 +74,10 @@ define(function (require) {
return [Math.max(1, yMin), yMax];
};
YAxis.prototype._setDefaultYExtents = function () {
return this._attr.defaultYExtents;
};
/**
* Returns the domain, i.e. the extent of the y axis
*
@ -83,6 +87,7 @@ define(function (require) {
* @returns {*[]}
*/
YAxis.prototype.getDomain = function (scale, yMin, yMax) {
if (this._setDefaultYExtents()) return [yMin, yMax];
if (scale === 'log') return this.returnLogDomain(yMin, yMax); // Negative values cannot be displayed with a log scale.
if (yMin === 0 && yMax === 0) return this.throwNoResultsError(); // yMin and yMax can never both be equal to zero
@ -97,24 +102,22 @@ define(function (require) {
* @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
*/
YAxis.prototype.getYScale = function (height) {
return this.yScale = this.getScaleType(this._attr.scale)
this.yScale = this.getScaleType(this._attr.scale)
.domain(this.getDomain(this._attr.scale, this.yMin, this.yMax))
.range([height, 0])
.nice();
.range([height, 0]);
// Nicing the scale, rounds values down or up to make the scale look better
// When defaultYExtents are selected, the extents (i.e. min and max) should
// be shown without any rounding.
if (!this._attr.defaultYExtents) return this.yScale.nice();
return this.yScale;
};
/**
* By default, d3.format('s') returns billion values
* with a `G` instead of a `B`. @method formatAxisLabel returns
* billion values with a B instead of a G. Else, it defaults
* to the d3.format('s') value.
*
* @method formatAxisLabel
* @param d {Number}
* @returns {*}
*/
YAxis.prototype.formatAxisLabel = function (d) {
return numeral(d).format('0.[0]a');
YAxis.prototype.tickFormat = function () {
var isPercentage = this._attr.mode === 'percentage';
if (isPercentage) return d3.format('%');
if (this.yAxisFormatter) return this.yAxisFormatter;
return d3.format('n');
};
/**
@ -126,16 +129,6 @@ define(function (require) {
*/
YAxis.prototype.getYAxis = function (height) {
var yScale = this.getYScale(height);
var isPercentage = (this._attr.mode === 'percentage');
var tickFormat;
if (isPercentage) {
tickFormat = d3.format('%');
} else if (this.yMax <= 100 && this.yMin >= -100 && !isPercentage) {
tickFormat = d3.format('n');
} else {
tickFormat = this.formatAxisLabel;
}
// y scale should never be `NaN`
if (!yScale || _.isNaN(yScale)) {
@ -145,7 +138,7 @@ define(function (require) {
// Create the d3 yAxis function
this.yAxis = d3.svg.axis()
.scale(yScale)
.tickFormat(tickFormat)
.tickFormat(this.tickFormat())
.ticks(this.tickScale(height))
.orient('left');

View file

@ -108,6 +108,10 @@
padding: 0 !important;
}
.leaflet-draw-tooltip {
display: none;
}
/* filter to desaturate mapquest tiles */
img.leaflet-tile {
@ -116,4 +120,4 @@ img.leaflet-tile {
img.leaflet-tile.filters-off {
.filter(none);
}
}

View file

@ -3,7 +3,9 @@ define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var L = require('leaflet');
require('leaflet-draw');
var Dispatch = Private(require('components/vislib/lib/dispatch'));
var Chart = Private(require('components/vislib/visualizations/_chart'));
var errors = require('errors');
@ -32,6 +34,8 @@ define(function (require) {
// track the map objects
this.maps = [];
this.events = new Dispatch(handler);
// add allmin and allmax to geoJson
chartData.geoJson.properties.allmin = chartData.geoJson.properties.min;
chartData.geoJson.properties.allmax = chartData.geoJson.properties.max;
@ -76,6 +80,21 @@ define(function (require) {
subdomains: '1234'
});
var drawOptions = {draw: {}};
_.each(['polyline', 'polygon', 'circle', 'marker', 'rectangle'], function (drawShape) {
if (!self.events.dispatch[drawShape]) {
drawOptions.draw[drawShape] = false;
} else {
drawOptions.draw[drawShape] = {
shapeOptions: {
stroke: false,
color: '#000'
}
};
}
});
var mapOptions = {
minZoom: 1,
maxZoom: 18,
@ -85,11 +104,15 @@ define(function (require) {
noWrap: true,
maxBounds: worldBounds,
scrollWheelZoom: false,
fadeAnimation: false
fadeAnimation: false,
};
var map = L.map(div[0], mapOptions);
if (data.geoJson.features.length) {
map.addControl(new L.Control.Draw(drawOptions));
}
tileLayer.on('tileload', function () {
self.saturateTiles();
});
@ -103,6 +126,31 @@ define(function (require) {
map.on('moveend', function setZoomCenter() {
mapZoom = self._attr.mapZoom = map.getZoom();
mapCenter = self._attr.mapCenter = map.getCenter();
featureLayer.clearLayers();
featureLayer = self.markerType(map, mapData).addTo(map);
});
map.on('draw:created', function (e) {
var drawType = e.layerType;
if (!self.events.dispatch[drawType]) return;
// TODO: Different drawTypes need differ info. Need a switch on the object creation
var bounds = e.layer.getBounds();
self.events.dispatch[drawType]({
e: e,
data: self.chartData,
bounds: {
top_left: {
lat: bounds.getNorthWest().lat,
lon: bounds.getNorthWest().lng
},
bottom_right: {
lat: bounds.getSouthEast().lat,
lon: bounds.getSouthEast().lng
}
}
});
});
// add label for splits
@ -134,6 +182,19 @@ define(function (require) {
};
};
/**
* Return features within the map bounds
*/
TileMap.prototype._filterToMapBounds = function (map) {
return function (feature) {
var coordinates = feature.geometry.coordinates;
var p0 = coordinates[0];
var p1 = coordinates[1];
return map.getBounds().contains([p1, p0]);
};
};
/**
* zoom map to fit all features in featureLayer
*
@ -215,7 +276,8 @@ define(function (require) {
},
style: function (feature) {
return self.applyShadingStyle(feature, min, max);
}
},
filter: self._filterToMapBounds(map)
});
// add legend
@ -254,7 +316,8 @@ define(function (require) {
},
style: function (feature) {
return self.applyShadingStyle(feature, min, max);
}
},
filter: self._filterToMapBounds(map)
});
// add legend
@ -309,7 +372,8 @@ define(function (require) {
},
style: function (feature) {
return self.applyShadingStyle(feature, min, max);
}
},
filter: self._filterToMapBounds(map)
});
// add legend
@ -352,6 +416,10 @@ define(function (require) {
*/
TileMap.prototype.addLegend = function (data, map) {
var self = this;
var isLegend = $('div.tilemap-legend').length;
if (isLegend) return; // Don't add Legend if already one
var legend = L.control({position: 'bottomright'});
legend.onAdd = function () {
var div = L.DomUtil.create('div', 'tilemap-legend');
@ -434,6 +502,7 @@ define(function (require) {
TileMap.prototype.bindPopup = function (feature, layer) {
var props = feature.properties;
var popup = L.popup({
className: 'leaflet-popup-kibana',
autoPan: false
})
.setContent(
@ -548,6 +617,9 @@ define(function (require) {
* @return {undefined}
*/
TileMap.prototype.destroy = function () {
// Cleanup hanging DOM nodes
$(this.chartEl).find('[class*=" leaflet"]').remove();
this.maps.forEach(function (map) {
map.remove();
});

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

@ -3,14 +3,28 @@
// 'foo.bar.baz'.replace(/(.+?\.)/g,function(v) {return v[0]+'.';});
define(function (require) {
var _ = require('lodash');
require('modules')
.get('kibana')
.filter('shortDots', function (config) {
return function (str) {
if (!_.isString(str) || config.get('shortDots:enable') !== true) {
return str;
}
return str.replace(/(.+?\.)/g, function (v) { return v[0] + '.'; });
};
.filter('shortDots', function (Private) {
return Private(shortDotsFilterProvider);
});
});
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) {
return filter(str);
};
}
return shortDotsFilterProvider;
});

View file

@ -349,38 +349,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

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