diff --git a/src/core_plugins/kibana/public/visualize/editor/__tests__/agg.js b/src/core_plugins/kibana/public/visualize/editor/__tests__/agg.js index df8d592098b7f..f8b1baeacedd8 100644 --- a/src/core_plugins/kibana/public/visualize/editor/__tests__/agg.js +++ b/src/core_plugins/kibana/public/visualize/editor/__tests__/agg.js @@ -47,7 +47,8 @@ describe('Vis-Editor-Agg plugin directive', function () { $parentScope.agg = { id: 1, params: {}, - schema: makeConfig() + schema: makeConfig(), + getFieldOptions: () => null }; $parentScope.groupName = 'metrics'; $parentScope.group = [{ diff --git a/src/core_plugins/kibana/public/visualize/editor/agg_params.js b/src/core_plugins/kibana/public/visualize/editor/agg_params.js index 81f40941305bf..12db500fd7cad 100644 --- a/src/core_plugins/kibana/public/visualize/editor/agg_params.js +++ b/src/core_plugins/kibana/public/visualize/editor/agg_params.js @@ -59,6 +59,7 @@ uiModules // create child scope, used in the editors $aggParamEditorsScope = $scope.$new(); + $aggParamEditorsScope.indexedFields = $scope.agg.getFieldOptions(); const agg = $scope.agg; if (!agg) return; @@ -81,10 +82,6 @@ uiModules // build collection of agg params html type.params.forEach(function (param, i) { let aggParam; - // if field param exists, compute allowed fields - if (param.name === 'field') { - $aggParamEditorsScope.indexedFields = getIndexedFields(param); - } if ($aggParamEditorsScope.indexedFields) { const hasIndexedFields = $aggParamEditorsScope.indexedFields.length > 0; @@ -136,30 +133,6 @@ uiModules .append(param.editor) .get(0); } - - function getIndexedFields(param) { - let fields = _.filter($scope.agg.vis.indexPattern.fields.raw, 'aggregatable'); - const fieldTypes = param.filterFieldTypes; - - if (fieldTypes) { - fields = $filter('fieldType')(fields, fieldTypes); - fields = $filter('orderBy')(fields, ['type', 'name']); - } - - return new IndexedArray({ - - /** - * @type {Array} - */ - index: ['name'], - - /** - * [group description] - * @type {Array} - */ - initialSet: fields - }); - } } }; }); diff --git a/src/fixtures/logstash_fields.js b/src/fixtures/logstash_fields.js index 824a66ba25094..4cdfdd43d4aa7 100644 --- a/src/fixtures/logstash_fields.js +++ b/src/fixtures/logstash_fields.js @@ -1,36 +1,65 @@ function stubbedLogstashFields() { - let sourceData = [ - { name: 'bytes', type: 'number', indexed: true, analyzed: true, sortable: true, filterable: true, count: 10 }, - { name: 'ssl', type: 'boolean', indexed: true, analyzed: true, sortable: true, filterable: true, count: 20 }, - { name: '@timestamp', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 }, - { name: 'time', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true, count: 30 }, - { name: '@tags', type: 'string', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'utc_time', type: 'date', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'phpmemory', type: 'number', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'ip', type: 'ip', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'request_body', type: 'attachment', indexed: true, analyzed: true, sortable: false, filterable: true }, - { name: 'point', type: 'geo_point', indexed: true, analyzed: true, sortable: false, filterable: false }, - { name: 'area', type: 'geo_shape', indexed: true, analyzed: true, sortable: true, filterable: false }, - { name: 'hashed', type: 'murmur3', indexed: true, analyzed: true, sortable: false, filterable: false }, - { name: 'geo.coordinates', type: 'geo_point', indexed: true, analyzed: true, sortable: false, filterable: true }, - { name: 'extension', type: 'string', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'machine.os', type: 'string', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: 'geo.src', type: 'string', indexed: true, analyzed: true, sortable: true, filterable: true }, - { name: '_type', type: 'string', indexed: false, analyzed: true, sortable: true, filterable: true }, - { name: '_id', type: 'string', indexed: false, analyzed: false, sortable: false, filterable: true}, - { name: '_source', type: 'string', indexed: false, analyzed: false, sortable: false, filterable: false}, - { name: 'custom_user_field', type: 'conflict', indexed: false, analyzed: false, sortable: false, filterable: true }, - { name: 'script string', type: 'string', scripted: true, script: '\'i am a string\'', lang: 'expression' }, - { name: 'script number', type: 'number', scripted: true, script: '1234', lang: 'expression' }, - { name: 'script date', type: 'date', scripted: true, script: '1234', lang: 'painless' }, - { name: 'script murmur3', type: 'murmur3', scripted: true, script: '1234', lang: 'expression'}, - ].map(function (field) { - field.count = field.count || 0; - field.scripted = field.scripted || false; - return field; - }); + return [ + // |indexed + // | |analyzed + // | | |aggregatable + // | | | |searchable + // name type | | | | |metadata + ['bytes', 'number', true, true, true, true, { count: 10 } ], + ['ssl', 'boolean', true, true, true, true, { count: 20 } ], + ['@timestamp', 'date', true, true, true, true, { count: 30 } ], + ['time', 'date', true, true, true, true, { count: 30 } ], + ['@tags', 'string', true, true, true, true ], + ['utc_time', 'date', true, true, true, true ], + ['phpmemory', 'number', true, true, true, true ], + ['ip', 'ip', true, true, true, true ], + ['request_body', 'attachment', true, true, true, true ], + ['point', 'geo_point', true, true, true, true ], + ['area', 'geo_shape', true, true, true, true ], + ['hashed', 'murmur3', true, true, false, true ], + ['geo.coordinates', 'geo_point', true, true, true, true ], + ['extension', 'string', true, true, true, true ], + ['machine.os', 'string', true, true, true, true ], + ['geo.src', 'string', true, true, true, true ], + ['_id', 'string', false, false, true, true ], + ['_type', 'string', false, false, true, true ], + ['_source', 'string', false, false, true, true ], + ['custom_user_field', 'conflict', false, false, true, true ], + ['script string', 'string', false, false, true, false, { script: '\'i am a string\'' } ], + ['script number', 'number', false, false, true, false, { script: '1234' } ], + ['script date', 'date', false, false, true, false, { script: '1234', lang: 'painless' } ], + ['script murmur3', 'murmur3', false, false, true, false, { script: '1234' } ], + ].map(function (row) { + const [ + name, + type, + indexed, + analyzed, + aggregatable, + searchable, + metadata = {} + ] = row; + + const { + count = 0, + script, + lang = script ? 'expression' : undefined, + scripted = !!script, + } = metadata; - return sourceData; + return { + name, + type, + indexed, + analyzed, + aggregatable, + searchable, + count, + script, + lang, + scripted, + }; + }); } export default stubbedLogstashFields; diff --git a/src/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/ui/public/agg_response/tabify/__tests__/_response_writer.js index 5f81ef8190d68..e6cf7cf035afd 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ b/src/ui/public/agg_response/tabify/__tests__/_response_writer.js @@ -51,9 +51,9 @@ describe('ResponseWriter class', function () { it('collects the aggConfigs from each column in aggStack', function () { let aggs = [ - { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, + { type: 'date_histogram', schema: 'segment', params: { field: 'time' } }, { type: 'terms', schema: 'segment', params: { field: 'extension' } }, - { type: 'avg', schema: 'metric', params: { field: '@timestamp' } } + { type: 'avg', schema: 'metric', params: { field: 'bytes' } } ]; getColumns.returns(aggs.map(function (agg) { diff --git a/src/ui/public/agg_types/__tests__/agg_type.js b/src/ui/public/agg_types/__tests__/agg_type.js index 94dbcab47d495..697a12403029d 100644 --- a/src/ui/public/agg_types/__tests__/agg_type.js +++ b/src/ui/public/agg_types/__tests__/agg_type.js @@ -81,8 +81,6 @@ describe('AggType Class', function () { describe('getFormat', function () { it('returns the formatter for the aggConfig', function () { - let aggType = new AggType({}); - let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ @@ -94,6 +92,7 @@ describe('AggType Class', function () { }); let aggConfig = vis.aggs.byTypeName.date_histogram[0]; + const aggType = aggConfig.type; expect(aggType.getFormat(aggConfig)).to.be(fieldFormat.getDefaultInstance('date')); diff --git a/src/ui/public/agg_types/buckets/significant_terms.js b/src/ui/public/agg_types/buckets/significant_terms.js index 9c16522849ecf..789df97ff00cf 100644 --- a/src/ui/public/agg_types/buckets/significant_terms.js +++ b/src/ui/public/agg_types/buckets/significant_terms.js @@ -16,6 +16,7 @@ export default function SignificantTermsAggDefinition(Private) { params: [ { name: 'field', + scriptable: false, filterFieldTypes: 'string' }, { diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js index b07453fd8c396..1a326ec8e8e81 100644 --- a/src/ui/public/agg_types/buckets/terms.js +++ b/src/ui/public/agg_types/buckets/terms.js @@ -43,7 +43,6 @@ export default function TermsAggDefinition(Private) { params: [ { name: 'field', - scriptable: true, filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] }, { diff --git a/src/ui/public/agg_types/param_types/field.js b/src/ui/public/agg_types/param_types/field.js index 8d42f40ae834b..c71ee03502387 100644 --- a/src/ui/public/agg_types/param_types/field.js +++ b/src/ui/public/agg_types/param_types/field.js @@ -2,9 +2,13 @@ import { SavedObjectNotFound } from 'ui/errors'; import _ from 'lodash'; import editorHtml from 'ui/agg_types/controls/field.html'; import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; -export default function FieldAggParamFactory(Private) { +import 'ui/filters/field_type'; +import IndexedArray from 'ui/indexed_array'; +import Notifier from 'ui/notify/notifier'; +export default function FieldAggParamFactory(Private, $filter) { let BaseAggParam = Private(AggTypesParamTypesBaseProvider); + const notifier = new Notifier(); _.class(FieldAggParam).inherits(BaseAggParam); function FieldAggParam(config) { @@ -12,7 +16,7 @@ export default function FieldAggParamFactory(Private) { } FieldAggParam.prototype.editor = editorHtml; - FieldAggParam.prototype.scriptable = false; + FieldAggParam.prototype.scriptable = true; FieldAggParam.prototype.filterFieldTypes = '*'; /** @@ -25,6 +29,32 @@ export default function FieldAggParamFactory(Private) { return field.name; }; + /** + * Get the options for this field from the indexPattern + */ + FieldAggParam.prototype.getFieldOptions = function (aggConfig) { + const indexPattern = aggConfig.getIndexPattern(); + let fields = indexPattern.fields.raw; + + fields = fields.filter(f => f.aggregatable); + + if (!this.scriptable) { + fields = fields.filter(field => !field.scripted); + } + + if (this.filterFieldTypes) { + fields = $filter('fieldType')(fields, this.filterFieldTypes); + } + + fields = $filter('orderBy')(fields, ['type', 'name']); + + return new IndexedArray({ + index: ['name'], + group: ['type'], + initialSet: fields + }); + }; + /** * Called to read values from a database record into the * aggConfig object @@ -33,13 +63,18 @@ export default function FieldAggParamFactory(Private) { * @return {field} */ FieldAggParam.prototype.deserialize = function (fieldName, aggConfig) { - let field = aggConfig.vis.indexPattern.fields.byName[fieldName]; + const field = aggConfig.getIndexPattern().fields.byName[fieldName]; if (!field) { throw new SavedObjectNotFound('index-pattern-field', fieldName); } - return field; + const validField = this.getFieldOptions(aggConfig).byName[fieldName]; + if (!validField) { + notifier.error(`"field" is invalid`); + } + + return validField; }; /** @@ -56,7 +91,7 @@ export default function FieldAggParamFactory(Private) { let field = aggConfig.getField(); if (!field) { - throw new Error(`"${aggConfig.makeLabel()}" requires a field`); + throw new TypeError('"field" is a required parameter'); } if (field.scripted) { diff --git a/src/ui/public/index_patterns/__tests__/_index_pattern.js b/src/ui/public/index_patterns/__tests__/_index_pattern.js index 2b6d7ca3d9e37..98b26aea61b06 100644 --- a/src/ui/public/index_patterns/__tests__/_index_pattern.js +++ b/src/ui/public/index_patterns/__tests__/_index_pattern.js @@ -12,8 +12,11 @@ import IndexPatternsMapperProvider from 'ui/index_patterns/_mapper'; import UtilsMappingSetupProvider from 'ui/utils/mapping_setup'; import IndexPatternsIntervalsProvider from 'ui/index_patterns/_intervals'; import IndexPatternsIndexPatternProvider from 'ui/index_patterns/_index_pattern'; +import NoDigestPromises from 'test_utils/no_digest_promises'; describe('index pattern', function () { + NoDigestPromises.activateForSuite(); + let IndexPattern; let mapper; let mappingSetup; @@ -55,7 +58,7 @@ describe('index pattern', function () { // stub calculateIndices calculateIndices = sinon.spy(function () { - return $injector.get('Promise').resolve([ + return Promise.resolve([ { index: 'foo', max: Infinity, min: -Infinity }, { index: 'bar', max: Infinity, min: -Infinity } ]); @@ -150,7 +153,6 @@ describe('index pattern', function () { describe('refresh fields', function () { // override the default indexPattern, with a truncated field list - require('test_utils/no_digest_promises').activateForSuite(); const indexPatternId = 'test-pattern'; let indexPattern; let fieldLength; @@ -321,7 +323,6 @@ describe('index pattern', function () { }); describe('#toDetailedIndexList', function () { - require('test_utils/no_digest_promises').activateForSuite(); context('when index pattern is an interval', function () { let interval; beforeEach(function () { @@ -400,7 +401,6 @@ describe('index pattern', function () { describe('#toIndexList', function () { context('when index pattern is an interval', function () { - require('test_utils/no_digest_promises').activateForSuite(); let interval; beforeEach(function () { @@ -431,7 +431,6 @@ describe('index pattern', function () { }); context('when index pattern is a time-base wildcard', function () { - require('test_utils/no_digest_promises').activateForSuite(); beforeEach(function () { sinon.stub(indexPattern, 'getInterval').returns(false); sinon.stub(indexPattern, 'hasTimeField').returns(true); @@ -453,7 +452,6 @@ describe('index pattern', function () { }); context('when index pattern is a time-base wildcard that is configured not to expand', function () { - require('test_utils/no_digest_promises').activateForSuite(); beforeEach(function () { sinon.stub(indexPattern, 'getInterval').returns(false); sinon.stub(indexPattern, 'hasTimeField').returns(true); @@ -472,13 +470,8 @@ describe('index pattern', function () { sinon.stub(indexPattern, 'getInterval').returns(false); }); - it('is fulfilled by id', function () { - let indexList; - indexPattern.toIndexList().then(function (val) { - indexList = val; - }); - $rootScope.$apply(); - + it('is fulfilled by id', async function () { + let indexList = await indexPattern.toIndexList(); expect(indexList).to.equal(indexPattern.id); }); }); diff --git a/src/ui/public/notify/__tests__/notifier.js b/src/ui/public/notify/__tests__/notifier.js index 2fd0a7fbe7e6d..02671423d9639 100644 --- a/src/ui/public/notify/__tests__/notifier.js +++ b/src/ui/public/notify/__tests__/notifier.js @@ -33,10 +33,13 @@ describe('Notifier', function () { beforeEach(function () { params = { location: 'foo' }; - while (Notifier.prototype._notifs.pop()); // clear global notifications notifier = new Notifier(params); }); + afterEach(function () { + Notifier.prototype._notifs.length = 0; // clear global notifications + }); + describe('#constructor()', function () { it('sets #from from given location', function () { expect(notifier.from).to.equal(params.location); @@ -465,13 +468,12 @@ describe('Directive Notification', function () { scope; }); - while (Notifier.prototype._notifs.pop()); // clear global notifications - notifier = new Notifier({ location: 'directiveFoo' }); directiveNotification = notifier.directive(directiveParam, customParams); }); afterEach(() => { + Notifier.prototype._notifs.length = 0; // clear global notifications directiveNotification.clear(); scope.$destroy(); }); diff --git a/src/ui/public/route_based_notifier/__tests__/index.js b/src/ui/public/route_based_notifier/__tests__/index.js index 20e26b1227a4f..39fb4ce083bb5 100644 --- a/src/ui/public/route_based_notifier/__tests__/index.js +++ b/src/ui/public/route_based_notifier/__tests__/index.js @@ -10,12 +10,15 @@ describe('ui/route_based_notifier', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(($injector) => { - remove(Notifier.prototype._notifs); // hack to reset the global notification array const Private = $injector.get('Private'); routeBasedNotifier = Private(routeBasedNotifierProvider); $rootScope = $injector.get('$rootScope'); })); + afterEach(() => { + Notifier.prototype._notifs.length = 0; // reset the global notification array + }); + describe('#warning()', () => { it('adds a warning notification', () => { routeBasedNotifier.warning('wat'); diff --git a/src/ui/public/test_harness/test_harness.js b/src/ui/public/test_harness/test_harness.js index 3c1cba2ea0327..0f33f84915855 100644 --- a/src/ui/public/test_harness/test_harness.js +++ b/src/ui/public/test_harness/test_harness.js @@ -6,6 +6,7 @@ import chrome from 'ui/chrome'; import Nonsense from 'Nonsense'; import sinon from 'sinon'; import _ from 'lodash'; +import Notifier from 'ui/notify/notifier'; import StackTraceMapper from 'ui/stack_trace_mapper'; import { parse } from 'url'; @@ -55,6 +56,13 @@ before(function () { sinon.useFakeXMLHttpRequest(); }); +beforeEach(function () { + if (Notifier.prototype._notifs.length) { + Notifier.prototype._notifs.length = 0; + throw new TypeError('notifications were left in the notifier'); + } +}); + /*** Kick off mocha, called at the end of test entry files ***/ exports.bootstrap = function () { diff --git a/src/ui/public/vis/__tests__/_agg_config.js b/src/ui/public/vis/__tests__/_agg_config.js index c69127e9d830a..edcaa6a1984a5 100644 --- a/src/ui/public/vis/__tests__/_agg_config.js +++ b/src/ui/public/vis/__tests__/_agg_config.js @@ -197,7 +197,7 @@ describe('AggConfig', function () { { type: 'count', schema: 'metric', - params: {field: '@timestamp'} + params: {field: 'time'} } ] }, { @@ -295,7 +295,7 @@ describe('AggConfig', function () { { type: 'count', schema: 'metric', - params: {field: '@timestamp'} + params: {field: 'time'} } ] }, @@ -305,7 +305,7 @@ describe('AggConfig', function () { { type: 'count', schema: 'metric', - params: {field: '@timestamp'} + params: {field: 'time'} }, { type: 'date_histogram', @@ -421,7 +421,7 @@ describe('AggConfig', function () { { type: 'date_histogram', schema: 'segment', - params: { field: '@timestamp' } + params: { field: 'time' } } ] }); @@ -433,7 +433,7 @@ describe('AggConfig', function () { { type: 'count', schema: 'metric', - params: { field: '@timestamp' } + params: { field: 'time' } } ] }); @@ -447,7 +447,7 @@ describe('AggConfig', function () { { type: 'date_histogram', schema: 'segment', - params: { field: '@timestamp' } + params: { field: 'time' } } ] }); @@ -464,7 +464,7 @@ describe('AggConfig', function () { { type: 'date_histogram', schema: 'segment', - params: { field: '@timestamp' } + params: { field: 'time' } } ] }); @@ -481,12 +481,12 @@ describe('AggConfig', function () { { type: 'avg', schema: 'metric', - params: { field: 'ssl' } + params: { field: 'bytes' } } ] }); - let field = indexPattern.fields.byName.ssl; + let field = indexPattern.fields.byName.bytes; expect(vis.aggs[0].fieldFormatter('html')).to.be(field.format.getConverterFor('html')); }); }); diff --git a/src/ui/public/vis/agg_config.js b/src/ui/public/vis/agg_config.js index d841d6e12e7c0..86a8872e3649f 100644 --- a/src/ui/public/vis/agg_config.js +++ b/src/ui/public/vis/agg_config.js @@ -148,13 +148,11 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { * @return {object} the new params object */ AggConfig.prototype.resetParams = function () { - let fieldParam = this.type && this.type.params.byName.field; let field; + const fieldOptions = this.getFieldOptions(); - if (fieldParam) { - let prevField = this.params.field; - let fieldOpts = fieldTypeFilter(this.vis.indexPattern.fields, fieldParam.filterFieldTypes); - field = _.contains(fieldOpts, prevField) ? prevField : null; + if (fieldOptions) { + field = fieldOptions.byName[this.fieldName()] || null; } return this.fillDefaults({ row: this.params.row, field: field }); @@ -286,6 +284,20 @@ export default function AggConfigFactory(Private, fieldTypeFilter) { return pre += this.type.makeLabel(this); }; + AggConfig.prototype.getIndexPattern = function () { + return this.vis.indexPattern; + }; + + AggConfig.prototype.getFieldOptions = function () { + const fieldParamType = this.type && this.type.params.byName.field; + + if (!fieldParamType || !fieldParamType.getFieldOptions) { + return null; + } + + return fieldParamType.getFieldOptions(this); + }; + AggConfig.prototype.fieldFormatter = function (contentType, defaultFormat) { let format = this.type && this.type.getFormat(this); if (format) return format.getConverterFor(contentType);