diff --git a/src/kibana/components/index_patterns/_cast_mapping_type.js b/src/kibana/components/index_patterns/_cast_mapping_type.js index 38b4076d95e2..e082caab47cb 100644 --- a/src/kibana/components/index_patterns/_cast_mapping_type.js +++ b/src/kibana/components/index_patterns/_cast_mapping_type.js @@ -1,30 +1,41 @@ define(function (require) { return function CastMappingTypeFn() { + var IndexedArray = require('utils/indexed_array/index'); + + castMappingType.types = new IndexedArray({ + index: ['name'], + group: ['type'], + immutable: true, + initialSet: [ + {name: 'string', type: 'string', group: 'base'}, + {name: 'date', type: 'date', group: 'base'}, + {name: 'boolean', type: 'boolean', group: 'base'}, + {name: 'float', type: 'number', group: 'number'}, + {name: 'double', type: 'number', group: 'number'}, + {name: 'integer', type: 'number', group: 'number'}, + {name: 'long', type: 'number', group: 'number'}, + {name: 'short', type: 'number', group: 'number'}, + {name: 'byte', type: 'number', group: 'number'}, + {name: 'token_count', type: 'number', group: 'number'}, + {name: 'geo_point', type: 'geo_point', group: 'geo'}, + {name: 'geo_shape', type: 'geo_shape', group: 'geo'}, + {name: 'ip', type: 'ip', group: 'other'}, + {name: 'attachment', type: 'attachment', group: 'other'}, + ] + }); + /** * Accepts a mapping type, and converts it into it's js equivilent * @param {String} type - the type from the mapping's 'type' field * @return {String} - the most specific type that we care for */ - return function castMappingType(type) { - switch (type) { - case 'float': - case 'double': - case 'integer': - case 'long': - case 'short': - case 'byte': - case 'token_count': - return 'number'; - case 'date': - case 'boolean': - case 'ip': - case 'attachment': - case 'geo_point': - case 'geo_shape': - return type; - default: // including 'string' - return 'string'; - } - }; + function castMappingType(name) { + var match = castMappingType.types.byName[name]; + + if (match) return match.type; + return 'string'; + } + + return castMappingType; }; }); \ No newline at end of file diff --git a/src/kibana/components/index_patterns/_index_pattern.js b/src/kibana/components/index_patterns/_index_pattern.js index dc42a019cd4e..872f8e33c075 100644 --- a/src/kibana/components/index_patterns/_index_pattern.js +++ b/src/kibana/components/index_patterns/_index_pattern.js @@ -24,8 +24,7 @@ define(function (require) { timeFieldName: 'string', intervalName: 'string', customFormats: 'json', - fields: 'json', - scriptedFields: 'json' + fields: 'json' }); function IndexPattern(id) { @@ -71,7 +70,7 @@ define(function (require) { _.assign(self, resp._source); if (self.id) { - if (!self.fields || !self.scriptedFields) { + if (!self.fields) { return self.refreshFields(); } else { setIndexedValue('fields'); @@ -99,6 +98,7 @@ define(function (require) { // non-enumerable type so that it does not get included in the JSON Object.defineProperties(field, { format: { + configurable: true, enumerable: false, get: function () { var formatName = self.customFormats && self.customFormats[field.name]; @@ -106,6 +106,7 @@ define(function (require) { } }, displayName: { + configurable: true, enumerable: false, get: function () { return shortDotsFilter(field.name); @@ -118,6 +119,26 @@ define(function (require) { }); } + self.addScriptedField = function (name, script, type) { + type = type || 'string'; + var scriptedField = self.fields.push({ + name: name, + script: script, + type: type, + scripted: true, + }); + self.save(); + }; + + self.removeScriptedField = function (name) { + var fieldIndex = _.findIndex(self.fields, { + name: name, + scripted: true + }); + self.fields.splice(fieldIndex, 1); + self.save(); + }; + self.popularizeField = function (fieldName, unit) { if (_.isUndefined(unit)) unit = 1; if (!(self.fields.byName && self.fields.byName[fieldName])) return; @@ -129,6 +150,13 @@ define(function (require) { self.save(); }; + self.getFields = function (type) { + var getScripted = (type === 'scripted'); + return _.where(self.fields, function (field) { + return field.scripted ? getScripted : !getScripted; + }); + }; + self.getInterval = function () { return this.intervalName && _.find(intervals, { name: this.intervalName }); }; @@ -172,7 +200,6 @@ define(function (require) { return mapper.clearCache(self) .then(function () { return self._fetchFields() - .then(self._fetchScriptedFields) .then(self.save); }); }; @@ -180,14 +207,12 @@ define(function (require) { self._fetchFields = function () { return mapper.getFieldsForIndexPattern(self, true) .then(function (fields) { + // append existing scripted fields + fields = fields.concat(self.getFields('scripted')); setIndexedValue('fields', fields); }); }; - self._fetchScriptedFields = function () { - setIndexedValue('scriptedFields', []); - }; - self.toJSON = function () { return self.id; }; diff --git a/src/kibana/components/index_patterns/_mapper.js b/src/kibana/components/index_patterns/_mapper.js index 458944f90ee8..43e279016a66 100644 --- a/src/kibana/components/index_patterns/_mapper.js +++ b/src/kibana/components/index_patterns/_mapper.js @@ -46,7 +46,8 @@ define(function (require) { } return es.indices.getFieldMapping({ - // TODO: Change index to be the resolved in some way, last three months, last hour, last year, whatever + // TODO: Change index to be the resolved in some way, + // last three months, last hour, last year, whatever index: indexList, field: '*', ignoreUnavailable: _.isArray(indexList), diff --git a/src/kibana/components/paginated_table/paginated_table.html b/src/kibana/components/paginated_table/paginated_table.html index 8262ffc98477..a1443d245ec2 100644 --- a/src/kibana/components/paginated_table/paginated_table.html +++ b/src/kibana/components/paginated_table/paginated_table.html @@ -14,6 +14,7 @@ {{ fieldType.title }} - ({{ indexPattern[fieldType.index].length }}) + ({{ fieldType.count }}) -
- - -
+ -
- No scripted fields defined -
+ diff --git a/src/kibana/plugins/settings/sections/indices/_edit.js b/src/kibana/plugins/settings/sections/indices/_edit.js index 4b401038ecc3..a0f1a78df57f 100644 --- a/src/kibana/plugins/settings/sections/indices/_edit.js +++ b/src/kibana/plugins/settings/sections/indices/_edit.js @@ -1,6 +1,7 @@ define(function (require) { var _ = require('lodash'); - require('components/paginated_table/paginated_table'); + require('plugins/settings/sections/indices/_indexed_fields'); + require('plugins/settings/sections/indices/_scripted_fields'); require('routes') .when('/settings/indices/:id', { @@ -14,10 +15,8 @@ define(function (require) { }); require('modules').get('apps/settings') - .controller('settingsIndicesEdit', function ($scope, $location, $route, $compile, - config, courier, Notifier, Private, AppState) { + .controller('settingsIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState) { - var rowScopes = []; // track row scopes, so they can be destroyed as needed var notify = new Notifier(); var $state = $scope.state = new AppState(); var popularityHtml = require('text!plugins/settings/sections/indices/_popularity.html'); @@ -26,52 +25,11 @@ define(function (require) { $scope.indexPattern = $route.current.locals.indexPattern; var otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id); - $scope.fieldTypes = Private(require('plugins/settings/sections/indices/_field_types')); - - $scope.fieldColumns = [{ - title: 'name' - }, { - title: 'type' - }, { - 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', - }]; - - $scope.showPopularityControls = function (field) { - $scope.popularityHoverState = (field) ? field : null; - }; - - $scope.$watchCollection('indexPattern.fields', function () { - _.invoke(rowScopes, '$destroy'); - - $scope.fieldRows = $scope.indexPattern.fields.map(function (field) { - var childScope = $scope.$new(); - rowScopes.push(childScope); - childScope.field = field; - - // update the active field via object comparison - if (_.isEqual(field, $scope.popularityHoverState)) { - $scope.showPopularityControls(field); - } - - return [field.name, field.type, field.analyzed, field.indexed, - { - markup: $compile(popularityHtml)(childScope), - value: field.count - } - ]; - }); + var fieldTypes = Private(require('plugins/settings/sections/indices/_field_types')); + $scope.$watch('indexPattern.fields', function () { + $scope.fieldTypes = fieldTypes($scope.indexPattern); }); - - $scope.perPage = 25; - $scope.changeTab = function (obj) { $state.tab = obj.index; $state.save(); diff --git a/src/kibana/plugins/settings/sections/indices/_field_types.js b/src/kibana/plugins/settings/sections/indices/_field_types.js index 4822cdbfbb83..ad239fdb5630 100644 --- a/src/kibana/plugins/settings/sections/indices/_field_types.js +++ b/src/kibana/plugins/settings/sections/indices/_field_types.js @@ -1,13 +1,26 @@ define(function (require) { - return function GetFieldTyles($route) { - var indexPattern = $route.current.locals.indexPattern; + return function GetFieldTypes() { + var _ = require('lodash'); - return [{ - title: 'fields', - index: 'fields' - }, { - title: 'scripted fields', - index: 'scriptedFields' - }]; + return function (indexPattern) { + var fieldCount = _.countBy(indexPattern.fields, function (field) { + return (field.scripted) ? 'scripted' : 'indexed'; + }); + + _.defaults(fieldCount, { + indexed: 0, + scripted: 0 + }); + + return [{ + title: 'fields', + index: 'indexedFields', + count: fieldCount.indexed + }, { + title: 'scripted fields', + index: 'scriptedFields', + count: fieldCount.scripted + }]; + }; }; }); \ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/_indexed_fields.html b/src/kibana/plugins/settings/sections/indices/_indexed_fields.html new file mode 100644 index 000000000000..0f606e7e334f --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_indexed_fields.html @@ -0,0 +1,5 @@ + + diff --git a/src/kibana/plugins/settings/sections/indices/_indexed_fields.js b/src/kibana/plugins/settings/sections/indices/_indexed_fields.js new file mode 100644 index 000000000000..8871deed8cac --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_indexed_fields.js @@ -0,0 +1,60 @@ +define(function (require) { + var _ = require('lodash'); + require('components/paginated_table/paginated_table'); + + require('modules').get('apps/settings') + .directive('indexedFields', function ($compile) { + var popularityHtml = require('text!plugins/settings/sections/indices/_popularity.html'); + + return { + restrict: 'E', + template: require('text!plugins/settings/sections/indices/_indexed_fields.html'), + scope: true, + link: function ($scope, $el, attr) { + var rowScopes = []; // track row scopes, so they can be destroyed as needed + $scope.perPage = 25; + + $scope.columns = [{ + title: 'name' + }, { + title: 'type' + }, { + 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', + }]; + + $scope.showPopularityControls = function (field) { + $scope.popularityHoverState = (field) ? field : null; + }; + + $scope.$watchCollection('indexPattern.fields', function () { + _.invoke(rowScopes, '$destroy'); + + $scope.rows = $scope.indexPattern.getFields().map(function (field) { + var childScope = $scope.$new(); + rowScopes.push(childScope); + childScope.field = field; + + // update the active field via object comparison + if (_.isEqual(field, $scope.popularityHoverState)) { + $scope.showPopularityControls(field); + } + + return [field.name, field.type, field.analyzed, field.indexed, + { + markup: $compile(popularityHtml)(childScope), + value: field.count + } + ]; + }); + }); + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/_scripted_field_controls.html b/src/kibana/plugins/settings/sections/indices/_scripted_field_controls.html new file mode 100644 index 000000000000..fa3d29e84bf8 --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_scripted_field_controls.html @@ -0,0 +1,12 @@ +
+ + + +
\ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/_scripted_fields.html b/src/kibana/plugins/settings/sections/indices/_scripted_fields.html new file mode 100644 index 000000000000..dbc485458c6f --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_scripted_fields.html @@ -0,0 +1,14 @@ +
+ +
+ + + + +
No scripted fields
\ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/_scripted_fields.js b/src/kibana/plugins/settings/sections/indices/_scripted_fields.js new file mode 100644 index 000000000000..e89f47a0fcc2 --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/_scripted_fields.js @@ -0,0 +1,72 @@ +define(function (require) { + var _ = require('lodash'); + require('components/paginated_table/paginated_table'); + + require('modules').get('apps/settings') + .directive('scriptedFields', function ($compile, kbnUrl) { + var rowScopes = []; // track row scopes, so they can be destroyed as needed + var controlsHtml = require('text!plugins/settings/sections/indices/_scripted_field_controls.html'); + + return { + restrict: 'E', + template: require('text!plugins/settings/sections/indices/_scripted_fields.html'), + scope: true, + link: function ($scope, $el, attr) { + var fieldCreatorPath = '/settings/indices/{{ indexPattern }}/scriptedField'; + var fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}'; + + $scope.perPage = 25; + + $scope.columns = [{ + title: 'name' + }, { + title: 'script' + }, { + title: 'type' + }, { + title: 'controls', + sortable: false + }]; + + $scope.$watch('indexPattern.fields', function () { + _.invoke(rowScopes, '$destroy'); + rowScopes.length = 0; + + $scope.rows = $scope.indexPattern.getFields('scripted').map(function (field) { + var rowScope = $scope.$new(); + var columns = [field.name, field.script, field.type]; + rowScope.field = field; + rowScopes.push(rowScope); + + columns.push({ + markup: $compile(controlsHtml)(rowScope) + }); + + return columns; + }); + }); + + $scope.create = function () { + var params = { + indexPattern: $scope.indexPattern.id + }; + + kbnUrl.change(fieldCreatorPath, params); + }; + + $scope.edit = function (field) { + var params = { + indexPattern: $scope.indexPattern.id, + fieldName: field.name + }; + + kbnUrl.change(fieldEditorPath, params); + }; + + $scope.remove = function (field) { + $scope.indexPattern.removeScriptedField(field.name); + }; + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/index.html b/src/kibana/plugins/settings/sections/indices/index.html index f5bd3380af39..597dd77b48ae 100644 --- a/src/kibana/plugins/settings/sections/indices/index.html +++ b/src/kibana/plugins/settings/sections/indices/index.html @@ -32,4 +32,5 @@
+
\ No newline at end of file diff --git a/src/kibana/plugins/settings/sections/indices/index.js b/src/kibana/plugins/settings/sections/indices/index.js index f36d0c15d4cf..82bc4e569110 100644 --- a/src/kibana/plugins/settings/sections/indices/index.js +++ b/src/kibana/plugins/settings/sections/indices/index.js @@ -1,6 +1,7 @@ define(function (require) { var _ = require('lodash'); + require('plugins/settings/sections/indices/scripted_fields/index'); require('plugins/settings/sections/indices/_create'); require('plugins/settings/sections/indices/_edit'); diff --git a/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html new file mode 100644 index 000000000000..15d71c47b646 --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.html @@ -0,0 +1,44 @@ + + + +
+

{{ action }} Scripted Field

+ +
+

Proceed with caution

+ +

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.

+ +

Please familiarize yourself with script fields and with + scripts in aggregations + before using scripted fields.

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ You already have a field with the name {{ scriptedField.name }}. Naming your scripted + field with the same name means you won't be able to query both fields at the same time. +
+
+ + +
+
+
+
diff --git a/src/kibana/plugins/settings/sections/indices/scripted_fields/index.js b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.js new file mode 100644 index 000000000000..bba8b67f49e4 --- /dev/null +++ b/src/kibana/plugins/settings/sections/indices/scripted_fields/index.js @@ -0,0 +1,73 @@ +define(function (require) { + var _ = require('lodash'); + require('plugins/settings/sections/indices/_indexed_fields'); + require('plugins/settings/sections/indices/_scripted_fields'); + + require('routes') + .addResolves(/settings\/indices\/(.+)\/scriptedField/, { + indexPattern: function ($route, courier) { + return courier.indexPatterns.get($route.current.params.id) + .catch(courier.redirectWhenMissing('/settings/indices')); + } + }) + .when('/settings/indices/:id/scriptedField', { + template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'), + }) + .when('/settings/indices/:id/scriptedField/:field', { + template: require('text!plugins/settings/sections/indices/scripted_fields/index.html'), + }); + + require('modules').get('apps/settings') + .controller('scriptedFieldsEdit', function ($scope, $route, $window, Notifier, Private) { + var typeOptions = Private(require('components/index_patterns/_cast_mapping_type')); + var fieldEditorPath = '/settings/indices/{{ indexPattern }}/scriptedField'; + var notify = new Notifier(); + var createMode = (!$route.current.params.field); + + $scope.indexPattern = $route.current.locals.indexPattern; + $scope.indexTypes = typeOptions.types; + + if (createMode) { + $scope.action = 'Create'; + } else { + var scriptName = $route.current.params.field; + $scope.action = 'Edit'; + $scope.scriptedField = _.find($scope.indexPattern.fields, { + name: scriptName, + scripted: true + }); + } + + $scope.cancel = function () { + $window.history.back(); + }; + + $scope.submit = function () { + var field = $scope.scriptedField; + if (createMode) { + $scope.indexPattern.addScriptedField(field.name, field.script, field.type); + } else { + $scope.indexPattern.save(); + } + + notify.info('Scripted field \'' + $scope.scriptedField.name + '\' successfully saved'); + $window.history.back(); + }; + + $scope.$watch('scriptedField.name', function (name) { + checkConflict(name); + }); + + function checkConflict(name) { + var match = _.find($scope.indexPattern.getFields(), { + name: name + }); + + if (match) { + $scope.namingConflict = true; + } else { + $scope.namingConflict = false; + } + } + }); +}); \ No newline at end of file diff --git a/src/kibana/plugins/settings/styles/main.less b/src/kibana/plugins/settings/styles/main.less index aa52bbe826d9..03b6296d8ccc 100644 --- a/src/kibana/plugins/settings/styles/main.less +++ b/src/kibana/plugins/settings/styles/main.less @@ -136,6 +136,19 @@ kbn-settings-indices .fields { } } +kbn-settings-indices .scripted-fields { + & header { + margin: 5px 0; + text-align: right; + } + + & th:last-child, + & td:last-child { + text-align: right; + } +} + + .kbn-settings-indices-create { .time-and-pattern > div {} } diff --git a/test/unit/specs/components/index_pattern/_cast_mapping_type.js b/test/unit/specs/components/index_pattern/_cast_mapping_type.js index b996295d002f..87e0eb66bf2d 100644 --- a/test/unit/specs/components/index_pattern/_cast_mapping_type.js +++ b/test/unit/specs/components/index_pattern/_cast_mapping_type.js @@ -13,6 +13,10 @@ define(function (require) { expect(fn).to.be.a(Function); }); + it('should have a types property', function () { + expect(fn).to.have.property('types'); + }); + it('should cast numeric types to "number"', function () { var types = [ 'float',